Spring Security 全面介紹
1. 什么是 Spring Security?
Spring Security 是一個功能強大且高度可定制的認證和訪問控制框架,是保護基于 Spring 的應用程序的標準工具。它是一個專注于為 Java 應用程序提供認證和授權的框架,實際上它是 Spring 生態系統中負責安全方面的重要成員。
核心特性
- 全面的安全性:支持認證、授權和防護常見攻擊
- 與 Spring 生態系統深度集成
- 適應多種環境:Web 應用、RESTful API、微服務
- 高度可擴展性:幾乎所有組件都可以定制或替換
- 多種認證機制:表單認證、Basic 認證、OAuth2、LDAP、SAML 等
2. 基本概念
認證 (Authentication)
認證回答了"你是誰?"的問題,是確認用戶身份的過程。
核心工作流程:
- 收集認證憑據(如用戶名和密碼)
- 驗證憑據的有效性
- 對有效憑據發放安全令牌
授權 (Authorization)
授權回答了"你能做什么?"的問題,是確定用戶是否有權執行特定操作的過程。
核心工作流程:
- 確定資源要求的權限
- 檢查已認證用戶是否擁有所需權限
- 授予或拒絕訪問權限
主體 (Principal)
表示當前通過認證的用戶,通常包含用戶標識(如用戶名)和授予的權限。
權限 (Authorities/Roles)
表示授予用戶的特定權限,通常分為:
- 角色 (Roles):代表用戶組,如 ROLE_ADMIN
- 權限 (Authorities):代表具體權限,如 READ_DATA
3. 核心架構
Spring Security 架構基于過濾器鏈模式,請求通過一系列專門的過濾器,各自負責安全流程的不同方面。
關鍵組件
Security Filters
過濾器鏈,按特定順序執行的安全過濾器:
SecurityContextPersistenceFilter
:管理 SecurityContextUsernamePasswordAuthenticationFilter
:處理表單登錄BasicAuthenticationFilter
:處理 HTTP Basic 認證ExceptionTranslationFilter
:處理安全異常FilterSecurityInterceptor
:處理授權決策
Authentication Manager
認證管理器,主要實現是 ProviderManager
,它維護一個 AuthenticationProvider
列表,依次嘗試處理認證請求。
Authentication Providers
認證提供者,負責特定類型的認證,如:
DaoAuthenticationProvider
:基于用戶名密碼的認證JwtAuthenticationProvider
:JWT 令牌驗證LdapAuthenticationProvider
:LDAP 認證
UserDetailsService
負責加載用戶數據,是自定義用戶存儲的主要擴展點。
Security Context
安全上下文,存儲當前認證信息,通過 SecurityContextHolder
提供訪問。
4. 常見認證機制
表單登錄認證
最常見的認證方式,用戶通過 HTML 表單提交憑據:
http.formLogin().loginPage("/custom-login").defaultSuccessUrl("/dashboard").failureUrl("/login?error=true").permitAll();
HTTP Basic 認證
簡單的認證機制,通過 HTTP 頭發送憑據:
http.httpBasic().realmName("My API");
記住我功能
允許用戶在會話過期后保持登錄狀態:
http.rememberMe().key("uniqueAndSecret").tokenValiditySeconds(86400);
OAuth 2.0 / OpenID Connect
用于實現單點登錄和 API 授權:
http.oauth2Login().loginPage("/oauth_login").clientRegistrationRepository(clientRegistrationRepository).authorizedClientService(authorizedClientService);
5. 授權模型
基于 URL 的授權
控制對 Web URL 的訪問:
http.authorizeHttpRequests().requestMatchers("/public/**").permitAll().requestMatchers("/admin/**").hasRole("ADMIN").requestMatchers("/api/**").hasAuthority("API_ACCESS").anyRequest().authenticated();
方法級安全
在服務層保護方法調用:
@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public UserDetails loadUserByUsername(String username) {// 實現邏輯
}
基于表達式的訪問控制
使用 SpEL 表達式定義復雜授權規則:
http.authorizeHttpRequests().requestMatchers("/orders/**").access("hasRole('USER') and @webSecurity.checkUserId(authentication, #id)");
6. 會話管理
會話創建策略
控制會話的創建方式:
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
可選策略:
ALWAYS
:總是創建會話NEVER
:不主動創建會話IF_REQUIRED
:需要時創建(默認)STATELESS
:不創建或使用會話(適用于 REST API)
并發會話控制
限制用戶同時活躍的會話數:
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
會話固定保護
防止會話固定攻擊:
http.sessionManagement().sessionFixation().migrateSession();
7. 保護常見攻擊
CSRF 保護
默認啟用,保護表單提交免受跨站請求偽造:
http.csrf() // 默認啟用.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
跨域資源共享 (CORS)
啟用跨域支持:
http.cors().configurationSource(corsConfigurationSource());
安全頭部配置
添加安全相關的 HTTP 頭:
http.headers().frameOptions().deny().xssProtection().block().contentSecurityPolicy("script-src 'self'");
8. 實際應用示例
基礎 Web 應用配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(auth -> auth.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll().requestMatchers("/user/**").hasRole("USER").requestMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated()).formLogin(form -> form.loginPage("/login").permitAll().defaultSuccessUrl("/dashboard")).logout(logout -> logout.permitAll().logoutSuccessUrl("/login?logout"));return http.build();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
自定義用戶存儲
@Service
public class CustomUserDetailsService implements UserDetailsService {private final UserRepository userRepository;public CustomUserDetailsService(UserRepository userRepository) {this.userRepository = userRepository;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),user.isEnabled(),true, true, true,mapRolesToAuthorities(user.getRoles()));}private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Set<Role> roles) {return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName())).collect(Collectors.toSet());}
}
REST API 安全配置
@Configuration
@EnableWebSecurity
public class ApiSecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()).authorizeHttpRequests(auth -> auth.requestMatchers("/api/public/**").permitAll().anyRequest().authenticated()).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).httpBasic(Customizer.withDefaults());return http.build();}
}
9. 高級特性
方法安全
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {// 配置代碼
}@Service
public class UserService {@PreAuthorize("hasRole('ADMIN')")public List<User> findAllUsers() {// 實現邏輯}@PostAuthorize("returnObject.username == authentication.name")public User getUser(String id) {// 實現邏輯}
}
多租戶安全
@Bean
public TenantContextHolder tenantContextHolder() {return new TenantContextHolder();
}@Bean
public UserDetailsService userDetailsService(DataSource dataSource, TenantContextHolder holder) {return username -> {String tenant = holder.getTenant();// 基于租戶獲取用戶};
}
事件監聽
@Component
public class AuthenticationEventListener {private static final Logger logger = LoggerFactory.getLogger(AuthenticationEventListener.class);@EventListenerpublic void onSuccess(AuthenticationSuccessEvent event) {logger.info("User logged in: {}", event.getAuthentication().getName());}@EventListenerpublic void onFailure(AuthenticationFailureBadCredentialsEvent event) {logger.warn("Login failed for user: {}", event.getAuthentication().getName());}
}
10. 最佳實踐
1. 使用強密碼哈希
@Bean
public PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(12); // 更高的強度
}
2. 保持依賴更新
定期更新 Spring Security 版本,以獲取安全修復和新功能。
3. 使用多種防御機制
不要僅依賴一種安全機制,應組合使用認證、授權、CSRF 保護等。
4. 最小權限原則
默認拒絕訪問,只明確允許必要的權限:
.anyRequest().denyAll() // 而不是 .authenticated()
5. 安全日志記錄
記錄所有重要的安全事件,但避免記錄敏感信息:
@EventListener
public void handleBadCredentials(AuthenticationFailureBadCredentialsEvent event) {logger.warn("Failed login attempt from IP: {}", request.getRemoteAddr());// 不要記錄密碼!
}
6. 適當的錯誤處理
不要泄露敏感信息:
.failureHandler((request, response, exception) -> {response.sendRedirect("/login?error=true"); // 通用錯誤,不指明原因
})
11. 生態系統集成
Spring Boot 集成
Spring Boot 自動配置大部分 Spring Security 功能:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
OAuth2 客戶端集成
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
資源服務器
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
12. 性能和可擴展性考慮
- 使用適當的會話策略(無狀態 vs. 有狀態)
- 緩存用戶詳情和權限檢查
- 在負載均衡環境中注意會話復制/共享
- 考慮分布式會話存儲(Redis, Hazelcast)
Spring Security 攔截器鏈工作步驟
作為一名資深軟件工程師,我將詳細分析 Spring Security 的攔截器鏈(Filter Chain)工作流程。Spring Security 使用一系列過濾器來實現其安全功能,這些過濾器按特定順序組織成攔截器鏈。
攔截器鏈概述
Spring Security 的攔截器鏈是由 FilterChainProxy
管理的一系列 SecurityFilterChain
對象,每個 SecurityFilterChain
包含多個安全過濾器。
標準過濾器執行順序
以下是典型的 Spring Security 過濾器鏈執行順序(從先到后):
-
WebAsyncManagerIntegrationFilter
- 將 SecurityContext 集成到 Spring 異步處理中
- 確保異步請求中可以訪問 SecurityContext
-
SecurityContextPersistenceFilter
- 在請求開始時從 SecurityContextRepository(通常是 HTTP Session)中恢復 SecurityContext
- 在請求結束時將 SecurityContext 保存回 SecurityContextRepository
- 使安全上下文在整個請求中可用
-
HeaderWriterFilter
- 向響應添加安全相關 HTTP 頭
- 如 X-XSS-Protection, X-Frame-Options, X-Content-Type-Options 等
-
CsrfFilter
- 提供 CSRF(跨站請求偽造)保護
- 驗證 POST/PUT/DELETE 等請求中的 CSRF token
-
LogoutFilter
- 處理 /logout 路徑的請求
- 執行用戶注銷邏輯,清除認證信息和會話
-
UsernamePasswordAuthenticationFilter
- 處理表單登錄嘗試(通常是 /login POST 請求)
- 提取用戶名和密碼并創建認證令牌
- 委托給 AuthenticationManager 進行實際驗證
-
DefaultLoginPageGeneratingFilter
- 如果沒有自定義登錄頁,生成默認登錄頁面
- 處理 /login GET 請求
-
DefaultLogoutPageGeneratingFilter
- 生成默認注銷頁面
- 處理 /logout GET 請求
-
BasicAuthenticationFilter
- 處理 HTTP Basic 認證頭
- 提取憑據并嘗試認證
-
RequestCacheAwareFilter
- 處理請求緩存
- 如果用戶在登錄前訪問受保護資源,登錄后可以重定向到原始 URL
-
SecurityContextHolderAwareRequestFilter
- 包裝 HttpServletRequest,添加安全相關方法
- 實現 Servlet API 安全方法
-
AnonymousAuthenticationFilter
- 如果當前沒有認證信息,創建匿名用戶認證
- 確保 SecurityContext 總是有 Authentication 對象
-
SessionManagementFilter
- 檢測會話相關問題
- 處理會話固定保護、并發會話控制等
-
ExceptionTranslationFilter
- 捕獲安全異常并轉換為適當的 HTTP 響應
- 處理 AccessDeniedException 和 AuthenticationException
-
FilterSecurityInterceptor
- 最后一道防線,保護 HTTP 資源
- 使用 AccessDecisionManager 確定是否允許當前請求訪問資源
- 在這里應用具體的訪問控制決策
工作流程詳解
-
請求到達:
- 請求首先進入
FilterChainProxy
FilterChainProxy
找到匹配當前請求的第一個SecurityFilterChain
- 請求首先進入
-
上下文準備:
SecurityContextPersistenceFilter
檢索或創建SecurityContext
- 將
SecurityContext
存儲在SecurityContextHolder
中
-
認證流程:
- 如果請求包含認證信息(如登錄請求),相應的認證過濾器處理認證
- 認證成功后,
Authentication
對象被放入SecurityContext
-
授權檢查:
FilterSecurityInterceptor
檢查用戶是否有權限訪問請求的資源- 使用
SecurityMetadataSource
獲取資源所需權限 - 使用
AccessDecisionManager
決定是否授予訪問權限
-
異常處理:
- 如果發生安全異常,
ExceptionTranslationFilter
進行處理 - 認證異常觸發認證流程(通常重定向到登錄頁)
- 授權異常產生 403 禁止訪問響應
- 如果發生安全異常,
-
請求完成:
- 所有過濾器處理完畢后,請求傳遞給實際的應用程序處理器
- 請求處理完成后,
SecurityContextPersistenceFilter
保存SecurityContext
SecurityContextHolder
被清理
自定義配置示例
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/public/**").permitAll().antMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();}
}
這個配置會設置適當的過濾器并配置它們的行為。
調試技巧
要查看實際應用的過濾器鏈,可以設置日志級別:
logging.level.org.springframework.security.web.FilterChainProxy=DEBUG
這將輸出每個請求應用的確切過濾器鏈。
Spring Security 的攔截器鏈設計體現了職責分離原則,每個過濾器專注于特定安全功能,共同構成了一個強大而靈活的安全框架。
Spring Security 核心類詳解
作為資深軟件工程師,我將深入介紹 Spring Security 的核心類結構。這些核心類共同構成了 Spring Security 的基礎架構,理解它們有助于掌握整個框架的工作原理。
認證核心類
1. Authentication
(接口)
public interface Authentication extends Principal, Serializable {Collection<? extends GrantedAuthority> getAuthorities();Object getCredentials();Object getDetails();Object getPrincipal();boolean isAuthenticated();void setAuthenticated(boolean isAuthenticated);
}
- 職責: 代表認證請求或已認證的主體
- 關鍵方法:
getPrincipal()
: 獲取主體身份(通常是 UserDetails)getCredentials()
: 獲取憑證(如密碼)getAuthorities()
: 獲取授予的權限isAuthenticated()
: 判斷是否已認證
- 常見實現:
UsernamePasswordAuthenticationToken
,JwtAuthenticationToken
2. AuthenticationManager
(接口)
public interface AuthenticationManager {Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
- 職責: 處理認證請求,是認證架構的核心接口
- 工作方式: 接收一個 Authentication 對象,驗證后返回完全填充的 Authentication
- 主要實現:
ProviderManager
3. ProviderManager
public class ProviderManager implements AuthenticationManager {private List<AuthenticationProvider> providers;private AuthenticationManager parent;public Authentication authenticate(Authentication authentication) throws AuthenticationException {// 遍歷所有 provider 嘗試認證}
}
- 職責: AuthenticationManager 的主要實現
- 工作流程: 維護 AuthenticationProvider 列表,依次嘗試認證
4. AuthenticationProvider
(接口)
public interface AuthenticationProvider {Authentication authenticate(Authentication authentication) throws AuthenticationException;boolean supports(Class<?> authentication);
}
- 職責: 執行特定類型的認證
- 關鍵實現:
DaoAuthenticationProvider
: 基于用戶名密碼的認證JwtAuthenticationProvider
: JWT令牌認證
5. UserDetailsService
(接口)
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
- 職責: 從數據源加載用戶信息
- 使用場景: 被 AuthenticationProvider 調用以獲取用戶數據
6. UserDetails
(接口)
public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();String getPassword();String getUsername();boolean isAccountNonExpired();boolean isAccountNonLocked();boolean isCredentialsNonExpired();boolean isEnabled();
}
- 職責: 提供核心用戶信息
- 特點: 框架對用戶概念的抽象,與應用用戶模型解耦
- 常用實現:
User
和自定義實現
安全上下文管理
7. SecurityContext
(接口)
public interface SecurityContext extends Serializable {Authentication getAuthentication();void setAuthentication(Authentication authentication);
}
- 職責: 存儲當前線程的安全信息
- 主要實現:
SecurityContextImpl
8. SecurityContextHolder
public class SecurityContextHolder {private static SecurityContextHolderStrategy strategy;public static SecurityContext getContext() {return strategy.getContext();}public static void setContext(SecurityContext context) {strategy.setContext(context);}public static void clearContext() {strategy.clearContext();}
}
- 職責: 提供對當前 SecurityContext 的訪問
- 存儲策略: 支持 ThreadLocal, InheritableThreadLocal 和全局模式
9. SecurityContextRepository
(接口)
public interface SecurityContextRepository {SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);boolean containsContext(HttpServletRequest request);
}
- 職責: 在請求之間持久化 SecurityContext
- 主要實現:
HttpSessionSecurityContextRepository
授權核心類
10. AccessDecisionManager
(接口)
public interface AccessDecisionManager {void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)throws AccessDeniedException, InsufficientAuthenticationException;boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);
}
- 職責: 做出訪問控制決策
- 主要實現:
AffirmativeBased
: 只要有一個投票者同意即通過ConsensusBased
: 基于多數原則UnanimousBased
: 要求全體一致同意
11. AccessDecisionVoter
(接口)
public interface AccessDecisionVoter<S> {int ACCESS_GRANTED = 1;int ACCESS_ABSTAIN = 0;int ACCESS_DENIED = -1;boolean supports(ConfigAttribute attribute);boolean supports(Class<?> clazz);int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
}
- 職責: 對訪問請求進行投票
- 主要實現:
RoleVoter
,WebExpressionVoter
12. FilterSecurityInterceptor
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {private FilterInvocationSecurityMetadataSource securityMetadataSource;public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {// 攔截請求并應用安全檢查}
}
- 職責: HTTP 資源的安全攔截器
- 工作流程: 從 SecurityMetadataSource 獲取配置屬性,調用 AccessDecisionManager 做決策
過濾器鏈管理
13. FilterChainProxy
public class FilterChainProxy extends GenericFilterBean {private List<SecurityFilterChain> filterChains;public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {// 找到匹配的過濾器鏈并執行}
}
- 職責: Spring Security 過濾器的主入口點
- 特點: 管理多個 SecurityFilterChain 實例
14. SecurityFilterChain
(接口)
public interface SecurityFilterChain {boolean matches(HttpServletRequest request);List<Filter> getFilters();
}
- 職責: 持有與特定請求匹配的過濾器集合
- 主要實現:
DefaultSecurityFilterChain
關鍵過濾器
15. UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {// 處理表單登錄public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {// 提取用戶名密碼并創建認證令牌}
}
- 職責: 處理表單登錄認證
16. ExceptionTranslationFilter
public class ExceptionTranslationFilter extends GenericFilterBean {private AccessDeniedHandler accessDeniedHandler;private AuthenticationEntryPoint authenticationEntryPoint;public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {// 捕獲安全異常并轉換為HTTP響應}
}
- 職責: 轉換 Spring Security 異常為 HTTP 響應
- 處理邏輯:
- AuthenticationException → AuthenticationEntryPoint
- AccessDeniedException → AccessDeniedHandler
17. SecurityContextPersistenceFilter
public class SecurityContextPersistenceFilter extends GenericFilterBean {private SecurityContextRepository repo;public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {// 在請求前加載上下文,請求后保存上下文}
}
- 職責: 管理 SecurityContext 的生命周期
配置核心類
18. WebSecurityConfigurerAdapter
(在Spring Security 5.7+已廢棄)
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {protected void configure(HttpSecurity http) throws Exception {// 配置HTTP安全}protected void configure(AuthenticationManagerBuilder auth) throws Exception {// 配置認證}
}
- 職責: 提供安全配置的便捷基類
- 注: 在新版中使用組件化配置代替
19. HttpSecurity
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> {// 提供流式API配置HTTP安全public HttpSecurity authorizeRequests() {...}public HttpSecurity formLogin() {...}// 更多配置方法...
}
- 職責: 配置 HTTP 請求級別的安全特性
- 特點: 提供流式 API 進行安全配置
20. SecurityBuilder
& SecurityConfigurer
public interface SecurityBuilder<O> {O build() throws Exception;
}public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {void init(B builder) throws Exception;void configure(B builder) throws Exception;
}
- 職責: 構建器模式的核心接口,支持模塊化安全配置
實際應用示例
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.disable()).authorizeHttpRequests(auth -> auth.requestMatchers("/api/public/**").permitAll().requestMatchers("/api/admin/**").hasRole("ADMIN").anyRequest().authenticated()).formLogin(form -> form.loginPage("/login").permitAll()).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));return http.build();}@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build();return new InMemoryUserDetailsManager(user);}
}
理解這些核心類及其關系,能讓你更透徹掌握 Spring Security 的工作方式,并更有效地進行安全配置和定制化開發。無論是標準認證流程還是實現自定義安全機制,這些類都是框架的基礎構建塊。
通用鑒權模塊實戰
我們新建一個單獨的模塊security-contract,封裝Security的相關組件,這樣就能在我們自己的項目中引入。
依賴管理
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.4</version><relativePath/></parent><groupId>com.xujie</groupId><artifactId>security-contract</artifactId><version>1.0.0</version><properties><maven.compiler.source>23</maven.compiler.source><maven.compiler.target>23</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- 新版本的 JJWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson --><version>0.11.5</version><scope>runtime</scope></dependency><!-- pom.xml --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>4.4.0</version></dependency></dependencies>
</project>
相關類代碼
IgnoreUrlsConfig 白名單配置類
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "secure")
public class IgnoreUrlsConfig {private List<String> ignored = new ArrayList<>();}
UserSecurityConfiguration 用戶鑒權核心配置類
public class UserSecurityConfiguration {private final StringRedisTemplate redisTemplate;private final MyAuthenticationTokenFilter tokenFilter;private List<String> ingoreList;private final SecurityContextFilter securityContextFilter;public UserSecurityConfiguration(StringRedisTemplate stringRedisTemplate, List<String> ingoreList, SecurityContextFilter securityContextFilter) {this.redisTemplate = stringRedisTemplate;this.tokenFilter = new MyAuthenticationTokenFilter(stringRedisTemplate);this.ingoreList = ingoreList;this.securityContextFilter = securityContextFilter;}@Beanpublic SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {http.cors(cors -> cors.configurationSource(corsConfigurationSource())) // 開啟跨域.csrf(AbstractHttpConfigurer::disable) // 對API禁用CSRF.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).authorizeHttpRequests(authorize -> authorize.requestMatchers(ingoreList.toArray(String[]::new)).permitAll().requestMatchers("/api/admin/**").hasAuthority("ROLE_ADMIN").anyRequest().authenticated()).exceptionHandling(exceptions -> exceptions.authenticationEntryPoint((request, response, ex) -> {response.setCharacterEncoding("UTF-8");response.setStatus(401);response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);ErrorInfo errorInfo = new ErrorInfo(HttpStatus.UNAUTHORIZED.value(), "未登錄或登錄已過期,請重新登錄!");ObjectMapper mapper = new ObjectMapper();mapper.writeValue(response.getWriter(), errorInfo);response.getWriter().flush();}).accessDeniedHandler((request, response, ex) -> {response.setContentType("application/json");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getWriter().write("{\"error\":\"Forbidden\",\"message\":\"" + ex.getMessage() + "\"}");})).addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class).addFilterBefore(securityContextFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}@Beanpublic CorsConfigurationSource corsConfigurationSource() {CorsConfiguration configuration = new CorsConfiguration();configuration.setAllowedOrigins(List.of("http://localhost:*")); // 允許的前端域configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); // 允許的HTTP方法configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With")); // 允許的請求頭configuration.setExposedHeaders(List.of("Authorization")); // 允許前端訪問的響應頭configuration.setAllowCredentials(true); // 允許發送憑證configuration.setMaxAge(3600L); // 預檢請求緩存時間(秒)UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", configuration); // 對所有路徑應用此配置return source;}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}public record ErrorInfo(int status, String message) {}
}
AuthenticationConstant 常量類
public class AuthenticationConstant {public static final String HEAD_AUTH = "Authorization";public static final String HEAD_USER_ID = "userId";public static final String REDIS_KEY = "Auth:%s";}
MyAuthenticationTokenFilter Token驗證攔截器
@Slf4j
public class MyAuthenticationTokenFilter extends OncePerRequestFilter {private final StringRedisTemplate redisTemplate;public MyAuthenticationTokenFilter(StringRedisTemplate stringRedisTemplate) {this.redisTemplate = stringRedisTemplate;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader(AuthenticationConstant.HEAD_AUTH);// 判斷Token是否為空if (token == null) {SecurityContextHolder.getContext().setAuthentication(null);filterChain.doFilter(request, response);}Long userId = JWTUtil.getUserIdFromTokenUnsafe(token);// TODO 暫時不開啟Redis 驗證
// String key = String.format(AuthenticationConstant.REDIS_KEY, userId);
// if (!redisTemplate.hasKey(key)) {
// SecurityContextHolder.getContext().setAuthentication(null);
// filterChain.doFilter(request, response);
// }String userSign = "user123password";if (userSign != null && !userSign.isEmpty()) {try {JWTUtil.verifyToken(token, userSign);request.setAttribute(AuthenticationConstant.HEAD_USER_ID, userId);} catch (Exception e) {SecurityContextHolder.getContext().setAuthentication(null);} finally {filterChain.doFilter(request, response);}}}
}
SecurityContextFilter 上下文填充攔截器
public abstract class SecurityContextFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {try {Long userId = (Long) request.getAttribute(AuthenticationConstant.HEAD_USER_ID);if (userId == null) {SecurityContextHolder.getContext().setAuthentication(null);filterChain.doFilter(request, response);return;}Collection<GrantedAuthority> userAuth = getAuths(getUserAuth(userId), getUserRoles(userId));UsernamePasswordAuthenticationToken securityContextFilter = new UsernamePasswordAuthenticationToken(userId, null, userAuth);SecurityContextHolder.getContext().setAuthentication(securityContextFilter);} finally {filterChain.doFilter(request, response);}}protected abstract List<String> getUserRoles(Long userId);protected abstract List<String> getUserAuth(Long userId);private Collection<GrantedAuthority> getAuths(List<String> auths, List<String> userRoles) {Collection<GrantedAuthority> authorities = new ArrayList<>();if (auths == null) {return authorities;}for (String auth : auths) {authorities.add(new SimpleGrantedAuthority(auth));}for (String userRole : userRoles) {authorities.add(new SimpleGrantedAuthority("ROLE_" + userRole));}return authorities;}
}
JWTUtil 工具類
public class JWTUtil {/*** 從用戶密碼生成安全的JWT密鑰** @param userPassword 用戶密碼* @return 安全的JWT密鑰*/public static SecretKey generateSecureKeyFromPassword(String userPassword) throws NoSuchAlgorithmException {// 1. 組合用戶密碼String combined = userPassword;// 2. 使用SHA-256對組合字符串進行哈希處理MessageDigest digest = MessageDigest.getInstance("SHA-256");byte[] hashedBytes = digest.digest(combined.getBytes(StandardCharsets.UTF_8));// 3. 確保密鑰長度足夠(SHA-256已經產生32字節/256位的輸出)return Keys.hmacShaKeyFor(hashedBytes);}/*** 創建JWT令牌*/public static String createToken(String username, String userPassword) throws NoSuchAlgorithmException {SecretKey key = generateSecureKeyFromPassword(userPassword);return Jwts.builder().setSubject(username).setId(UUID.randomUUID().toString()).signWith(key).compact();}/*** 創建JWT令牌*/public static String createToken(String username, String userPassword, Map<String, String> claims) throws NoSuchAlgorithmException {SecretKey key = generateSecureKeyFromPassword(userPassword);return Jwts.builder().setClaims(claims).setSubject(username).setId(UUID.randomUUID().toString()).signWith(key).compact();}/*** 驗證并解析令牌*/public static void verifyToken(String token, String sign) throws NoSuchAlgorithmException {try {SecretKey key = generateSecureKeyFromPassword(sign);Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();} catch (Exception e) {throw new IllegalArgumentException("Token is not valid");}}/*** 不驗證簽名,直接解析JWT獲取對應的Claim* 警告:此方法不驗證令牌的真實性,僅用于讀取*/private static String parseTokenWithoutVerification(String token, String claimKey) {// 將token拆分成頭部、負載和簽名String[] parts = token.split("\\.");if (parts.length != 3) {throw new IllegalArgumentException("Invalid token format");}DecodedJWT decode = JWT.decode(token);Map<String, Claim> claims = decode.getClaims();Claim claim = claims.get(claimKey);if (claim == null) {throw new IllegalArgumentException("Invalid claim");}return claim.asString();}/*** 不驗證簽名,直接獲取用戶ID*/public static Long getUserIdFromTokenUnsafe(String token) {String userId = parseTokenWithoutVerification(token, "userId");return Long.valueOf(userId);}public static void main(String[] args) {try {// 模擬用戶密碼String userPassword = "user123password";String username = "john_doe";// 創建令牌Map<String, String> claimsMap = new HashMap<>();claimsMap.put("userId", "1001");String jwt = createToken(username, userPassword, claimsMap);System.out.println("生成的JWT: " + jwt);// 獲取Token的subjectLong userId = JWTUtil.getUserIdFromTokenUnsafe(jwt);System.out.println("UserId: " + userId);// 驗證令牌verifyToken(jwt, userPassword);System.out.println("驗證成功");// 驗證使用錯誤密碼try {verifyToken(jwt, "wrong_password");System.out.println("這行不應該執行");} catch (Exception e) {System.out.println("使用錯誤密碼驗證失敗(預期行為): " + e.getMessage());}} catch (Exception e) {e.printStackTrace();}}
}
引入自己模塊使用
WebSecurityConfig 配置類
@Configuration
public class WebSecurityConfig extends UserSecurityConfiguration {public WebSecurityConfig(StringRedisTemplate stringRedisTemplate, IgnoreUrlsConfig ignoreUrlsConfig, MySecurityContextFilter mySecurityContextFilter) {super(stringRedisTemplate, ignoreUrlsConfig.getIgnored(), mySecurityContextFilter);}
}
MySecurityContextFilter 實現用戶角色和權限接口
@Component
public class MySecurityContextFilter extends SecurityContextFilter {@Overrideprotected List<String> getUserRoles(Long userId) {return List.of("testRole");}@Overrideprotected List<String> getUserAuth(Long userId) {return List.of("testAuth");}
}
配置白名單
secure:ignored:- /test