Spring Security賬戶與密碼驗證實現(xiàn)過程
這里使用Spring Boot 2.7.4版本,對應(yīng)Spring Security 5.7.3版本
本文樣例代碼地址: spring-security-oauth2.0-sample
關(guān)于Username/Password認(rèn)證的基本流程和基本方法參見官網(wǎng) Username/Password Authentication
Introduction
Username/Password認(rèn)證主要就是Spring Security 在 HttpServletRequest中讀取用戶登錄提交的信息的認(rèn)證機(jī)制。
Spring Security提供了登錄頁面,是前后端不分離的形式,前后端分離時的配置需另加配置。本文基于前后端分離模式來敘述。
基本流程如下:
Username/Password認(rèn)證可分為兩部分:
- 從HttpServletRequest中獲取用戶登錄信息
- 從密碼存儲處獲取密碼并比較
關(guān)于獲取獲取用戶登錄信息,Spring Security支持三種方式(基本用的都是Form表單提交,即POST方式提交):
- Form
- Basic
- Digest
關(guān)于密碼的獲取和比對,關(guān)注下面幾個類和接口:
UsernamePasswordAuthenticationFilter
: 過濾器,父類AbstractAuthenticationProcessingFilter
中組合了AuthenticationManager
,AuthenticationManager
的默認(rèn)實現(xiàn)ProviderManager
中又組合了多個AuthenticationProvider
,該接口實現(xiàn)類,有一個DaoAuthenticationProvider
負(fù)責(zé)獲取用戶密碼以及權(quán)限信息,DaoAuthenticationProvider
又把責(zé)任推卸給了UserDetailService
。PasswordEncoder
: 密碼加密方式UserDetails
: 代表用戶,包括 用戶名、密碼、權(quán)限等信息UserDetailsService
: 最終實際調(diào)用獲取UserDetails
的接口,通常用戶實現(xiàn)。
整個流程的UML圖如下:
前后端分離模式下配置
先來看對SecurityFilterChain的配置:
@Configuration @EnableMethodSecurity() @RequiredArgsConstructor public class SecurityConfig { // 自定義成功處理,主要存儲登錄信息并返回jwt private final LoginSuccessHandler loginSuccessHandler; // 自定義失敗處理,返回json格式而非默認(rèn)的html private final LoginFailureHandler loginFailureHandler; private final CustomSessionAuthenticationStrategy customSessionAuthenticationStrategy; ... @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // 設(shè)置登錄成功后session處理, 認(rèn)證成功后 // SessionAuthenticationStrategy的最早執(zhí)行,詳見AbstractAuthenticationProcessingFilter // 執(zhí)行順序: // 1. SessionAuthenticationStrategy#onAuthentication // 2. SecurityContextHolder#setContext // 3. SecurityContextRepository#saveContext // 4. RememberMeServices#loginSuccess // 5. ApplicationEventPublisher#publishEvent // 6. AuthenticationSuccessHandler#onAuthenticationSuccess http.sessionManagement().sessionAuthenticationStrategy(customSessionAuthenticationStrategy); ... // 前后端不分離,可指定html返回。該項未測試 // http.formLogin().loginPage("login").loginProcessingUrl("/hello/login"); // 前后端分離下username/password登錄 http.formLogin() .usernameParameter("userId") .passwordParameter("password") // 前端登陸頁面對這個url提交username/password即可 // 必須為Post請求,且Body格式為x-www-form-urlencoded,如果要接受application/json格式,需另加配置 .loginProcessingUrl("/hello/login") .successHandler(loginSuccessHandler) .failureHandler(loginFailureHandler); // .securityContextRepository(...) // pass ... return http.build(); } ... }
使用Postman測試:
登錄成功登陸失敗
AbstractAuthenticationProcessingFilter
該類是UsernamePasswordAuthenticationFilter
和OAuth2LoginAuthenticationFilter
的父類,使用模板模式構(gòu)建。
UsernamePasswordAuthenticationFilter
只負(fù)責(zé)從HttpServletRequest
中獲取用戶提交的用戶名密碼,而真正去認(rèn)證、事件發(fā)布、SessionAuthenticationStrategy、AuthenticationSuccessHandler、AuthenticationFailureHandler、SecurityContextRepository、RememberMeServices這些內(nèi)容均組合在AbstractAuthenticationProcessingFilter
中。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { ... // 委托給子類ProviderManager執(zhí)行認(rèn)證,最終由DaoAuthenticationProvider認(rèn)證 // DaoAuthenticationProvider中會調(diào)用UserDetailsService#loadUserByUsername(username)接口方法 // 我們只需實現(xiàn)該UserDetailsService接口注入Bean容器即可 private AuthenticationManager authenticationManager; private SessionAuthenticationStrategy sessionStrategy; protected ApplicationEventPublisher eventPublisher; private RememberMeServices rememberMeServices; private AuthenticationSuccessHandler successHandler; private AuthenticationFailureHandler failureHandler; private SecurityContextRepository securityContextRepository; ... private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { ... try { // 模板模式,該方法子類實現(xiàn) Authentication authenticationResult = attemptAuthentication(request, response); // 1. this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 成功后續(xù)處理 successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { // 失敗后續(xù)處理 unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); } } // 模板模式,由UsernamePasswordAuthenticationFilter完成 public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException;} protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(authResult); // 2. SecurityContextHolder.setContext(context); // 3. this.securityContextRepository.saveContext(context, request, response); // 4. this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { // 5. this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } // 6. this.successHandler.onAuthenticationSuccess(request, response, authResult); } }
UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username.trim() : ""; String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); // 調(diào)用父類中字段去認(rèn)證,最終是 UserDetailService#loadUserByUsername(String username),該接口實現(xiàn)類由程序員根據(jù)業(yè)務(wù)定義。 return this.getAuthenticationManager().authenticate(authRequest); } // ************** 重要 ************** // 這里只能通過x-www-urlencoded方式獲取,如果前端傳過來application/json,是解析不到的 // 非要用application/json,建議重寫UsernamePasswordAuthenticationFilter方法,但由于body中內(nèi)容默認(rèn)只能讀一次,又要做很多其他配置,比較麻煩,建議這里x-www-urlencoded // ********************************* @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } }
到此這篇關(guān)于Spring Security賬戶與密碼驗證實現(xiàn)過程的文章就介紹到這了,更多相關(guān)Spring Security內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java-collection中的null,isEmpty用法
這篇文章主要介紹了java-collection中的null,isEmpty用法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02JDBC如何訪問MySQL數(shù)據(jù)庫,并增刪查改
這篇文章主要介紹了JDBC如何訪問MySQL數(shù)據(jù)庫,幫助大家更好的理解和學(xué)習(xí)java與MySQL,感興趣的朋友可以了解下2020-08-08Java案例之HashMap集合存儲學(xué)生對象并遍歷
這篇文章主要介紹了Java案例之HashMap集合存儲學(xué)生對象并遍歷,創(chuàng)建一個HashMap集合,鍵是學(xué)號(String),值是學(xué)生對象(Student),存儲三個鍵值對元素并遍歷,下文具體操作需要的朋友可以參考一下2022-04-04淺談springfox-swagger原理解析與使用過程中遇到的坑
本篇文章主要介紹了淺談springfox-swagger原理解析與使用過程中遇到的坑,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02