安全是微服务架构的基石。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 Token | 15-60 分钟 | API 鉴权 | 内存/Header |
| Refresh Token | 7-30 天 | 换取新 Access Token | HttpOnly 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 与审计日志,可构建企业级安全防线。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。