Java深入解析篇十七之Spring Security
一、Spring Security概述与架构
1.1 什么是Spring Security
Spring Security是Spring生态中最成熟的安全框架,提供认证(Authentication)和授权(Authorization)两大核心能力。它基于Servlet过滤器链实现,与Spring MVC深度集成,支持从传统Session到JWT/OAuth2的多种安全方案。
1.2 核心架构组件
┌─────────────────────────────────────────────────────────┐
│ Servlet Container │
│ ┌───────────────────────────────────────────────────┐ │
│ │ DelegatingFilterProxy │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ FilterChainProxy │ │ │
│ │ │ ┌───────────────────────────────────────┐ │ │ │
│ │ │ │ SecurityFilterChain #1 │ │ │ │
│ │ │ │ Filter1 → Filter2 → ... → FilterN │ │ │ │
│ │ │ └───────────────────────────────────────┘ │ │ │
│ │ │ ┌───────────────────────────────────────┐ │ │ │
│ │ │ │ SecurityFilterChain #2 │ │ │ │
│ │ │ │ Filter1 → Filter2 → ... → FilterN │ │ │ │
│ │ │ └───────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
1.3 两大核心能力
| 能力 | 说明 | 核心接口 |
|---|---|---|
| 认证(Authentication) | 验证"你是谁" | AuthenticationManager |
| 授权(Authorization) | 验证"你能做什么" | AuthorizationManager |
二、过滤器链原理
2.1 DelegatingFilterProxy
DelegatingFilterProxy是Servlet容器与Spring容器之间的桥梁。Servlet容器只能管理web.xml或@WebServlet注册的Filter,而Spring Security的过滤器由Spring容器管理。DelegatingFilterProxy将请求委托给Spring容器中的FilterChainProxy。
java
// Spring Boot中自动注册(无需手动配置)
// 等价于在web.xml中:
// <filter>
// <filter-name>springSecurityFilterChain</filter-name>
// <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
// </filter>
// Spring Boot自动配置源码(SecurityFilterAutoConfiguration)
@Bean
public DelegatingFilterProxy springSecurityFilterChain() {
DelegatingFilterProxy proxy = new DelegatingFilterProxy("springSecurityFilterChain");
return proxy;
}
2.2 FilterChainProxy
FilterChainProxy是Spring Security的核心入口,它持有所有SecurityFilterChain实例,根据请求URL匹配对应的过滤器链。
java
// FilterChainProxy核心逻辑(简化)
public class FilterChainProxy extends GenericFilterBean {
private List<SecurityFilterChain> filterChains;
private HttpFirewall firewall;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 防火墙检查
HttpServletRequest httpRequest = (HttpServletRequest) request;
Request rejectRequest = firewall.getFirewalledRequest(httpRequest);
// 2. 匹配SecurityFilterChain
for (SecurityFilterChain filterChain : filterChains) {
if (filterChain.matches(httpRequest)) {
// 3. 执行该链中的所有Filter
VirtualFilterChain virtualChain = new VirtualFilterChain(filterChain);
virtualChain.doFilter(request, response);
return;
}
}
// 4. 无匹配链,直接放行
chain.doFilter(request, response);
}
}
2.3 SecurityFilterChain配置(Spring Security 6)
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 授权规则
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login", "/register").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().authenticated()
)
// 表单登录
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/api/login")
.successHandler(customSuccessHandler())
.failureHandler(customFailureHandler())
.permitAll()
)
// 登出
.logout(logout -> logout
.logoutUrl("/api/logout")
.logoutSuccessHandler(customLogoutSuccessHandler())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
)
// 异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(customAuthEntryPoint())
.accessDeniedHandler(customAccessDeniedHandler())
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
2.4 多SecurityFilterChain配置
java
@Configuration
@EnableWebSecurity
public class MultiChainSecurityConfig {
// API安全链(无状态,JWT)
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.csrf(csrf -> csrf.disable())
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// Web页面安全链(有状态,Session)
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form.loginPage("/login").permitAll())
.logout(logout -> logout.logoutSuccessUrl("/login?logout"));
return http.build();
}
}
三、核心过滤器详解
3.1 UsernamePasswordAuthenticationFilter
处理表单登录(POST /login)的核心过滤器,继承自AbstractAuthenticationProcessingFilter。
java
// 核心源码逻辑(简化)
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String usernameParameter = "username";
private String passwordParameter = "password";
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 提取用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 2. 创建未认证的Authentication Token
UsernamePasswordAuthenticationToken authRequest =
UsernamePasswordAuthenticationToken.unauthenticated(username, password);
// 3. 设置详情(IP、SessionId等)
setDetails(request, authRequest);
// 4. 委托AuthenticationManager进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
3.2 ExceptionTranslationFilter
捕获过滤器链中的异常,将AuthenticationException和AccessDeniedException转换为HTTP响应。
java
// 核心逻辑(简化)
public class ExceptionTranslationFilter extends GenericFilterBean {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
} catch (IOException | ServletException ex) {
Throwable cause = extractException(ex);
if (cause instanceof AuthenticationException) {
// 未认证 → 返回401或重定向到登录页
handleAuthenticationException(request, response, chain,
(AuthenticationException) cause);
} else if (cause instanceof AccessDeniedException) {
// 已认证但无权限 → 返回403
handleAccessDeniedException(request, response, chain,
(AccessDeniedException) cause);
} else {
throw ex;
}
}
}
private void handleAuthenticationException(HttpServletRequest request,
HttpServletResponse response, FilterChain chain,
AuthenticationException exception) throws IOException, ServletException {
// 保存当前请求到Session(登录后可恢复)
requestCache.saveRequest(request, response);
// 调用EntryPoint(如返回401 JSON或重定向登录页)
authenticationEntryPoint.commence(request, response, exception);
}
}
3.3 AuthorizationFilter(6.x)/ FilterSecurityInterceptor(5.x)
过滤器链中最后一个安全过滤器,执行最终的授权决策。
java
// Spring Security 6: AuthorizationFilter
public class AuthorizationFilter extends GenericFilterBean {
private final AuthorizationManager<HttpServletRequest> authorizationManager;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 调用AuthorizationManager进行授权检查
AuthorizationDecision decision =
authorizationManager.check(this::getAuthentication, httpRequest);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
}
}
四、认证流程详解
4.1 认证核心接口关系
AuthenticationManager(认证管理器)
└── ProviderManager(默认实现)
└── AuthenticationProvider(认证提供者,可多个)
└── DaoAuthenticationProvider
├── UserDetailsService(加载用户)
└── PasswordEncoder(密码验证)
4.2 AuthenticationManager与ProviderManager
java
// ProviderManager核心逻辑(简化)
public class ProviderManager implements AuthenticationManager {
private List<AuthenticationProvider> providers;
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Authentication result = null;
// 1. 遍历所有Provider尝试认证
for (AuthenticationProvider provider : providers) {
if (!provider.supports(authentication.getClass())) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
// 认证成功后擦除凭证(密码)
if (eraseCredentialsAfterAuthentication) {
((CredentialsContainer) result).eraseCredentials();
}
break;
}
} catch (AccountStatusException | InternalAuthenticationServiceException e) {
throw e;
} catch (AuthenticationException e) {
// 当前Provider认证失败,尝试下一个
}
}
// 2. 所有Provider都失败,尝试parent
if (result == null && parent != null) {
result = parent.authenticate(authentication);
}
// 3. 全部失败
if (result == null) {
throw new ProviderNotFoundException(
"ProviderManager cannot find a provider for " + authentication);
}
return result;
}
}
4.3 UserDetailsService自定义实现
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户基本信息
SysUser user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 2. 查询用户角色
List<String> roles = roleMapper.findRolesByUserId(user.getId());
// 3. 查询用户权限
List<String> permissions = permissionMapper.findPermissionsByUserId(user.getId());
// 4. 构建GrantedAuthority集合
List<GrantedAuthority> authorities = new ArrayList<>();
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
permissions.forEach(perm ->
authorities.add(new SimpleGrantedAuthority(perm)));
// 5. 返回UserDetails
return new User(
user.getUsername(),
user.getPassword(),
user.isEnabled(), // enabled
true, // accountNonExpired
true, // credentialsNonExpired
!user.isLocked(), // accountNonLocked
authorities
);
}
}
4.4 完整认证流程时序
1. 用户提交 POST /login (username, password)
2. UsernamePasswordAuthenticationFilter.attemptAuthentication()
3. 创建 UsernamePasswordAuthenticationToken(未认证)
4. AuthenticationManager.authenticate(token)
5. ProviderManager遍历AuthenticationProvider
6. DaoAuthenticationProvider.authenticate()
6.1 UserDetailsService.loadUserByUsername(username)
6.2 PasswordEncoder.matches(rawPassword, encodedPassword)
6.3 创建已认证的Authentication(含authorities)
7. SecurityContextHolder.getContext().setAuthentication(result)
8. 后续过滤器通过SecurityContext获取当前用户
五、PasswordEncoder详解
5.1 推荐配置
java
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// 使用DelegatingPasswordEncoder,支持多种算法平滑升级
// 默认使用bcrypt,前缀 {bcrypt}
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// 或者直接使用BCrypt
@Bean
public PasswordEncoder bcryptEncoder() {
// strength: 4-31,默认10,值越大越安全但越慢
return new BCryptPasswordEncoder(12);
}
}
5.2 DelegatingPasswordEncoder工作原理
java
// 存储格式: {算法id}编码后的密码
// 例如: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMQgel3u...
// {argon2}$argon2id$v=19$m=4096...
// {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
// 验证时根据前缀选择对应的Encoder
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String idPrefix = extractId(encodedPassword); // 提取 {bcrypt}
PasswordEncoder delegate = this.idToPasswordEncoder.get(idPrefix);
if (delegate == null) {
throw new IllegalArgumentException("No PasswordEncoder for id: " + idPrefix);
}
return delegate.matches(rawPassword, encodedPassword);
}
5.3 自定义PasswordEncoder
java
public class CustomPasswordEncoder implements PasswordEncoder {
private final BCryptPasswordEncoder delegate = new BCryptPasswordEncoder(12);
@Override
public String encode(CharSequence rawPassword) {
// 可在此添加盐值或其他预处理
return delegate.encode(rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return delegate.matches(rawPassword, encodedPassword);
}
@Override
public boolean upgradeEncoding(String encodedPassword) {
// 判断是否需要升级编码(如从strength=10升级到12)
return delegate.upgradeEncoding(encodedPassword);
}
}
六、授权模型
6.1 GrantedAuthority
java
// GrantedAuthority表示用户被授予的权限
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
// 常用实现
SimpleGrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
SimpleGrantedAuthority permUserRead = new SimpleGrantedAuthority("user:read");
SimpleGrantedAuthority permUserWrite = new SimpleGrantedAuthority("user:write");
6.2 URL级别授权配置
java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// 公开资源
.requestMatchers("/public/**", "/health", "/actuator/info").permitAll()
// 静态资源
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
// 角色控制
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
// 权限控制(细粒度)
.requestMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("user:read")
.requestMatchers(HttpMethod.POST, "/api/users/**").hasAuthority("user:write")
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasAuthority("user:delete")
// 表达式
.requestMatchers("/api/reports/**").access(
(auth, context) -> new AuthorizationDecision(
auth.get().getAuthorities().stream()
.anyMatch(a -> a.getAuthority().startsWith("report:"))
)
)
// 其余需认证
.anyRequest().authenticated()
);
return http.build();
}
6.3 AccessDecisionManager(5.x)与AuthorizationManager(6.x)
java
// === Spring Security 5.x: AccessDecisionManager ===
// 基于投票机制
public class AffirmativeBased extends AbstractAccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) {
int grant = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (result == ACCESS_GRANTED) {
grant++;
break; // 一票通过
}
}
if (grant == 0) {
throw new AccessDeniedException("Access Denied");
}
}
}
// === Spring Security 6.x: AuthorizationManager ===
// 函数式接口,更简洁
@FunctionalInterface
public interface AuthorizationManager<T> {
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
default void verify(Supplier<Authentication> authentication, T object) {
AuthorizationDecision decision = check(authentication, object);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
}
// 自定义AuthorizationManager示例
public class TimeBasedAuthorizationManager implements AuthorizationManager<HttpServletRequest> {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
HttpServletRequest request) {
LocalTime now = LocalTime.now();
// 仅允许工作时间访问
boolean granted = now.isAfter(LocalTime.of(9, 0))
&& now.isBefore(LocalTime.of(18, 0));
return new AuthorizationDecision(granted);
}
}
七、方法安全
7.1 启用方法安全
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(
prePostEnabled = true, // 启用@PreAuthorize/@PostAuthorize(6.x默认true)
securedEnabled = true, // 启用@Secured
jsr250Enabled = true // 启用@RolesAllowed
)
public class MethodSecurityConfig {
}
7.2 @PreAuthorize / @PostAuthorize
java
@Service
public class OrderService {
// 方法执行前检查:需要ADMIN角色
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long orderId) {
// 删除订单
}
// 方法执行前检查:需要特定权限
@PreAuthorize("hasAuthority('order:export')")
public List<Order> exportOrders() {
return orderMapper.findAll();
}
// 方法执行前检查:SpEL表达式,参数绑定
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public User getUserProfile(Long userId) {
return userMapper.findById(userId);
}
// 方法执行后检查:只能查看自己的订单
@PostAuthorize("returnObject.userId == authentication.principal.id")
public Order getOrder(Long orderId) {
return orderMapper.findById(orderId);
}
// 组合条件
@PreAuthorize("hasRole('MANAGER') and #amount < 10000")
@PostAuthorize("returnObject.status == 'APPROVED'")
public ApprovalResult approveExpense(Long expenseId, BigDecimal amount) {
return expenseService.approve(expenseId, amount);
}
}
7.3 @PreFilter / @PostFilter
java
@Service
public class DocumentService {
// 过滤集合参数:只保留当前用户有权限的文档ID
@PreFilter("hasPermission(filterObject, 'READ')")
public void batchRead(List<Long> documentIds) {
// documentIds已被过滤,只包含有权限的
}
// 过滤返回值:只返回当前用户拥有的文档
@PostFilter("filterObject.ownerId == authentication.principal.id")
public List<Document> getAllDocuments() {
return documentMapper.findAll();
}
}
7.4 @Secured与@RolesAllowed
java
@Service
public class LegacyService {
// @Secured: Spring Security原生,仅支持角色
@Secured("ROLE_ADMIN")
public void adminOperation() { }
@Secured({"ROLE_ADMIN", "ROLE_SUPER_ADMIN"})
public void superAdminOperation() { }
// @RolesAllowed: JSR-250标准(javax.annotation.security)
@RolesAllowed("ROLE_MANAGER")
public void managerOperation() { }
}
7.5 自定义PermissionEvaluator
java
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null) {
return false;
}
String targetType = targetDomainObject.getClass().getSimpleName();
return permissionService.hasPermission(
authentication.getName(), targetType, permission.toString());
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId, String targetType, Object permission) {
if (authentication == null || targetId == null) {
return false;
}
return permissionService.hasPermission(
authentication.getName(), targetType, targetId, permission.toString());
}
}
// 使用: @PreAuthorize("hasPermission(#orderId, 'Order', 'WRITE')")
八、JWT集成
8.1 JWT结构
Header.Payload.Signature
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "userId", "iat": 1700000000, "exp": 1700003600, "roles": ["ADMIN"]}
Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
8.2 Maven依赖
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
8.3 JWT工具类(Token生成/验证/刷新)
java
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expiration:3600000}")
private long accessTokenExpiration; // 1小时
@Value("${jwt.refresh-token-expiration:604800000}")
private long refreshTokenExpiration; // 7天
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成Access Token
*/
public String generateAccessToken(String username, List<String> roles) {
Date now = new Date();
Date expiry = new Date(now.getTime() + accessTokenExpiration);
return Jwts.builder()
.subject(username)
.claim("roles", roles)
.claim("type", "access")
.issuedAt(now)
.expiration(expiry)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/**
* 生成Refresh Token
*/
public String generateRefreshToken(String username) {
Date now = new Date();
Date expiry = new Date(now.getTime() + refreshTokenExpiration);
return Jwts.builder()
.subject(username)
.claim("type", "refresh")
.issuedAt(now)
.expiration(expiry)
.signWith(getSigningKey(), Jwts.SIG.HS256)
.compact();
}
/**
* 验证Token有效性
*/
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (ExpiredJwtException e) {
throw new JwtException("Token已过期");
} catch (MalformedJwtException e) {
throw new JwtException("Token格式错误");
} catch (SignatureException e) {
throw new JwtException("Token签名无效");
}
}
/**
* 从Token中解析Claims
*/
public Claims parseClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 获取用户名
*/
public String getUsernameFromToken(String token) {
return parseClaims(token).getSubject();
}
/**
* 获取角色列表
*/
@SuppressWarnings("unchecked")
public List<String> getRolesFromToken(String token) {
return parseClaims(token).get("roles", List.class);
}
/**
* 刷新Token(使用Refresh Token换取新的Access Token)
*/
public String refreshAccessToken(String refreshToken) {
Claims claims = parseClaims(refreshToken);
if (!"refresh".equals(claims.get("type", String.class))) {
throw new JwtException("无效的Refresh Token");
}
String username = claims.getSubject();
// 重新加载用户权限(可能已变更)
List<String> roles = loadCurrentRoles(username);
return generateAccessToken(username, roles);
}
private List<String> loadCurrentRoles(String username) {
// 从数据库重新加载
return new ArrayList<>();
}
}
8.4 自定义JWT认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 1. 从请求头提取Token
String token = resolveToken(request);
// 2. 验证Token并设置SecurityContext
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
// 3. 加载用户详情(或直接从Token解析权限)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 4. 创建已认证的Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
// 5. 设置到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (JwtException e) {
// Token无效,不设置认证信息,后续过滤器会返回401
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":401,\"message\":\"" + e.getMessage() + "\"}");
return;
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
8.5 JWT安全配置
java
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // JWT无状态,禁用CSRF
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/api/auth/refresh").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint)
)
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
九、OAuth2
9.1 Authorization Code(授权码模式)
java
// === OAuth2 Client配置(application.yml)===
// spring:
// security:
// oauth2:
// client:
// registration:
// github:
// client-id: ${GITHUB_CLIENT_ID}
// client-secret: ${GITHUB_CLIENT_SECRET}
// scope: read:user, user:email
// google:
// client-id: ${GOOGLE_CLIENT_ID}
// client-secret: ${GOOGLE_CLIENT_SECRET}
// scope: openid, profile, email
@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/oauth2/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
.successHandler(oAuth2SuccessHandler())
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}
// 自定义OAuth2用户服务
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = delegate.loadUser(userRequest);
// 将OAuth2用户映射到本地用户系统
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String email = oauth2User.getAttribute("email");
// 查找或创建本地用户
SysUser localUser = findOrCreateLocalUser(registrationId, email, oauth2User);
// 合并权限
Set<GrantedAuthority> authorities = new HashSet<>(oauth2User.getAuthorities());
authorities.add(new SimpleGrantedAuthority("ROLE_" + localUser.getRole()));
return new DefaultOAuth2User(authorities, oauth2User.getAttributes(), "email");
}
}
9.2 Client Credentials(客户端凭证模式)
java
// === 资源服务器配置(验证Client Credentials颁发的Token)===
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/internal/**").hasAuthority("SCOPE_internal:read")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 使用授权服务器的公钥验证JWT
return NimbusJwtDecoder.withJwkSetUri(
"http://auth-server:9000/oauth2/jwks").build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("scope");
converter.setAuthorityPrefix("SCOPE_");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}
// === 客户端调用(WebClient + Client Credentials)===
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(OAuth2AuthorizedClientManager clientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientManager);
oauth2Client.setDefaultClientRegistrationId("internal-service");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRepo,
OAuth2AuthorizedClientRepository clientRepository) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRepo, clientRepository);
// 配置Client Credentials授权
OAuth2AuthorizedClientProvider provider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
manager.setAuthorizedClientProvider(provider);
return manager;
}
}
9.3 Spring Authorization Server(授权服务器)
java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerFilterChain(
HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // 启用OpenID Connect
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-app")
.clientSecret("{bcrypt}$2a$10$...")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/web-app")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("read")
.scope("write")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
RegisteredClient serviceClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("internal-service")
.clientSecret("{bcrypt}$2a$10$...")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("internal:read")
.scope("internal:write")
.build();
return new InMemoryRegisteredClientRepository(webClient, serviceClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}
十、CSRF防护
10.1 CSRF攻击原理
1. 用户登录 bank.com,浏览器保存了Session Cookie
2. 用户访问 evil.com
3. evil.com页面包含: <form action="https://bank.com/transfer" method="POST">
4. 浏览器自动携带bank.com的Cookie发送请求
5. bank.com服务器认为是合法用户操作 → 转账成功
10.2 CsrfFilter工作机制
java
// CsrfFilter核心逻辑(简化)
public final class CsrfFilter extends OncePerRequestFilter {
private final CsrfTokenRepository tokenRepository;
private final RequestMatcher requireCsrfProtectionMatcher;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 1. 加载或生成CSRF Token
CsrfToken csrfToken = tokenRepository.loadToken(request);
if (csrfToken == null) {
csrfToken = tokenRepository.generateToken(request);
tokenRepository.saveToken(csrfToken, request, response);
}
// 2. 将Token放入request属性(供模板使用)
request.setAttribute(CsrfToken.class.getName(), csrfToken);
// 3. 判断是否需要验证(GET/HEAD/OPTIONS/TRACE不需要)
if (!requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 4. 验证Token
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
throw new InvalidCsrfTokenException(csrfToken, actualToken);
}
filterChain.doFilter(request, response);
}
}
10.3 CSRF配置策略
java
@Configuration
public class CsrfConfig {
// 方案1: 前后端分离(Cookie + Header)
@Bean
public SecurityFilterChain csrfWithCookie(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new XorCsrfTokenRequestAttributeHandler())
);
return http.build();
}
// 前端从Cookie读取XSRF-TOKEN,放入请求头X-XSRF-TOKEN
// 方案2: 无状态API(JWT),禁用CSRF
@Bean
public SecurityFilterChain csrfDisabled(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable());
return http.build();
}
// 方案3: 部分路径忽略CSRF
@Bean
public SecurityFilterChain csrfPartial(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/webhook/**", "/api/public/**")
);
return http.build();
}
}
十一、Session管理
11.1 SessionCreationPolicy
java
http.sessionManagement(session -> session
// ALWAYS: 每次请求都创建Session
// IF_REQUIRED: 需要时创建(默认)
// NEVER: 不主动创建,但使用已有的
// STATELESS: 完全不使用Session(JWT场景)
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
11.2 并发Session控制
java
http.sessionManagement(session -> session
.maximumSessions(1) // 最大并发Session数
.maxSessionsPreventsLogin(true) // 超出时拒绝新登录(而非踢掉旧Session)
.expiredUrl("/login?expired") // Session过期跳转
.sessionRegistry(sessionRegistry()) // Session注册表
);
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
// 查询在线用户
@Service
public class OnlineUserService {
@Autowired
private SessionRegistry sessionRegistry;
public List<String> getOnlineUsers() {
return sessionRegistry.getAllPrincipals().stream()
.filter(principal -> !sessionRegistry.getAllSessions(principal, false).isEmpty())
.map(Object::toString)
.collect(Collectors.toList());
}
public void forceLogout(String username) {
sessionRegistry.getAllPrincipals().stream()
.filter(p -> p.toString().equals(username))
.forEach(p -> sessionRegistry.getAllSessions(p, false)
.forEach(SessionInformation::expireNow));
}
}
11.3 Session固定攻击防护
java
http.sessionManagement(session -> session
.sessionFixation(fixation -> fixation
// changeSessionId: 保留Session属性,更换SessionId(默认,推荐)
.changeSessionId()
// migrateSession: 创建新Session,复制属性
// .migrateSession()
// newSession: 创建新Session,不复制属性
// .newSession()
)
);
11.4 分布式Session(Spring Session + Redis)
xml
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
java
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
十二、Spring Security 6新特性
12.1 Lambda DSL配置
java
// === Spring Security 5.x(旧式,已废弃)===
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login");
}
// === Spring Security 6.x(Lambda DSL)===
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
);
return http.build();
}
12.2 SecurityFilterChain Bean替代WebSecurityConfigurerAdapter
java
// === 5.x: 继承WebSecurityConfigurerAdapter(6.x已移除)===
@Configuration
public class OldConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception { ... }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { ... }
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
// === 6.x: 组件化Bean配置 ===
@Configuration
@EnableWebSecurity
public class NewConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// HTTP安全配置
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// 用户详情服务
return new CustomUserDetailsService();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
12.3 授权API变更
java
// 5.x → 6.x 对照
// authorizeRequests() → authorizeHttpRequests()
// antMatchers() → requestMatchers()
// mvcMatchers() → requestMatchers()(自动推断)
// AccessDecisionManager → AuthorizationManager
// 6.x新写法
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN")
// 自定义AuthorizationManager
.requestMatchers("/special/**").access(customAuthorizationManager())
);
// 方法安全: @EnableGlobalMethodSecurity → @EnableMethodSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
12.4 其他重要变更
java
// 1. SecurityContextHolderFilter替代SecurityContextPersistenceFilter
// 不再自动保存SecurityContext到Session,需显式配置
http.securityContext(context -> context
.requireExplicitSave(true) // 6.x默认true
);
// 2. AuthorizationFilter替代FilterSecurityInterceptor
// 无需手动配置,自动生效
// 3. 默认安全头增强
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'")
)
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
)
);
十三、实战:JWT + RBAC权限系统
13.1 数据库模型
sql
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(200) NOT NULL,
email VARCHAR(100),
enabled TINYINT DEFAULT 1,
locked TINYINT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(200)
);
-- 权限表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
perm_code VARCHAR(100) NOT NULL UNIQUE,
perm_name VARCHAR(100),
resource_type VARCHAR(20), -- MENU/BUTTON/API
resource_url VARCHAR(200)
);
-- 用户-角色关联
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
-- 角色-权限关联
CREATE TABLE sys_role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
13.2 登录控制器
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody @Valid LoginRequest request) {
try {
// 1. 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword())
);
// 2. 获取用户权限
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 3. 生成Token
String accessToken = jwtTokenProvider.generateAccessToken(
userDetails.getUsername(), roles);
String refreshToken = jwtTokenProvider.generateRefreshToken(
userDetails.getUsername());
return ResponseEntity.ok(new LoginResponse(
accessToken, refreshToken, "Bearer", 3600));
} catch (BadCredentialsException e) {
return ResponseEntity.status(401)
.body(new LoginResponse("用户名或密码错误"));
}
}
@PostMapping("/refresh")
public ResponseEntity<LoginResponse> refresh(@RequestBody RefreshRequest request) {
try {
String newAccessToken = jwtTokenProvider.refreshAccessToken(
request.getRefreshToken());
return ResponseEntity.ok(new LoginResponse(newAccessToken, null, "Bearer", 3600));
} catch (JwtException e) {
return ResponseEntity.status(401)
.body(new LoginResponse("Refresh Token无效或已过期"));
}
}
}
// 请求/响应DTO
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
@Data
@AllArgsConstructor
public class LoginResponse {
private String accessToken;
private String refreshToken;
private String tokenType;
private long expiresIn;
private String message;
public LoginResponse(String message) {
this.message = message;
}
}
13.3 动态权限加载
java
@Service
public class DynamicPermissionService {
@Autowired
private PermissionMapper permissionMapper;
/**
* 加载所有API权限映射(用于动态URL授权)
*/
public List<PermissionMapping> loadAllPermissionMappings() {
return permissionMapper.findAllApiPermissions();
}
}
// 动态SecurityFilterChain(基于数据库配置)
@Configuration
public class DynamicSecurityConfig {
@Autowired
private DynamicPermissionService permissionService;
@Bean
public SecurityFilterChain dynamicFilterChain(HttpSecurity http) throws Exception {
// 基础配置
http.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
// 动态加载权限规则
http.authorizeHttpRequests(auth -> {
List<PermissionMapping> mappings = permissionService.loadAllPermissionMappings();
for (PermissionMapping mapping : mappings) {
auth.requestMatchers(mapping.getMethod(), mapping.getUrl())
.hasAuthority(mapping.getPermCode());
}
auth.anyRequest().authenticated();
});
return http.build();
}
}
13.4 统一异常处理
java
@RestControllerAdvice
public class SecurityExceptionHandler {
// 认证失败(401)
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ErrorResponse> handleAuthException(AuthenticationException e) {
return ResponseEntity.status(401)
.body(new ErrorResponse(401, "认证失败: " + e.getMessage()));
}
// 授权失败(403)
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(403)
.body(new ErrorResponse(403, "权限不足: " + e.getMessage()));
}
}
// 自定义AuthenticationEntryPoint(返回JSON而非重定向)
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":401,\"message\":\"未认证,请先登录\",\"path\":\""
+ request.getRequestURI() + "\"}");
}
}
// 自定义AccessDeniedHandler
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":403,\"message\":\"权限不足\",\"path\":\""
+ request.getRequestURI() + "\"}");
}
}
十四、最佳实践
14.1 安全配置清单
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ProductionSecurityConfig {
@Bean
public SecurityFilterChain productionFilterChain(HttpSecurity http) throws Exception {
http
// 1. 强制HTTPS
.requiresChannel(channel -> channel
.anyRequest().requiresSecure()
)
// 2. 安全响应头
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true)
.preload(true)
)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.headerValue(
XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
)
)
// 3. CORS配置
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 4. 请求限流(配合自定义Filter)
.addFilterBefore(rateLimitFilter(), SecurityContextHolderFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://yourdomain.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-XSRF-TOKEN"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
14.2 密码安全
java
// 1. 使用强哈希算法
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt strength=12(约250ms/次,平衡安全与性能)
return new BCryptPasswordEncoder(12);
}
// 2. 密码强度验证
public class PasswordPolicyValidator {
private static final Pattern STRONG_PASSWORD =
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{10,}$");
public static boolean validate(String password) {
return STRONG_PASSWORD.matcher(password).matches();
}
}
// 3. 登录失败锁定
@Component
public class LoginAttemptService {
private final Cache<String, Integer> attemptsCache =
Caffeine.newBuilder().expireAfterWrite(Duration.ofMinutes(15)).build();
private static final int MAX_ATTEMPTS = 5;
public void loginFailed(String username) {
int attempts = attemptsCache.get(username, k -> 0);
attemptsCache.put(username, attempts + 1);
}
public void loginSucceeded(String username) {
attemptsCache.invalidate(username);
}
public boolean isBlocked(String username) {
return attemptsCache.get(username, k -> 0) >= MAX_ATTEMPTS;
}
}
14.3 JWT安全注意事项
java
// 1. 密钥管理:使用环境变量或密钥管理服务,不要硬编码
// application.yml:
// jwt:
// secret: ${JWT_SECRET} # 至少256位(32字节)
// 2. Token过期时间不宜过长
// Access Token: 15-60分钟
// Refresh Token: 7-30天
// 3. Token黑名单(登出/修改密码后使Token失效)
@Service
public class TokenBlacklistService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String BLACKLIST_PREFIX = "jwt:blacklist:";
public void blacklist(String token, long expirationMillis) {
long ttl = expirationMillis - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
BLACKLIST_PREFIX + token, "1", ttl, TimeUnit.MILLISECONDS);
}
}
public boolean isBlacklisted(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token));
}
}
// 4. 在JwtAuthenticationFilter中检查黑名单
if (tokenBlacklistService.isBlacklisted(token)) {
throw new JwtException("Token已被撤销");
}
14.4 微服务安全架构
┌──────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │────▶│ API Gateway │────▶│ Microservice A │
└──────────┘ │ (OAuth2 RS) │ │ (验证JWT) │
│ 路由+鉴权 │ └─────────────────┘
│ │ ┌─────────────────┐
│ │────▶│ Microservice B │
└──────────────┘ │ (Client Creds) │
│ └─────────────────┘
▼
┌──────────────┐
│ Auth Server │
│ (颁发Token) │
└──────────────┘
java
// Gateway全局过滤器(Spring Cloud Gateway)
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenProvider jwtTokenProvider;
private static final List<String> WHITE_LIST = List.of(
"/api/auth/login", "/api/auth/refresh", "/api/public/"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 白名单放行
if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// 验证Token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return unauthorized(exchange);
}
try {
jwtTokenProvider.validateToken(token.substring(7));
return chain.filter(exchange);
} catch (JwtException e) {
return unauthorized(exchange);
}
}
@Override
public int getOrder() {
return -100;
}
}
14.5 常见坑与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 403 Forbidden | CSRF Token缺失 | 无状态API禁用CSRF |
| 循环重定向到登录页 | EntryPoint配置错误 | 自定义返回JSON |
| @PreAuthorize不生效 | 未启用方法安全 | 添加@EnableMethodSecurity |
| Token解析失败 | 密钥不一致 | 统一密钥管理 |
| 跨域请求被拒 | CORS未配置 | 配置CorsConfigurationSource |
| Session丢失 | 分布式环境 | Spring Session + Redis |
| 权限变更后Token未更新 | JWT无状态 | 短过期+Refresh机制 |
| SecurityContext在异步线程丢失 | ThreadLocal | 使用DelegatingSecurityContextExecutor |
14.6 异步线程SecurityContext传播
java
@Configuration
public class AsyncSecurityConfig {
@Bean("securityAwareExecutor")
public Executor securityAwareExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setThreadNamePrefix("sec-async-");
executor.initialize();
// 包装Executor,自动传播SecurityContext
return new DelegatingSecurityContextExecutor(executor.getThreadPoolExecutor());
}
}
// 使用
@Service
public class ReportService {
@Async("securityAwareExecutor")
public CompletableFuture<Report> generateReport(Long userId) {
// 此线程中SecurityContext可用
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// ...
return CompletableFuture.completedFuture(report);
}
}
总结
Spring Security的核心设计围绕过滤器链展开,通过可插拔的过滤器实现认证、授权、CSRF防护、Session管理等安全能力。Spring Security 6通过Lambda DSL、SecurityFilterChain Bean、AuthorizationManager等改进,使配置更加简洁和类型安全。在实际项目中,应根据架构选择合适方案:传统应用用Session,前后端分离用JWT,微服务用OAuth2,并始终遵循最小权限、纵深防御的安全原则。
