springboot 集成 spring security + jsp 登錄失敗返回異常消息到前台的實現方式以及源碼的分析


一. 運行環境
springboot+mybatis+mysql+security+jsp

 

二. 前台登錄頁面代碼

<form class="form form-horizontal" id="loginForm" action="login" method="post">
  <div class="row cl">
    <label class="form-label col-md-7 ">用戶登錄</label>
  </div>
  <div class="row cl">
    <label class="form-label col-md-2"><i class="Hui-iconfont">&#xe60d;</i></label>
    <div class="formControls col-md-9">
      <input id="username" name="username" type="text" placeholder="請輸入用戶名" class="input-text size-L" style="width: 350px;">
        <c:if test="${not empty param.error}">
          <span style="color: red">用戶名或密碼錯誤,請重新輸入</span>
        </c:if>
        <c:if test="${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message!=null}">
          <span style="color: red">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}</span>
        </c:if>
    </div>
  </div>
.....代碼省略.....

 

三. 后台代碼配置
1.spring security配置文件configuration,繼承WebSecurityConfigurerAdapter,重寫

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 創建PasswordEncoder密碼解析對象
* @return
*/
@Bean
public PasswordEncoder getPasswordEncoder(){
  return new BCryptPasswordEncoder();
}


@Override
protected void configure(HttpSecurity http) throws Exception {
//關閉csrf防護
http.csrf().disable();
//防止iframe
http.headers().frameOptions().disable();
//表單認證
http.formLogin()
  .loginPage("/index.jsp")
  .successForwardUrl("/menu/main")
  .loginProcessingUrl("/login")
  .failureUrl("/index?error=true");

//退出登錄
http.logout().logoutSuccessUrl("/index")
  .logoutUrl("/logout");
}
}

 

2. 實現UserDetailsService接口,實現loadUserByUsername方法,也可以繼承WebSecurityConfigurerAdapter類,因為WebSecurityConfigurerAdapter也實現了loadUserByUsername方法

public class UserServiceImpl implements UserService, UserDetailsService {
//注入userMapper對象
@Autowired
private UserMapper userMapper;


/**
* 自定義邏輯,用戶效驗
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  //通過用戶名從數據庫查詢用戶信息
  User user = userMapper.selectUserByUsername(username);
  if (user==null){
    //拋出異常信息
    throw new BadCredentialsException("用戶名不正確");
  }

  //校驗用戶信息
  UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities("ROLE_admin").build();
    return userDetails;
  }
}

 

四. 源碼分析

其核心就是一組過濾器鏈 SpringSecurityFilterChain ,項目啟動后將會自動配置。最核心的就是 BasicAuthenticationFilter 用來認證用戶的身份,一個在spring security中一種過濾器處理一種認證方式

 

 

 

 

UsernamePasswordAuthenticationFilter:基於用戶表單登錄的驗證

BasicAuthenticationFilter:基於HttpBasic的驗證

ExceptionTranslationFilter: 認證出現異常處理的過濾器

FilterSecurityInterceptor:總的攔截器

 

認證流程

 

 

 

 

1.用戶登錄提交用戶名,密碼,過濾器 AbstractAuthenticationProcessingFilter.doFilter() 調用 UsernamePasswordAuthenticationFilter 中的 attempAuthentication 方法

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {
.....此處代碼省略.....
try {   authResult =attemptAuthentication(request, response);

  .....此處代碼省略.....

catch (AuthenticationException failed) {
// Authentication failed 
unsuccessfulAuthentication(request, response, failed);
  return;
}
.....此處代碼省略.....
successfulAuthentication(request, response, chain, authResult);
}

 

2. 通過調用 UsernamePasswordAuthenticationFilter 中的 attempAuthentication 方法

public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
.....此處代碼省略.....
// Allow subclasses to set the "details" property setDetails(request, authRequest); 

  return this.getAuthenticationManager().authenticate(authRequest);
}


3.通過 authenticate方法( authenticate 方法為 AuthenticationManager (認證管理器)接口中的方法)委托認證, DaoAuthenticationProvider 類中 retreveUser 方法

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  
this.prepareTimingAttackProtection();   try {     UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);     if (loadedUser == null) {       throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");     }else {       return loadedUser;     }   } catch (UsernameNotFoundException var4) {       this.mitigateAgainstTimingAttack(authentication);       throw var4;   } catch (InternalAuthenticationServiceException var5) {       throw var5;   } catch (Exception var6) {       throw new InternalAuthenticationServiceException(var6.getMessage(), var6);   } }

 

4.,通過調用 loadUserByUsername(username) 方法來獲取返回 UserDetails ,我們可以通過實現實現 UserDetailsService 接口,實現 loadUserByUsername 方法,也可以繼承 WebSecurityConfigurerAdapter 類,因為 WebSecurityConfigurerAdapter 也實現了 loadUserByUsername 方法來自定義用戶認證

public class UserServiceImpl implements UserService, UserDetailsService {
//注入userMapper對象
@Autowired
private UserMapper userMapper;
/**
* 自定義邏輯,用戶效驗
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通過用戶名從數據庫查詢用戶信息
User user = userMapper.selectUserByUsername(username);
if (user==null){
  //拋出異常信息
  throw new BadCredentialsException("用戶名不正確");
}
//校驗用戶信息
UserDetails userDetails = org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities("ROLE_admin").build();
return userDetails;
}
}

 

5. BadCredentialsException 異常拋出后,會將異常消息保存為 message

public BadCredentialsException(String msg) {
  super(msg);
}

 

6.異常拋出后,異常會往上拋出,直到在過濾器 AbstractAuthenticationProcessingFilter.doFilter() 方法中處理該異常,調用 unsuccessfulAuthentication 方法處理異常

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {

  .........此處省略代碼..........
  }
  catch (InternalAuthenticationServiceException failed) {
    logger.error("An internal error occurred while trying to authenticate the user.",failed);
    unsuccessfulAuthentication(request, response, failed);
    return;
  }
  .........此處省略代碼..........

}

 

7.然后繼續處理異常,通過調用 onAuthenticationFailure 方法繼續處理

protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException {
  SecurityContextHolder.clearContext();

  .........此處省略代碼..........

  failureHandler.onAuthenticationFailure(request, response, failed);
}

 

8.SimpleUrlAuthenticationFailureHandler 類中的 onAuthenticationFailure 方法通過調用 saveException 方法保存異常信息

public void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {

  .........此處省略代碼..........

  else {
    saveException(request, exception);

    .........此處省略代碼..........
  }
}

 

9.SimpleUrlAuthenticationFailureHandler 類中 WebAttributes.AUTHENTICATION_EXCEPTION

protected final void saveException(HttpServletRequest request,AuthenticationException exception) {
  if (forwardToDestination) {
    request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
  }
  .........此處省略代碼..........
}

 

10.WebAttributes 類中 AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION"

public static final String AUTHENTICATION_EXCEPTION = "SPRING_SECURITY_LAST_EXCEPTION";

 

11. 所以前台頁面可以通過 ${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message} 獲取到自定義返回的異常消息,以下提供了兩種方法來在前台實現登錄失敗的消息顯示

注意:如果用param.error判斷則需要在failureUrl("/login?error=true")進行設置
<c:if test="${not empty param.error}"> <span style="color: red">用戶名或密碼錯誤,請重新輸入</span> </c:if> <c:if test="${not empty sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}"> <span style="color: red">${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message}</span> </c:if>

 

以上就是我對spring security這部分流程的理解,理解的還不夠,有問題的地方還望大佬提出


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM