12. Java 安全认证与授权体系

Spring Security 完整实战:OAuth2 授权码/密码/客户端模式、JWT 无状态认证、RBAC 权限模型与安全加固方案

安全是微服务架构的基石。Spring Security 提供了一套可扩展的认证(Authentication)与授权(Authorization)框架,结合 OAuth2 协议与 JWT 令牌,可实现分布式系统的无状态安全体系。本文从基础配置到高阶场景,构建完整的 Java 安全方案。

1. Spring Security 核心架构

1.1 过滤器链

请求 → SecurityContextPersistenceFilter → LogoutFilter
      → UsernamePasswordAuthenticationFilter → ...
      → FilterSecurityInterceptor → 目标资源

核心概念:

  • Authentication:封装用户身份信息( principal + credentials + authorities )
  • SecurityContext:线程级安全上下文,存储当前 Authentication
  • UserDetailsService:加载用户数据的接口,需对接实际用户系统
  • AccessDecisionManager:投票决定访问是否通过

1.2 最小安全配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 无状态 API 关闭 CSRF
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/v1/orders/**").authenticated()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter(), 
                UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
        AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

2. JWT 无状态认证

2.1 JWT 结构与签名

Header.Payload.Signature

Header:  { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "123", "roles": ["USER"], "iat": 1690000000, "exp": 1690003600 }
Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Payload 声明

声明含义
sub主题(用户 ID)
iss签发者
aud接收者
iat签发时间
exp过期时间
jti唯一标识(用于令牌黑名单)

2.2 JWT 工具类

@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration:86400000}") // 默认 24 小时
    private long jwtExpirationMs;
    
    private SecretKey key;
    
    @PostConstruct
    public void init() {
        this.key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
    }
    
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .toList());
        
        return Jwts.builder()
            .claims(claims)
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
            .signWith(key)
            .compact();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
            return !isTokenBlacklisted(token);
        } catch (JwtException | IllegalArgumentException e) {
            log.warn("Invalid JWT: {}", e.getMessage());
            return false;
        }
    }
    
    public String getUsernameFromToken(String token) {
        return Jwts.parser().verifyWith(key).build()
            .parseSignedClaims(token)
            .getPayload()
            .getSubject();
    }
    
    private boolean isTokenBlacklisted(String token) {
        // 登出时加入 Redis Set,TTL 设为 token 剩余有效期
        return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember("jwt:blacklist", token));
    }
}

2.3 JWT 认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                String username = tokenProvider.getUsernameFromToken(jwt);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        } catch (Exception e) {
            log.error("Cannot set user authentication", e);
        }
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if (StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

2.4 刷新令牌机制

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @PostMapping("/refresh")
    public ResponseEntity<TokenResponse> refreshToken(
        @RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();
        
        if (!refreshTokenProvider.validate(refreshToken)) {
            throw new InvalidTokenException("Refresh token expired");
        }
        
        String username = refreshTokenProvider.getUsername(refreshToken);
        UserDetails user = userDetailsService.loadUserByUsername(username);
        
        // 刷新令牌使用一次即删除(轮换机制)
        refreshTokenProvider.delete(refreshToken);
        
        String newAccess = jwtTokenProvider.generateToken(user);
        String newRefresh = refreshTokenProvider.createToken(user);
        
        return ResponseEntity.ok(new TokenResponse(newAccess, newRefresh));
    }
}
令牌类型有效期用途存储
Access Token15-60 分钟API 鉴权内存/Header
Refresh Token7-30 天换取新 Access TokenHttpOnly Cookie

3. OAuth2 授权服务器

3.1 四种授权模式

模式场景安全性
授权码模式Web 应用、SPA 授权最高
简化模式纯前端应用(已废弃,推荐 PKCE)
密码凭证第一方应用(官方 App)
客户端凭证服务端到服务端

3.2 Spring Authorization Server(OAuth2 新标准)

Spring Security OAuth 项目已停止维护,新项目使用 Spring Authorization Server

@Configuration
public class AuthorizationServerConfig {
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.build();
    }
    
    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("web-client")
            .clientSecret("{bcrypt}$2a$10$...")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .redirectUri("http://localhost:8080/login/oauth2/code/web-client")
            .scope(OidcScopes.OPENID)
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder()
                .requireAuthorizationConsent(true)  // 显式授权确认页
                .build())
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(30))
                .refreshTokenTimeToLive(Duration.ofDays(7))
                .build())
            .build();
        return new InMemoryRegisteredClientRepository(client);
    }
    
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        KeyPair keyPair = generateRsaKey();
        RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
            .privateKey((RSAPrivateKey) keyPair.getPrivate())
            .keyID(UUID.randomUUID().toString())
            .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, context) -> jwkSelector.select(jwkSet);
    }
    
    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
}

3.3 OAuth2 资源服务器

@Configuration
@EnableMethodSecurity
public class ResourceServerConfig {
    
    @Bean
    public SecurityFilterChain resourceFilterChain(HttpSecurity http) throws Exception {
        http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = 
            new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthoritiesClaimName("roles");
        authoritiesConverter.setAuthorityPrefix("ROLE_");
        
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
}

3.4 社交登录(Google/GitHub)

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid,profile,email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user,user:email

4. RBAC 权限模型

4.1 数据模型

-- 五表结构:用户-角色-权限 + 用户角色关联 + 角色权限关联
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    enabled BOOLEAN DEFAULT true
);

CREATE TABLE roles (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL,  -- ROLE_ADMIN, ROLE_USER
    description VARCHAR(255)
);

CREATE TABLE permissions (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,        -- user:create, order:delete
    resource VARCHAR(50) NOT NULL,
    action VARCHAR(20) NOT NULL        -- CREATE, READ, UPDATE, DELETE
);

CREATE TABLE user_roles (
    user_id BIGINT REFERENCES users(id),
    role_id BIGINT REFERENCES roles(id),
    PRIMARY KEY (user_id, role_id)
);

CREATE TABLE role_permissions (
    role_id BIGINT REFERENCES roles(id),
    permission_id BIGINT REFERENCES permissions(id),
    PRIMARY KEY (role_id, permission_id)
);

4.2 方法级安全注解

@Service
public class OrderService {
    
    @PreAuthorize("hasRole('ADMIN') or hasRole('ORDER_MANAGER')")
    public List<OrderDTO> listAllOrders() { ... }
    
    @PreAuthorize("hasPermission(#orderId, 'Order', 'READ')")
    public OrderDTO getOrder(Long orderId) { ... }
    
    @PreAuthorize("@orderSecurity.isOwner(#orderId, authentication.name)")
    public void cancelOrder(Long orderId) { ... }
    
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public OrderDTO getOrderSecure(Long orderId) { ... }
    
    @Secured({"ROLE_ADMIN"})  // 旧式注解
    public void deleteUser(Long userId) { ... }
}

4.3 基于 SpEL 的自定义权限评估

@Component("orderSecurity")
public class OrderSecurity {
    
    @Autowired
    private OrderRepository orderRepository;
    
    public boolean isOwner(Long orderId, String username) {
        return orderRepository.findById(orderId)
            .map(order -> order.getBuyerName().equals(username))
            .orElse(false);
    }
}

启用方法安全:

@Configuration
@EnableMethodSecurity(
    prePostEnabled = true,   // @PreAuthorize / @PostAuthorize
    securedEnabled = true,   // @Secured
    jsr250Enabled = true     // @RolesAllowed
)
public class MethodSecurityConfig { }

5. 安全加固清单

5.1 HTTPS 强制

@Configuration
public class HttpsConfig {
    
    @Bean
    public SecurityFilterChain httpsFilterChain(HttpSecurity http) throws Exception {
        http
            .requiresChannel(channel -> channel
                .anyRequest().requiresSecure()) // 强制 HTTPS
            .headers(headers -> headers
                .httpStrictTransportSecurity(hsts -> hsts
                    .maxAgeInSeconds(31536000)
                    .includeSubDomains(true)
                )
            );
        return http.build();
    }
}

5.2 安全头部

http.headers(headers -> headers
    .contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
    .frameOptions(frame -> frame.deny())           // 防止点击劫持
    .xssProtection(xss -> xss.disable())           // 现代浏览器用 CSP 替代
    .contentTypeOptions(cto -> cto.disable())      // X-Content-Type-Options: nosniff
    .referrerPolicy(referrer -> 
        referrer.policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
);

5.3 密码安全

// 密码强度校验
public class PasswordValidator {
    private static final Pattern PATTERN = Pattern.compile(
        "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{12,}$"
    );
    
    public boolean isValid(String password) {
        return PATTERN.matcher(password).matches();
    }
}

// 密码哈希迭代(Spring Security 5+ 默认使用 DelegatingPasswordEncoder)
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    // 自动识别前缀:{bcrypt}, {pbkdf2}, {scrypt}, {argon2}
}

5.4 登录安全

@Component
public class LoginAttemptService {
    private final LoadingCache<String, Integer> attempts;
    
    public LoginAttemptService() {
        attempts = CacheBuilder.newBuilder()
            .expireAfterWrite(15, TimeUnit.MINUTES)
            .build(CacheLoader.from(() -> 0));
    }
    
    public void loginSucceeded(String key) {
        attempts.invalidate(key);
    }
    
    public void loginFailed(String key) {
        int attempts = this.attempts.getUnchecked(key);
        this.attempts.put(key, attempts + 1);
    }
    
    public boolean isBlocked(String key) {
        return attempts.getUnchecked(key) >= 5;
    }
}

6. 多因素认证(MFA)

@Service
public class MfaService {
    
    // TOTP 基于时间的一次性密码(Google Authenticator 兼容)
    public String generateSecret() {
        return new DefaultSecretGenerator().generate();
    }
    
    public String generateQrUrl(String username, String secret, String issuer) {
        return new QrGenerator().generate(
            username, secret, issuer, 
            BarcodeImageType.PNG, 200, 200);
    }
    
    public boolean verifyCode(String secret, String code) {
        return new TimeProvider().getTime() / 30 == 
            Integer.parseInt(code) / 100000; // 简化示意,实际使用 Totp 验证器
    }
}

7. API 安全审计与日志

@Component
public class SecurityAuditListener {
    
    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        log.info("[AUTH_SUCCESS] user={}, source={}, time={}",
            event.getAuthentication().getName(),
            getClientIp(),
            LocalDateTime.now());
    }
    
    @EventListener
    public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        log.warn("[AUTH_FAILURE] user={}, reason={}, ip={}",
            event.getAuthentication().getName(),
            event.getException().getMessage(),
            getClientIp());
    }
    
    @EventListener
    public void onAuthorizationDenied(AuthorizationDeniedEvent event) {
        log.error("[ACCESS_DENIED] user={}, resource={}, decision={}",
            event.getAuthentication().get().getName(),
            event.getAuthorizationDecision(),
            getRequestUri());
    }
}

总结

层次方案要点
传输层TLS 1.3 + HSTS强制 HTTPS,防范中间人攻击
认证层JWT + OAuth2无状态令牌,刷新令牌轮换
授权层RBAC + 方法级注解最小权限原则,SpEL 表达式
应用层Spring Security 过滤器链CSRF 关闭(API)、CORS 白名单
数据层BCrypt/Argon2 + 密码策略加盐哈希,防彩虹表
审计层安全事件日志登录失败、访问拒绝监控

现代 Java 安全体系的核心是:OAuth2 负责认证流转,JWT 负责无状态传输,Spring Security 负责权限控制,RBAC 负责授权管理。配合密码策略、登录保护、MFA 与审计日志,可构建企业级安全防线。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析