diff --git a/book-rent/src/main/java/com/example/demo/config/CompatiblePasswordEncoder.java b/book-rent/src/main/java/com/example/demo/config/CompatiblePasswordEncoder.java index cb30a48..6bd3d3c 100644 --- a/book-rent/src/main/java/com/example/demo/config/CompatiblePasswordEncoder.java +++ b/book-rent/src/main/java/com/example/demo/config/CompatiblePasswordEncoder.java @@ -1,37 +1,37 @@ package com.example.demo.config; - + import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; - + // データベースに保存されている平文パスワードと、BCryptでハッシュ化されたパスワードの両方を照合できるようにするクラス public class CompatiblePasswordEncoder implements PasswordEncoder { - + // パスワードを安全にハッシュ化するためのクラスを生成 private final BCryptPasswordEncoder bcrypt = new BCryptPasswordEncoder(); - + // パスワードのハッシュ化 @Override public String encode(CharSequence rawPassword) { return bcrypt.encode(rawPassword); } - + @Override public boolean matches( CharSequence rawPassword, String encodedPassword) { - + // 保存済みパスワードが存在するか確認 if (encodedPassword == null) { return false; } - + // 既存の平文パスワードに対応 if (!encodedPassword.startsWith("$2a$") && !encodedPassword.startsWith("$2b$") && !encodedPassword.startsWith("$2y$")) { return rawPassword.toString().equals(encodedPassword); } - + // BCrypt形式のパスワードに対応 return bcrypt.matches(rawPassword, encodedPassword); } diff --git a/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationFailureHandler.java b/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationFailureHandler.java index 512143c..7e64080 100644 --- a/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationFailureHandler.java +++ b/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationFailureHandler.java @@ -1,5 +1,5 @@ package com.example.demo.config; - + import java.io.IOException; import jakarta.servlet.ServletException; @@ -11,49 +11,49 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa import org.springframework.stereotype.Component; import com.example.demo.service.LoginAttemptService; - + // ログイン失敗時の処理 @Component public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { - + private LoginAttemptService loginAttemptService; - + public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { this.loginAttemptService = loginAttemptService; } - + @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { - + // 画面から入力されたメールアドレスを取得 String mailAddress = request.getParameter("mailAddress"); - + if (mailAddress != null && !mailAddress.isBlank()) { String mail = mailAddress.trim(); - + if (loginAttemptService.isBlocked(mail)) { getRedirectStrategy().sendRedirect( request, response, "/login?error=locked"); return; } - + // 失敗回数を1増やす loginAttemptService.loginFailed(mail); - + // すでにロック中なら失敗回数を増やさない if (loginAttemptService.isBlocked(mail)) { getRedirectStrategy().sendRedirect( request, response, "/login?error=locked"); return; } - + // あと何回失敗されるとロックされるか取得 int remaining = loginAttemptService.getRemainingAttempts(mail); - + getRedirectStrategy().sendRedirect( request, response, "/login?error=failed&remaining=" + remaining); } - + } -} +} \ No newline at end of file diff --git a/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationSuccessHandler.java b/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationSuccessHandler.java index b3adb27..087323c 100644 --- a/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationSuccessHandler.java +++ b/book-rent/src/main/java/com/example/demo/config/CustomAuthenticationSuccessHandler.java @@ -1,5 +1,5 @@ package com.example.demo.config; - + import java.io.IOException; import jakarta.servlet.ServletException; @@ -12,37 +12,37 @@ import org.springframework.stereotype.Component; import com.example.demo.common.Const; import com.example.demo.service.LoginAttemptService; - + @Component public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - + private final LoginAttemptService loginAttemptService; - + public CustomAuthenticationSuccessHandler( LoginAttemptService loginAttemptService) { this.loginAttemptService = loginAttemptService; setDefaultTargetUrl("/menu"); setAlwaysUseDefaultTargetUrl(true); } - + @Override public void onAuthenticationSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - + if (authentication.getPrincipal() instanceof CustomUserDetails userDetails) { String mail = userDetails.getUsername(); - + loginAttemptService.loginSucceeded(mail); - + request.getSession(true).setAttribute( Const.SESSION_LOGIN_USER, userDetails.getLoginUser()); } - + super.onAuthenticationSuccess(request, response, authentication); } } \ No newline at end of file diff --git a/book-rent/src/main/java/com/example/demo/config/CustomUserDetails.java b/book-rent/src/main/java/com/example/demo/config/CustomUserDetails.java index 852491f..d6f70f9 100644 --- a/book-rent/src/main/java/com/example/demo/config/CustomUserDetails.java +++ b/book-rent/src/main/java/com/example/demo/config/CustomUserDetails.java @@ -1,5 +1,5 @@ package com.example.demo.config; - + import java.util.Collection; import java.util.Collections; @@ -9,36 +9,36 @@ import org.springframework.security.core.userdetails.UserDetails; import com.example.demo.common.LoginUser; import com.example.demo.service.LoginAttemptService; - + // システムの社員情報をSpring Securityが扱えるクラスに変更 public class CustomUserDetails implements UserDetails { - + private final LoginUser loginUser; private final String password; // DBから取得した暗号化パスワードを保持 private final LoginAttemptService loginAttemptService; - + public CustomUserDetails(LoginUser loginUser, String password, LoginAttemptService loginAttemptService) { this.loginUser = loginUser; this.password = password; this.loginAttemptService = loginAttemptService; } - + @Override public String getPassword() { return this.password; } - + @Override public String getUsername() { return loginUser.getMailAddress(); } - + // アカウントのロック状態の確認 @Override public boolean isAccountNonLocked() { return !loginAttemptService.isBlocked(getUsername()); } - + // 管理者判定メソッド @Override @@ -46,29 +46,29 @@ public class CustomUserDetails implements UserDetails { String role = loginUser.isAdmin() ? "ROLE_ADMIN" : "ROLE_USER"; - + return Collections.singletonList( // 要素が一つだけの変更できないリストを作るメソッド new SimpleGrantedAuthority(role)); } - + @Override public boolean isAccountNonExpired() { return true; } - + @Override public boolean isCredentialsNonExpired() { return true; } - + @Override public boolean isEnabled() { return true; } - + // ログイン成功後にコントローラー等でLoginUserを取り出すためのゲッター public LoginUser getLoginUser() { return loginUser; } - -} + +} \ No newline at end of file diff --git a/book-rent/src/main/java/com/example/demo/config/SecurityConfig.java b/book-rent/src/main/java/com/example/demo/config/SecurityConfig.java index 3b7db6e..235f095 100644 --- a/book-rent/src/main/java/com/example/demo/config/SecurityConfig.java +++ b/book-rent/src/main/java/com/example/demo/config/SecurityConfig.java @@ -1,5 +1,5 @@ package com.example.demo.config; - + import org.springframework.boot.security.autoconfigure.web.servlet.PathRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -7,25 +7,25 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; - + @Configuration @EnableWebSecurity public class SecurityConfig { - + private final CustomAuthenticationFailureHandler failureHandler; private final CustomAuthenticationSuccessHandler successHandler; - + public SecurityConfig( CustomAuthenticationFailureHandler failureHandler, CustomAuthenticationSuccessHandler successHandler) { this.failureHandler = failureHandler; this.successHandler = successHandler; } - + @Bean public SecurityFilterChain securityFilterChain( HttpSecurity http) throws Exception { - + http .authorizeHttpRequests(authorize -> authorize .requestMatchers( @@ -47,10 +47,10 @@ public class SecurityConfig { .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") .permitAll()); - + return http.build(); } - + @Bean public PasswordEncoder passwordEncoder() { return new CompatiblePasswordEncoder(); diff --git a/book-rent/src/main/java/com/example/demo/controller/LoginController.java b/book-rent/src/main/java/com/example/demo/controller/LoginController.java index 87b6498..6bdc6c4 100644 --- a/book-rent/src/main/java/com/example/demo/controller/LoginController.java +++ b/book-rent/src/main/java/com/example/demo/controller/LoginController.java @@ -1,6 +1,5 @@ - package com.example.demo.controller; - + import jakarta.servlet.http.HttpSession; import org.springframework.stereotype.Controller; @@ -9,15 +8,15 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import com.example.demo.common.Const; - + @Controller public class LoginController { - + @GetMapping("/") public String root() { return "redirect:/login"; } - + @GetMapping("/login") public String loginForm( HttpSession session, @@ -25,21 +24,21 @@ public class LoginController { @RequestParam(name = "logout", required = false) String logout, @RequestParam(name = "remaining", required = false) Integer remaining, Model model) { - + // すでにログインしている場合はメニュー画面へ if (session.getAttribute(Const.SESSION_LOGIN_USER) != null) { return "redirect:/menu"; } - + // ログイン失敗時のメッセージ if ("locked".equals(error)) { model.addAttribute( "errorMessage", "ログインに5回連続で失敗したため、10分間アカウントをロックしました。" ); - + } else if ("failed".equals(error) && remaining != null) { - + if (remaining == 1) { model.addAttribute( "errorMessage", @@ -53,17 +52,17 @@ public class LoginController { + "回間違えると、10分間ログインできなくなります。" ); } - + } else if (error != null) { model.addAttribute( "errorMessage", "メールアドレスまたはパスワードが間違っています。" ); - + } else if (logout != null) { model.addAttribute("message", "ログアウトしました。"); } - + return "login"; } -} +} \ No newline at end of file diff --git a/book-rent/src/main/java/com/example/demo/service/CustomUserDetailsService.java b/book-rent/src/main/java/com/example/demo/service/CustomUserDetailsService.java index ff3e5a7..be96b55 100644 --- a/book-rent/src/main/java/com/example/demo/service/CustomUserDetailsService.java +++ b/book-rent/src/main/java/com/example/demo/service/CustomUserDetailsService.java @@ -1,5 +1,5 @@ package com.example.demo.service; - + import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -10,28 +10,28 @@ import com.example.demo.common.LoginUser; import com.example.demo.config.CustomUserDetails; import com.example.demo.entity.Employee; import com.example.demo.repository.EmployeeRepository; - + //Spring Securityから依頼を受けて、ログインする社員情報をDBから取得 @Service public class CustomUserDetailsService implements UserDetailsService { - + private final EmployeeRepository employeeRepository; private final LoginAttemptService loginAttemptService; - + public CustomUserDetailsService( EmployeeRepository employeeRepository, LoginAttemptService loginAttemptService) { this.employeeRepository = employeeRepository; this.loginAttemptService = loginAttemptService; } - + @Override public UserDetails loadUserByUsername(String maillAddress) throws UsernameNotFoundException { - + Employee employee = employeeRepository.findByMailAddressAndFlgRetirement(maillAddress.trim(), Const.RETIRE_NO) .orElseThrow(() -> new UsernameNotFoundException("認証に失敗しました。")); // ↑社員情報が見つからない場合は例外処理 - + // 取得したEmployeeをLoginUserに変換する LoginUser loginUser = new LoginUser( employee.getIdEmployee(), @@ -39,9 +39,9 @@ public class CustomUserDetailsService implements UserDetailsService { employee.getKnEmployee(), employee.getMailAddress(), employee.getFlgAdmin()); - + // Spring Security用の形式に変換 return new CustomUserDetails(loginUser, employee.getPassword(), loginAttemptService); } - -} + +} \ No newline at end of file diff --git a/book-rent/src/main/java/com/example/demo/service/LoginAttemptService.java b/book-rent/src/main/java/com/example/demo/service/LoginAttemptService.java index 3da38af..34db840 100644 --- a/book-rent/src/main/java/com/example/demo/service/LoginAttemptService.java +++ b/book-rent/src/main/java/com/example/demo/service/LoginAttemptService.java @@ -1,101 +1,101 @@ package com.example.demo.service; - + import java.time.LocalDateTime; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springframework.stereotype.Service; - + // 失敗回数とロック時間を管理 @Service public class LoginAttemptService { - + // 最大失敗回数 private static final int MAX_ATTEMPTS = 5; // ロック時間 private static final int LOCK_MINUTES = 10; - + // 失敗回数を保存 private final ConcurrentMap attempts = new ConcurrentHashMap<>(); - + public static class AttemptState { // 何回失敗したか private final int failedAttempts; // いつロックを解除するか private final LocalDateTime lockedUntil; - + private AttemptState(int failedAttempts, LocalDateTime lockedUntil) { this.failedAttempts = failedAttempts; this.lockedUntil = lockedUntil; } } - + private String normalize(String mailAddress) { return mailAddress.trim().toLowerCase(Locale.ROOT); } - + // ログイン失敗時 public void loginFailed(String mailAddress) { String mail = normalize(mailAddress); LocalDateTime now = LocalDateTime.now(); AttemptState current = attempts.get(mail); - + // ロック中なら何もしない if (current != null && current.lockedUntil != null - && now.isBefore(current.lockedUntil)) { +&& now.isBefore(current.lockedUntil)) { return; } - + // 失敗回数を数える int count = (current == null || current.lockedUntil != null) ? 1 : current.failedAttempts + 1; - + // 5回失敗したら10分間ロック LocalDateTime lockTime = count >= MAX_ATTEMPTS ? now.plusMinutes(LOCK_MINUTES) : null; - + // 失敗回数とロック時間を保存 attempts.put(mail, new AttemptState(count, lockTime)); } - + // 現在ロック中かどうか判定 public boolean isBlocked(String mailAddress) { String mail = normalize(mailAddress); AttemptState current = attempts.get(mail); - + if (current == null) { return false; } if (current.lockedUntil == null) { return false; } - + // 10分経過したらロックを解除 if (!LocalDateTime.now().isBefore(current.lockedUntil)) { attempts.remove(mail); return false; } - + return true; - + } - + // ログイン成功時に、過去の失敗回数をきれいにリセットする public void loginSucceeded(String mailAddress) { attempts.remove(normalize(mailAddress)); } - + public int getFailedAttempts(String mailAddress) { AttemptState state = attempts.get(normalize(mailAddress)); return state == null ? 0 : state.failedAttempts; } - + public int getRemainingAttempts(String mailAddress) { int failed = getFailedAttempts(mailAddress); return Math.max(0, MAX_ATTEMPTS - failed); } - -} + +} \ No newline at end of file