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