2020 年 5 月 14 日

IT Skills 波林

Polin WEI – 資訊工作者的技術手札

Spring Boot 客製登入失敗 ( Authentication Failure Handler ) 與訊息顯示

2 min read
spring_武嶺夜景

Spring Boot 客製登入失敗 ( AuthenticationFailureHandler ) 與訊息 ( ResourceBundleMessageSource ) 顯示

 

 

在  Spring Boot 客製化 登入 ( Login ) 與 認證 (Authenticate) 機制 與 Spring Boot 身份認證 (Authentication) 使用資料庫 (Database) 中客製了登入系統的程式。現在來思考一下安全性的問題,若有人一直嚐試要登入,在登入失敗時是否應該作額外的控制,例如:認證錯誤 3 次後,此帳號要被鎖住,這樣才能確保帳號及系統的安全性。要達成這樣的功能,需要實作 AuthenticationFailureHandler 並且向 HttpSecurity 作註冊。

  • 實作 AuthenticationFailureHandler

在 19 行 取得用戶輸入的 username 值,並將錯誤的次數 + 1,若錯誤的次數大於最大值時,則在 25行 鎖住帳號。最後,在 28 行再將 username 傳到 Controller 作訊息傳遞到前端網頁HTML前的處理。

@Component
public class AppsAuthenticationFailureHandler implements AuthenticationFailureHandler {

 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
 @Autowired 
 SystemService sysService;
 
 /**
  * 記錄帳號認證錯誤次數, 大於 table:systemConfig - field:max_login_errors 則鎖住
  */
 @Override
 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException exception) throws IOException, ServletException {
  
  SystemConfig sc = sysService.getSystemConfigByParamName("max_login_errors");
  int maxLoginErrors = Integer.parseInt(sc.getParamValue());
  
  String username = request.getParameter("username");   // 取得登入帳號   
  User user = sysService.getUserByUsername(username);
  int loginErrorTimes = user.getLoginErrorTimes() +1;
  user.setLoginErrorTimes(loginErrorTimes);
  
  if (loginErrorTimes > maxLoginErrors) {
   user.setAccountNonLocked(false);         
  }
  sysService.UserSave(user);
  response.sendRedirect("/login/failure?username=" + username);
 }

}

 

  • Controller 作訊息傳遞到前端網頁HTML前的處理

在 Spring Boot 取得 request 傳遞來的值很方便,只要加入 @RequestParam(name = "username") String username 即可。至於訊息可以透過 ResourceBundleMessageSource 來取得多語系裡的訊息,這可以參考 Spring Boot 多語系設置(國際化)

@Autowired
LocaleResolver localeResolver;
@Autowired
ResourceBundleMessageSource messageSource;


@RequestMapping("/login/failure")
public String loginFailure(HttpServletRequest request, Model model,@RequestParam(name = "username") String username ) {
 Locale locale = localeResolver.resolveLocale(request);
 User user = sysService.getUserByUsername(username);
 // 取得 spring-security-core.jar 裡 org.springframework.security 的 message 作參考
 model.addAttribute("message", messageSource.getMessage("page.text.login.badCredentials", null, "Bad credentials",locale));
 if ( !user.getEnabled()) {
  model.addAttribute("message", messageSource.getMessage("page.text.login.disabled", null, "User is disabled",locale));
 }
 if ( !user.getAccountNonLocked()) {
  model.addAttribute("message", messageSource.getMessage("page.text.login.locked", null, "User account is locked",locale));
 }      
 
 return "login";
}

 

  • 前端 Login 登入網頁HTML

對原來的 Login 登入網頁,增加 message 的顯示。

<div class="card">
  <div class="card-body login-card-body">
    <p class="login-box-msg"><@spring.message "page.text.login.formTitle" /></p>
    <#if message?? && message?has_content>
    <p class="alert alert-danger">${message!""}</p>
    </#if>
    <form action="/login" method="post">        
      <div class="input-group mb-3">          
        <input type="text" class="form-control" name="username" placeholder="Username">
        <div class="input-group-append">
          <div class="input-group-text">
            <span class="fas fa-envelope"></span>
          </div>
        </div>
      </div>
      <div class="input-group mb-3">
        <input type="password" class="form-control" name="password" placeholder="Password">
        <div class="input-group-append">
          <div class="input-group-text">
            <span class="fas fa-lock"></span>
          </div>
        </div>
      </div>
      <div class="row">
        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
        <div class="col-8">
          <div class="icheck-primary">
            <input type="checkbox" id="remember">
            <label for="remember">
              <@spring.message "page.text.login.rememberMe" />
            </label>
          </div>
        </div>
        <!-- /.col -->
        <div class="col-4">
          <button type="submit" class="btn btn-primary btn-block"><@spring.message "page.text.login.signIn" /></button>
        </div>
        <!-- /.col -->
      </div>
    </form>
  </div><!-- /.login-card-body -->
</div><!-- /.login-card -->

 

  • 向 HttpSecurity 作註冊

上述的工作都準備好後,只要在 HttpSecurity 作註冊,如第 13 行。最後要記得將 /login/failure 作允許訪問,這樣轉址到 Controller 才不會被擋住,如第 8 行。

@Autowired
AppsAuthenticationFailureHandler appsAuthenticationFailureHandler;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
 httpSecurity
   .authorizeRequests()                 
    .antMatchers("/", "/home", "/user/forgotPassword","/login/failure").permitAll() //首頁不需認證 認證機制設置     
    .anyRequest().authenticated()           // 除了以上的 URL 外, 都需要認證才可以訪問
    .and()              
   .formLogin()
    .loginPage("/login")                    // 認證頁面指向頁頁                 
    .failureHandler(appsAuthenticationFailureHandler)                   
    .defaultSuccessUrl("/auth/home")                    
    .permitAll()
    .successHandler(appsAuthenticationSuccessHandler)
    .and()
   .logout()                    
    .logoutSuccessUrl("/logout/process")
    .permitAll()
    .logoutSuccessHandler(appsLogoutSuccessHandler)
    .and()
   .sessionManagement()
    .maximumSessions(1).sessionRegistry(sessionRegistry());

}

 

以上的功能是可以作到基本的防護囉。

Copyright © All rights reserved. | Newsphere by AF themes.