一. 運行環境
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"></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這部分流程的理解,理解的還不夠,有問題的地方還望大佬提出