Java深入解析篇十七之Spring Security

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,并始终遵循最小权限、纵深防御的安全原则。

相关推荐
离陌在学C#3 小时前
C# 重载与重写:深入理解面向对象编程的核心概念
java·c#
洛阳泰山4 小时前
AI 应用层被 Python 卷成红海,为什么我偏要用 Java 造一个 RAG + 工作流引擎?
java·人工智能·后端
二十雨辰4 小时前
[Java]-Spring面试题
java·开发语言
老马历写记4 小时前
Maven POM 依赖管理总结
java·maven·system·pom·optional
古法安卓4 小时前
Android-车机 GNSS 定位数据接收问题排查
android·java·android studio
_oP_i4 小时前
python 后缀 mjs文件
开发语言·python
蔬菜_4 小时前
前端转全栈-day5(数组、list、set)
java·前端·数据结构·list
萧瑟余晖4 小时前
Java深入解析篇二十二之虚拟线程
java·开发语言
OuO-24 小时前
笔试强训 Day 34:ISBN 号码、kotori 和迷宫、矩阵最长递增路径
java·算法·矩阵