SpringSecurity認證和授權流程詳解

LvLaoTou發表於2024-04-04

什麼是SpringSecurity

Spring Security是一個Java框架,用於保護應用程式的安全性。它提供了一套全面的安全解決方案,包括身份驗證、授權、防止攻擊等功能。Spring Security基於過濾器鏈的概念,可以輕鬆地整合到任何基於Spring的應用程式中。它支援多種身份驗證選項和授權策略,開發人員可以根據需要選擇適合的方式。此外,Spring Security還提供了一些附加功能,如整合第三方身份驗證提供商和單點登入,以及會話管理和密碼編碼等。

Spring Security是一個強大且易於使用的框架,可以幫助開發人員提高應用程式的安全性和可靠性。而我們最常用的兩個功能就是認證和鑑權,因此作為入門文章本文也只介紹這兩個功能的使用。

Spring Security可以用於Servlet應用和Reactive應用,本文主要介紹基於Servlet應用的場景

如需更詳細的使用方式請參考官方文件:https://spring.io/projects/spring-security

架構

Untitled

上圖是Spring Security官方提供的架構圖。我們先看圖的左邊部分,就是一個典型Servlet Filter (過濾器)處理流程,我們依次講解流程涉及的元件。

FilterChain

FilterChain :過濾器鏈,是Servlet容器在接收到客戶端傳送的請求時建立的,一個FilterChain可以包含多個Filter和一個ServletServlet容器根據請求URI的路徑來處理HttpServletRequest

在Spring MVC中,Servlet 就是 DispatcherServlet例項。一個 Servlet 最多隻能處理一個 HttpServletRequestHttpServletResponse。然而,可以使用多個 Filter 來完成如下工作。

  • 防止下游的 FilterServlet 被呼叫。在這種阻斷請求的情況下,Filter 通常會使用 HttpServletResponse 對客戶端寫入響應內容。
  • 修改下游的 FilterServlet 所使用的 HttpServletRequestHttpServletResponse

DelegatingFilterProxy

DelegatingFilterProxy :Spring Security 對 Servlet 的支援是基於Servlet Filter的,而DelegatingFilterProxy 就是Spring Security的Filter實現。

DelegatingFilterProxy允許在 Servlet 容器的生命週期和 Spring 的 ApplicationContext 之間建立橋樑。Servlet容器允許透過使用自己的標準來註冊 Filter 例項,但Servlet容器不知道 Spring 定義的 Bean。因此大多數情況下我們透過標準的Servlet容器機制來註冊 DelegatingFilterProxy,但將所有工作委託給實現 Filter 的Spring Bean。

Spring Security會自動向Servlet容器機制註冊 DelegatingFilterProxy ,無需我們手動去註冊

FilterChainProxy

FilterChainProxy :是 Spring Security 提供的一個特殊的 Filter,允許透過 SecurityFilterChain 委託給許多 Filter 例項。由於 FilterChainProxy 是一個Spring Bean,因此它被包含在 DelegatingFilterProxy 中。

SecurityFilterChain

SecurityFilterChain :是FilterChainProxy用來確定當前請求應該呼叫哪些Spring Security Filter 例項的過濾器鏈。

SecurityFilterChain 中的 Security Filter 一般都是Spring Bean,但這些Security Filter是用 FilterChainProxy 進行註冊,而不是透過DelegatingFilterProxy註冊。與直接向Servlet容器或 DelegatingFilterProxy 註冊相比,FilterChainProxy 有很多優勢。

  • 首先,由於 FilterChainProxy 是 Spring Security 使用的核心,它可以處理一些必須要做的事情。 例如:
    • 清除 SecurityContext 以避免記憶體洩漏。
    • 應用Spring Security的 HttpFirewall 來保護應用程式免受某些型別的攻擊。
  • 其次,它在確定何時應該呼叫 SecurityFilterChain 方面提供了更大的靈活性。在Servlet容器中,Filter 例項僅基於URL被呼叫。 然而,FilterChainProxy 可以透過使用 RequestMatcher 介面,根據 HttpServletRequest 中的任何內容確定呼叫。

圖的右邊部分是存在多個SecurityFilterChainFilterChainProxy 的匹配策略則是匹配第一個滿足的 SecurityFilterChain

比如,請求的URL是 /api/messages/,它首先與 /api/**SecurityFilterChain 0 模式匹配,所以只有 SecurityFilterChain0 被呼叫;雖然它也與 SecurityFilterChain n 匹配。

如果請求的URL是 /messages/,它與 /api/**SecurityFilterChain 0 模式不匹配,所以 FilterChainProxy 繼續嘗試每個 SecurityFilterChain。如果沒有其他 SecurityFilterChain 例項相匹配,則呼叫 SecurityFilterChain n

SecurityFilter

SecurityFilter:是指透過SecurityFilterChain 插入 FilterChainProxy 中的 Filter。 這些 Filter 可以用於許多不同的目的,如 認證、 授權、 漏洞保護等。Filter 是按照特定的順序執行的,以保證它們在正確的時間被呼叫。

例如,執行認證的 Filter 應該在執行授權的 Filter 之前被呼叫。如果想要知道 Spring Security 的 Filter 的順序,可以檢視 FilterOrderRegistration 原始碼。

如果想檢視你應用中註冊了哪些SecurityFilter 的話可以將org.springframework.security的日誌級別調到info,這樣在你應用啟動的時候就會在控制檯列印出當前應用註冊的所有SecurityFilter 。效果如下:

2023-06-14T08:55:22.321-03:00  INFO 76975 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [
org.springframework.security.web.session.DisableEncodeUrlFilter@404db674,
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@50f097b5,
org.springframework.security.web.context.SecurityContextHolderFilter@6fc6deb7,
org.springframework.security.web.header.HeaderWriterFilter@6f76c2cc,
org.springframework.security.web.csrf.CsrfFilter@c29fe36,
org.springframework.security.web.authentication.logout.LogoutFilter@ef60710,
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7c2dfa2,
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@4397a639,
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@7add838c,
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@5cc9d3d0,
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@7da39774,
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@32b0876c,
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@3662bdff,
org.springframework.security.web.access.ExceptionTranslationFilter@77681ce4,
org.springframework.security.web.access.intercept.AuthorizationFilter@169268a7]

至此,Spring Security官方架構圖中涉及的元件就基本介紹完了,大家先對整體架構和執行流程有一個瞭解,只有先了解了整體架構,才方便接下來我們去理解Spring Security是如何去實現認證和授權的。

常用Spring Security開啟的SecurityFilter

  • CsrfFilter:防止Csrf攻擊的SecurityFilter
  • AuthorizationFilter:授權SecurityFilter
  • ExceptionTranslationFilter:處理認證和授權異常的SecurityFilter

異常處理

Spring Security中有一個ExceptionTranslationFilterExceptionTranslationFilter 作為 Security Filter 之一被插入到 FilterChainProxy 中。

ExceptionTranslationFilter可以處理AuthenticationException或AccessDeniedException,其邏輯大概是這樣:

try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}

這段程式碼的邏輯大致就是,攔截AccessDeniedException 或 AuthenticationException,如果不是這兩個異常則不處理。

ExceptionTranslationFilter流程如下:

Untitled

因此如果我們想自己處理AuthenticationException或者AccessDeniedException,分別實現AuthenticationEntryPoint或者AccessDeniedHandler 即可

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error("AccessDeniedException 請求URI:{}", request.getRequestURI(), accessDeniedException);
response.setCharacterEncoding("UTF-8");
HashMap<String, String> result = new HashMap();
result.put("code", "401");
result.put("message", "許可權不足");
// 處理沒有許可權的錯誤錯誤
response.getWriter().write(JsonUtil.toString(result));
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.error("AccessDeniedException 請求URI:{}", request.getRequestURI(), authException);
response.setCharacterEncoding("UTF-8");
// 處理認證失敗的錯誤
HashMap<String, String> result = new HashMap();
result.put("code", "500");
result.put("message", "使用者名稱或密碼錯誤");
// 處理沒有許可權的錯誤錯誤
response.getWriter().write(JsonUtil.toString(result));
}
}
@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
AuthenticationEntryPoint authenticationEntryPoint,
AccessDeniedHandler accessDeniedHandler)
throws Exception
{
// 配置異常處理
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
return httpSecurity.build();
}
}

上面是Spring Security對於認證和鑑權異常的處理機制,但是如果我們自定義了一個Filter。如果這個Filter丟擲異常,Spring的全域性異常處理機制是無法處理的(原因自行搜尋)。所以我們還需要自己做一個Filter異常的處理流程。

首先,我們自定義一個Filter,要在FilterChain中的位置比較靠前,沒有其他邏輯就是攔截後面filter丟擲的異常,然後轉發到指定Controller去處理,然後再用全域性異常去處理Filter丟擲的異常。

@Component
public class ExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try{
filterChain.doFilter(request, response);
}catch (Exception e){
// 將異常資訊寫入請求
request.setAttribute("filterException", e);
// 重定向到處理異常的controller
request.getRequestDispatcher("/exception").forward(request, response);
}
}
}
@RestController
public class ExceptionController {

@RequestMapping("/exception")
public void handleException(HttpServletRequest request) throws Exception {
Object attribute = request.getAttribute("filterException");
if (ObjectUtil.isNotEmpty(attribute) && attribute instanceof Exception e){
throw e;
}
}
}
@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
ExceptionFilter exceptionFilter)
throws Exception
{
// 配置異常處理過濾器
// 這裡我們配置在ExceptionTranslationFilter之前 讓ExceptionTranslationFilter先處理AuthenticationException或者AccessDeniedException
// 剩下的其他Exception再交由我們自定義的ExceptionFilter處理
httpSecurity.addFilterBefore(exceptionFilter, ExceptionTranslationFilter.class);
return httpSecurity.build();
}
}

認證

上面我們已經把Spring Security整體流程講完了,接下來我們就看一下具體認證的流程是怎樣的。Spring Security有提供一套基於標準頁面的流程,但是不適用於基於前後端分離的開發模式。地址給大家貼這,有需要的可自行去看看:認證 :: Spring Security Reference (springdoc.cn)

接下來介紹基於前後端分離的流程:

Untitled
  1. 先配置AuthenticationManager(認證管理器),其中最常用的AuthenticationManager實現是ProviderManager
@Configuration
public class SecurityConfig {

/**
* 配置AuthenticationManager(認證管理器)
* @return
*/

@Bean
public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider){
// ProviderManager 是 AuthenticationManager 最常用的實現
return new ProviderManager(authenticationProvider);
}
}
  1. 配置AuthenticationProvider,這裡我們的認證方案是使用資料庫儲存的密碼和登入請求的密碼進行匹配驗證,因此我們選擇DaoAuthenticationProviderDaoAuthenticationProvider 是一個 AuthenticationProvider 的實現,它使用 UserDetailsServicePasswordEncoder 來驗證一個使用者名稱和密碼。
@Configuration
public class SecurityConfig {

/**
* 配置PasswordEncoder(密碼編碼器)
* @return
*/

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

/**
* 配置AuthenticationProvider
* @return
*/

@Bean
public AuthenticationProvider authenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder)
{
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
return daoAuthenticationProvider;
}
}
  1. 配置UserDetailsUserDetailsService
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Setter
@EqualsAndHashCode
@ToString
public class SecurityUserDetail implements UserDetails {

@Getter
private Long userId;

private String userName;

private String password;

private List<GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public String getPassword() {
return this.password;
}

@Override
public String getUsername() {
return this.userName;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

}

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthUserDetailsService implements UserDetailsService {

private final UserService userService ;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user= userService .getValidUser(username);
Assert.notEmpty(user, "使用者名稱或密碼錯誤");
SecurityUserDetail userDetail = new SecurityUserDetail();
userDetail.setUserId(user.getId());
userDetail.setUserName(user.getUserName());
userDetail.setPassword(user.getPassword());
return userDetail;
}
}

  1. LoginController提供登入介面,虛擬碼如下:
@Slf4j
@RestController
@RequiredArgsConstructor
public class LoginController{

private final AuthenticationManager authenticationManager;

public Response<LoginVO> login(LoginAO loginAo) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginAo.getKey(), loginAo.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
Assert.notEmpty(authenticate, "使用者名稱或密碼錯誤");
if (authenticate.getPrincipal() instanceof SecurityUserDetail userDetail){
User user = User.builder().id(userDetail.getUserId()).userName(userDetail.getUsername()).build();
String token = JwtUtil.generateToken(user);
// 設定上下文
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, null);
// 設定子執行緒支援從父執行緒獲取使用者上下文 注意使用ForkJoinPool無法生效
// SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
SecurityContextHolder.getContext().setAuthentication(authentication);
return Response.success(LoginVO.builder().token(token).build());
}else {
log.error("登入異常,從上下文獲取使用者資訊失敗,authenticate:{}", JsonUtil.toString(authenticate));
return Response.fail(null);
}
}
}

授權

本文這裡分享兩種主流的Spring Security授權方式,一種是基於註解的方式,一種是基於配置的方式。

基於註解的授權校驗

基於註解的方式校驗授權,是透過Spring aop實現的,其流程如下:

Untitled

首先,我們要開啟註解鑑權的功能

@Configuration
/**
* 開啟基於註解的方式控制許可權
*/

@EnableMethodSecurity
public class SecurityConfig {

}

然後在需要鑑權的方法上新增許可權註解

@RestController
public class UserController{

private final UserService userService;

@PreAuthorize("hasAuthority('sys:user:page')")
public Response<IPage<UserVO>> page(QueryUserPageParam param) {
return Response.success(userService.page(param));
}
}

Spring Security的常用許可權註解有:

  • @PreAuthorize:前置校驗許可權,在方法執行之前校驗許可權,支援Spel表示式
  • @PostAuthorize:後置許可權校驗,在方法執行結束以後進行校驗,可以對返回結果進行校驗,支援Spel表示式
  • @PreFilter:對方法引數進行過濾
  • @PostFilter:對方法結果進行過濾

具體每個許可權註解的使用方式可以自行去官網學習,這裡就不具體介紹了。

下面就以最常用的@PreAuthorize註解為例,介紹一下Spring Security基於註解鑑權的流程與原理:

  1. AuthorizationManagerBeforeMethodInterceptor (授權管理前置方法攔截器),會將許可權註解與AuthorizationManager (授權管理器)進行關聯及初始化

    Untitled
  2. AuthorizationManagerBeforeMethodInterceptor攔截器攔截到請求後,會根據許可權註解@PostAuthorize呼叫匹配的PreAuthorizeAuthorizationManager#check 方法,並從SecurityContextHolder上下文中獲取Authentication物件,將Supplier<Authentication>MethodInvocation作為引數傳遞給PreAuthorizeAuthorizationManager#check 方法。

Untitled
Untitled
Untitled
  1. AuthorizationManager授權管理器使用 MethodSecurityExpressionHandler 解析@PostAuthorize註解的 SpEL 表示式,並從包含 Supplier<Authentication>MethodInvocationMethodSecurityExpressionRoot 構建相應的 EvaluationContext

    Untitled
  2. 然後從 Supplier 讀取Authentication,並檢查其許可權集合中是否有 sys:user:page

    Untitled
  3. 如果校驗透過,將繼續呼叫業務方法。如果校驗不透過,會釋出一個 AuthorizationDeniedEvent,並丟擲一個 AccessDeniedExceptionExceptionTranslationFilter 會捕獲並處理。

基於配置的授權校驗

基於配置的授權校驗是透過AuthorizationFilter 實現的,首先我們需要配置授權校驗規則:

@Configuration
public class SecurityConfig {

/**
* 不需要校驗許可權的資源
*/

public static final String[] PERMIT_URL = new String[]{
// knife4j 資源
"/doc.html",
"/favicon.ico",
"/swagger-resources",
"/v3/**",
"/webjars/**",
// 監控介面
"/actuator/**",
// 登入介面
"/login",
// 註冊介面
"/register",
};

@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity httpSecurity,
TokenFilter tokenFilter,
AuthenticationEntryPoint authenticationEntryPoint,
ExceptionFilter exceptionFilter,
AccessDeniedHandler accessDeniedHandler)
throws Exception
{
// 配置token校驗過濾器
httpSecurity.addFilterBefore(tokenFilter, AuthorizationFilter.class);
// 配置異常處理過濾器
httpSecurity.addFilterBefore(exceptionFilter, ExceptionTranslationFilter.class);
// 配置異常處理
httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
httpSecurity.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
// 配置授權攔截規則
httpSecurity.authorizeHttpRequests()
// 配置放行的規則 這樣配置只能在AuthorizationManager#check方法時返回true 如果不經過AuthorizationManager則不生效(比如自定義Filter)
// 可以透過自定義WebSecurityCustomizer 來達到SecurityFilterChain中的Filter忽略處理 參考下方自定義WebSecurityCustomizer
.antMatchers(PERMIT_URL).permitAll()
.antMatchers("/sys/user/page").hasAuthority("sys:user:page")
.anyRequest().authenticated();
// 配置元件 基於JWT認證 因此禁用csrf
httpSecurity.csrf().disable();
// 基於JWT認證 因此禁用session
httpSecurity.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 禁用快取
httpSecurity.headers().cacheControl().disable();
// 允許跨域
httpSecurity.cors();
return httpSecurity.build();
}

/**
自定義WebSecurityCustomizer忽略指定路徑 但是Spring Security不建議這麼做 建議透過antMatchers(PERMIT_URL).permitAll()實現
如果想使用此方式 只需要將其註冊為Spring Bean即可
*/

// @Bean
public WebSecurityCustomizer webSecurityCustomizer(){
// 配置放行規則
return customizer -> customizer.ignoring().antMatchers(PERMIT_URL);
}
}

這裡咱們還是以前置校驗是否擁有某個許可權為例:

// 配置授權攔截規則 校驗
httpSecurity.authorizeHttpRequests()
.antMatchers("/sys/user/page").hasAuthority("sys:user:page")
.anyRequest().authenticated();
Untitled
Untitled

這裡初始化設定了AuthorityAuthorizationManager 作為AuthorizationManager 的實現,不同的許可權校驗功能可能對應的AuthorizationManager實現會不一樣,比如anyRequest().authenticated() 對應的AuthorizationManager 實現則是AuthenticatedAuthorizationManager

Untitled

AuthorizationFilter 執行的時候,會根據配置的授權規則找到對應的AuthorizationManager 實現,然後執行check 方法,並從SecurityContextHolder上下文中獲取Authentication物件,將Authenticationrequest作為引數傳遞給AuthorizationManager#check 方法。 這裡根據上面的規則hasAuthority() 對應的AuthorizationManager 實現就是AuthorityAuthorizationManager

Untitled
Untitled
Untitled

然後AuthorityAuthorizationManager 會根據SecurityContextHolder的Authentication 中獲取所有許可權和配置需要的許可權進行對比,如果使用者上下文SecurityContextHolder中儲存的許可權集合包含配置需要的許可權則返回true透過,反之則返回false。

注意事項

  1. 需要特別說明的是,Spring Security儲存角色和許可權都是使用的GrantedAuthority 物件,因此Spring Security規定角色需要加上統一字首方便與許可權區分開

這個統一字首預設為ROLE_ ,無論是基於配置還是基於註解的授權校驗都是同樣的規則。

Untitled

當然你也可以自定義這個字首,只需要將自定義的GrantedAuthorityDefaults 物件註冊進Spring容器即可。

@Configuration
public class SecurityConfig {

/**
* 配置Role字首
* @return
*/

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("ROLE_");
}
}
Untitled
  1. 細心的朋友可能已經發現了,無論是基於註解還是基於配置的授權校驗,都是從使用者上下文SecurityContextHolder中獲取當前使用者擁有的角色和許可權,然後再和需要的許可權去比較是否擁有許可權。所以我們需要在授權校驗之前需要往使用者上下文SecurityContextHolder中設定當前使用者所擁有的許可權。這裡就需要用到自定義Filter了。

自定義Filter

如果Spring Security中的SecurityFilter 不能滿足你的業務需求,需要自定義SecurityFilter 。比如我們需要自定義一個Filter用於解析請求的Token,然後從Token中獲取使用者資訊和許可權。自定義SecurityFilter有兩種方式:

  1. 自定義SecurityFilter 實現jakarta.servlet.Filter,在doFilter方法中實現自己的業務邏輯,參考案例:
public class TokenFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

String token= request.getHeader("Authorization");
boolean hasAccess = checkToken(token);
if (hasAccess) {
filterChain.doFilter(request, response);
return;
}
// 注意AuthenticationException或AccessDeniedException會被ExceptionTranslationFilter處理 如果是其他異常需要自己處理 Springboot全域性異常無法處理
throw new AuthenticationException("許可權不足");
}

}

然後將該SecurityFilter註冊進SecurityFilterChain

@Configuration
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(new TokenFilter(), AuthorizationFilter.class);
return http.build();
}
}

注意,如果想把jakarta.servlet.Filter的實現註冊為Spring Bean,這可能會導致 filter 被呼叫兩次,一次由容器呼叫,一次由 Spring Security 呼叫,而且順序不同。可以透過宣告 FilterRegistrationBean Bean 並將其 enabled 屬性設定為false來告訴 Spring Boot不要向容器註冊它。配置如下:

@Bean
public FilterRegistrationBean<TokenFilter> tenantFilterRegistration(TokenFilter filter) {
FilterRegistrationBean<TokenFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
  1. 自定義SecurityFilter繼承OncePerRequestFilter,這樣能保證每個請求只會呼叫一次的filter(推薦方式),然後將該SecurityFilter註冊進SecurityFilterChain
@Component
public class TokenFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
// 如果時不需要授權的URI直接放行
if (!AntPathUtil.match(requestURI, SecurityConfig.PERMIT_URL)){
String token = request.getHeader("Authorization");
if (StrUtil.isBlank(token)){
// 注意AuthenticationException或AccessDeniedException會被ExceptionTranslationFilter處理 如果是其他異常需要自己處理 Springboot全域性異常無法處理
// AuthenticationCredentialsNotFoundException 是 AuthenticationException的子類
throw new AuthenticationCredentialsNotFoundException("請先登入");
}
// 這裡是虛擬碼 邏輯就是透過token解析出使用者資訊 然後查詢出使用者所有角色和許可權
User user = parse(token);
List<GrantedAuthority> authorities = listUserAllPermissions(user.getId());
PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(user, null, authorities);
// 設定子執行緒支援從父執行緒獲取使用者上下文 注意使用ForkJoinPool無法生效 如果是執行緒池可能導致資料錯誤 謹慎使用
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
// 設定上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 放行
filterChain.doFilter(request, response);
}
}
public class AntPathUtil {

private AntPathUtil(){}

public static final AntPathMatcher MATCHER = createMatcher();

public static boolean match(String path, String... pattern){
if (ArrayUtil.isEmpty(pattern)){
return true;
}
if (StrUtil.isBlank(path)){
return false;
}
return Arrays.stream(pattern).filter(p -> MATCHER.match(p, path)).findAny().isPresent();
}

private static AntPathMatcher createMatcher(){
AntPathMatcher antPathMatcher = new AntPathMatcher();
antPathMatcher.setCaseSensitive(false);
return antPathMatcher;
}
}

然後將該SecurityFilter註冊進SecurityFilterChain

@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http, TokenFilter tokenFilter) throws Exception {
http.addFilterBefore(tokenFilter, AuthorizationFilter.class);
return http.build();
}
}

總結

最後總結Spring Security的認證和授權的流程如下:

Untitled

梳理一下上面的流程:

  1. 首先,使用者攜帶使用者名稱密碼透過LoginController進行登入(認證流程),如果登入成功則返回token(推薦使用JWT作為token)
  2. 後續其他請求,攜帶透過登入獲取得到的token,然後先被TokenFilter解析token獲取使用者資訊,並將使用者資訊寫入SecurityContextHolder
  3. 然後進行授權流程
  4. AuthorizationManager 透過從SecurityContextHolder獲取到當前使用者的authentication(許可權集合),然後與需要的許可權進行對比,從而校驗當前使用者是否有許可權使用當前業務功能

本文使用 markdown.com.cn 排版

相關文章