前言
Spring Security是一個功能強大且高度且可定制的身份驗證和訪問控制框架,包含標準的身份認證和授權。
本文主要介紹SpringBoot中如何配置使用 Spring Security 安全認證框架并簡述相關原理和步驟。
核心認證流程解析
- 請求過濾
- 用戶提交登錄表單
AbstractAuthenticationProcessingFilter
過濾請求,創建AbstractAuthenticationToken
以默認提供的 UsernamePasswordAuthenticationFilter
舉例:
public class UsernamePasswordAuthenticationFilter extendsAbstractAuthenticationProcessingFilter {public UsernamePasswordAuthenticationFilter() {// 設置過濾規則super(new AntPathRequestMatcher("/login", "POST"));}public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {// ......String username = obtainUsername(request);String password = obtainPassword(request);if (username == null) {username = "";}if (password == null) {password = "";}username = username.trim();// 根據請求參數設置 UsernamePasswordAuthenticationToken 實例UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}// ......
}
- 認證管理器
AuthenticationManager
(通常是ProviderManager
)協調認證過程,根據AbstractAuthenticationToken
類型 選擇對應的AuthenticationProvider
在 ProviderManager
中根據不同的 Token 類型匹配不同的 Provider
public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;AuthenticationException parentException = null;Authentication result = null;Authentication parentResult = null;boolean debug = logger.isDebugEnabled();// 遍歷所有已注冊的 providers ,匹配能處理當前 authentication 的 provider 進行認證處理for (AuthenticationProvider provider : getProviders()) {// 判斷provider是否可以處理當前的authenticationif (!provider.supports(toTest)) {continue;}try {// 執行認證邏輯,返回認證結果result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}} catch (AccountStatusException | InternalAuthenticationServiceException e) {prepareException(e, authentication);throw e;} catch (AuthenticationException e) {lastException = e;}}// .......throw lastException;
}
- 匹配到對應的 provider 后,調用
provider.authenticate(authentication);
執行實際的認證過程。
:::tips
認證過程以 AbstractUserDetailsAuthenticationProvider
(實現DaoAuthenticationProvider)為例,大致過程為:
- getUserDetailsService().loadUserByUsername(username); 加載用戶信息
- preAuthenticationChecks.check(user); 校驗用戶信息,是否鎖定、過期、可用等
- additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); 驗證用戶密碼
:::
- 認證后處理
- 認證成功:生成已認證的
Authentication
對象,存入SecurityContext
- 認證失敗:拋出
AuthenticationException
由AuthenticationEntryPoint
處理
組件 | 職責 | 典型實現類 |
---|---|---|
AbstractAuthenticationProcessingFilter | 攔截認證請求,封裝認證對象 | UsernamePasswordAuthenticationFilter |
AuthenticationManager | 認證流程協調者 | ProviderManager |
AuthenticationProvider | 執行具體認證邏輯 | DaoAuthenticationProvider |
UserDetailsService | 加載用戶數據 | 自定義實現類 |
其中
UsernamePasswordAuthenticationFilter
和UsernamePasswordAuthenticationToken
為框架自帶的 登錄請求過濾和Token實例。如需自定義登錄請求過濾和Token實例,可自行實現
AbstractAuthenticationProcessingFilter
和AbstractAuthenticationToken
接口。不同的
AbstractAuthenticationToken
通常有不同的AuthenticationProvider
與之對應,用于實現不同的認證邏輯。自定義的認證邏輯中,通常都是對
AbstractAuthenticationToken
和AuthenticationProvider
的不同實現。
JWT
JWT(JSON Web Token) 是一種輕量級的開放標準(RFC 7519),用于在網絡應用間安全地傳輸信息。它通常用于 身份認證(Authentication) 和 數據交換(Information Exchange),特別適合 前后端分離 和 無狀態(Stateless) 的應用場景。
JWT在登錄中的應用過程
- 用戶提交 用戶名 + 密碼 登錄
- 服務器驗證后,生成 JWT 并返回給客戶端
- 客戶端存儲 JWT(通常放
localStorage
或Cookie
) - 后續請求 在
Authorization
頭攜帶 JWT - 服務器 驗證 JWT 簽名,并解析數據
特點
- 防篡改:簽名(Signature)確保 Token 未被修改
- 可設置有效期(
exp
字段)避免長期有效 - 無狀態:服務器不需要存儲 Session,適合分布式系統
自定義認證邏輯
接下來根據 Spring Security 的認證過程和JWT的特點進行自定義登錄邏輯的編寫
核心類圖
登錄流程
具體實現
JwtLoginFilter
實現 AbstractAuthenticationProcessingFilter
接口
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {public JwtLoginFilter() {// 設置當前 Filter ,也就是需要過濾的登錄URLsuper(new AntPathRequestMatcher("/auth/login", "POST"));}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException, IOException, ServletException {// 獲取前端傳遞登錄模式String loginType = request.getParameter("loginType");Authentication authentication = null;// 判斷前端使用的登錄模式if (CommonConstant.LoginType.SMS.equals(loginType)) {// 手機短信String phone = request.getParameter("phone");String code = request.getParameter("code");authentication = new SmsAuthenticationToken(phone, code);}if (CommonConstant.LoginType.WX.equals(loginType)) {String code = request.getParameter("code");authentication = new WxAuthenticationToken(code);}if (authentication == null) {throw new UnsupportedLoginTypeException();}return getAuthenticationManager().authenticate(authentication);}
}
SmsAuthenticationToken
TIPS:如果需要多種認證模式,如:用戶密碼、短信認證、掃描登錄、三方認證等,可實現不同的
Token
實例,并實現與之對應的Provider
。
以短信認證Token SmsAuthenticationToken
舉例
public class SmsAuthenticationToken extends AbstractAuthenticationToken {private final String phone;private final String code;public SmsAuthenticationToken(String phone, String code) {super(new ArrayList<>());this.phone = phone;this.code = code;}@Overridepublic String getCredentials() {return code;}@Overridepublic String getPrincipal() {return phone;}
}
SmsAuthProvider
具體實現短信認證的邏輯,主要工作原理是將前端傳遞的手機號和短信驗證碼進行匹配校驗,如果合法,則認證成功,如果不合法,返回認證失敗。
@Component
public class SmsAuthProvider implements AuthenticationProvider {@Autowiredprivate AbstractLogin abstractLogin;@Autowiredprivate JwtUtil jwtUtil;/*** 驗證手機驗證碼登錄認證** @param authentication the authentication request object.* @return* @throws AuthenticationException*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken token = (SmsAuthenticationToken) authentication;String phone = token.getPrincipal();String code = token.getCredentials();try {UserDetails userDetails = abstractLogin.smsLogin(phone, code);token.setDetails(userDetails);} catch (AuthenticationException authenticationException) {throw authenticationException;} catch (Exception e) {throw new LoginFailException();}return token;}@Overridepublic boolean supports(Class<?> authentication) {return SmsAuthenticationToken.class.equals(authentication);}
}
SpringBoot配置過程
我們已在上述的過程中將核心的認證邏輯實現,接下來就是把對應的代碼配置到 Spring Security 工程之中。
JwtAuthConfig
JwtAuthConfig
實現 WebSecurityConfigurerAdapter
作為整體的配置入口
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class JwtAuthConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtLoginConfig loginConfig;@Overridepublic void configure(WebSecurity web) throws Exception {super.configure(web);}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().formLogin().disable()// 應用登錄相關配置信息.apply(loginConfig).and().authorizeRequests()// 放行登錄 URL .antMatchers("/auth/login").permitAll();}
}
該配置類中,只做了初始化的簡單配置,如設置放行登錄URL、禁用 csrf、禁用 默認的formLogin等。更多的登錄認證配置在 JwtLoginConfig
中進行。
JwtLoginConfig
JwtLoginConfig
實現了SecurityConfigurerAdapter
,SecurityConfigurerAdapter
是 Spring Security 的核心配置基類,用于自定義安全規則(如認證、授權、過濾器鏈等)。
配置信息如下:
@Configuration
public class JwtLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate LoginSuccessHandler successHandler;@Autowiredprivate LoginFailHandler failHandler;@Autowiredprivate JwtProviderManager jwtProviderManager;/*** 將登錄接口的過濾器配置到過濾器鏈中* 1. 配置登錄成功、失敗處理器* 2. 配置自定義的userDetailService(從數據庫中獲取用戶數據)* 3. 將自定義的過濾器配置到spring security的過濾器鏈中,配置在UsernamePasswordAuthenticationFilter之前* @param http*/@Overridepublic void configure(HttpSecurity http) {JwtLoginFilter filter = new JwtLoginFilter();// authenticationManager 中已經預設系統內的 provider 集合filter.setAuthenticationManager(jwtProviderManager);//認證成功處理器filter.setAuthenticationSuccessHandler(successHandler);//認證失敗處理器filter.setAuthenticationFailureHandler(failHandler);//將這個過濾器添加到UsernamePasswordAuthenticationFilter之后執行http.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);}
}
JwtProviderManager
從上文中的認證過程(時序圖)中,AuthenticationManager
是委托 ProviderManager
進行認證模式的匹配和執行對應的 provider。
:::tips
為了后續的多認證模式的支持和動態匹配,所以將 ProviderManager
交給 Spring 容器管理,并且通過構造方法將平臺內所有已經注冊到Spring容器中的 provider
進行注入,以達到自動裝配的目的。
注:暫只做簡單實現。
:::
@Component
public class JwtProviderManager extends ProviderManager {public JwtProviderManager(List<AuthenticationProvider> providers) {super(providers);}
}
LoginSuccessHandler
認證成功后,對認證結果生成JWT Token 返回前端
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate NacosHcUserConfigProperties configProperties;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication)throws IOException, ServletException {// 生成 token 返回前端
// Object principal = authentication.getPrincipal();UserDetails details = (UserDetails) authentication.getDetails();// accessToken 過期時間 30分鐘Long accessTokenExpireSeconds = configProperties.getAuth().getAccessTokenExpireSeconds();// refreshToken 過期時間 6小時Long refreshTokenExpireSeconds = configProperties.getAuth().getRefreshTokenExpireSeconds();String accessToken = jwtUtil.createToken(details.getUsername(), accessTokenExpireSeconds);String refreshToken = jwtUtil.createToken(accessToken, refreshTokenExpireSeconds);Map<String, String> tokenMap = new HashMap<>();tokenMap.put("accessToken", accessToken);tokenMap.put("refreshToken", refreshToken);response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);response.getWriter().write(JSON.toJSONString(ApiResult.success(tokenMap)));}
}
END
至此,相關 Spring Security 配置已完成!💯💯
🧑?💻🧑?💻🧑?💻
下一篇繼續探究 Spring Security 在登錄后的認證鑒權過程。