前言
微服务架构带来了系统拆分、独立部署、技术异构等诸多优势,同时也引入了全新的安全挑战------服务间通信如何保障?用户身份如何在多个服务间传递?权限如何统一管控?本文从实际工程痛点出发,结合 Spring Security 的核心机制与 Spring Cloud 微服务架构,系统讲解如何构建一套生产级的统一认证体系。
一、核心问题:微服务为什么需要统一认证?
1.1 传统单体 vs 微服务安全困境
| 对比维度 | 单体应用 | 微服务应用 |
|---|---|---|
| 认证位置 | 单一入口完成认证 | 每个服务都可能被直接访问 |
| 会话管理 | 本地 Session | 需要分布式 Session 或 Token |
| 权限控制 | 统一权限模型 | 各服务独立权限需保持一致性 |
| 服务间调用 | 内部方法调用,无需认证 | 远程调用需要传递身份信息 |
微服务架构中,用户的一次请求往往需要经过多个服务协同处理。如果每个服务都独立实现认证逻辑,不仅代码冗余,还面临以下核心挑战:
- 安全逻辑分散:各服务独立实现认证授权,难以统一管控和审计
- 服务间身份传递困难:用户身份信息需要在调用链中持续传递,且需防篡改
- 权限管理碎片化:权限变更需逐个修改多个服务,运维成本高
- 单点登录需求:用户登录A服务后访问B服务不应重复认证
1.2 解决方案:OAuth2.0 + JWT 统一认证体系
核心思想:将安全逻辑收拢到网关和认证服务中,业务服务零安全负担,专注业务逻辑实现。
┌─────────────────────────────────────────────────────────────────────────┐
│ 统一认证体系架构图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ 前端应用 │ ──→ │ Spring Cloud │ ──→ │ 认证服务 │ │
│ │ (客户端) │ │ Gateway │ │ (Authorization Server) │ │
│ └──────────┘ │ (统一入口) │ │ 颁发/验证 JWT Token │ │
│ └──────────────┘ └──────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 资源服务器集群 │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │用户服务 │ │订单服务 │ │支付服务 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ 各服务通过 JWT 验证用户身份和权限 │ │
│ └──────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
二、Spring Security 核心机制回顾
在深入认证服务实现之前,先回顾 Spring Security 的几个核心概念,它们构成了认证服务的底层基础。
2.1 认证架构核心组件
| 组件 | 职责 | 通俗理解 |
|---|---|---|
| SecurityContextHolder | 存储当前用户信息(线程绑定) | 当前用户的白板 |
| Authentication | 用户身份凭证容器 | 白板上写的内容(谁、密码、权限) |
| AuthenticationManager | 身份验证总入口 | 门卫大队:验证身份 |
| ProviderManager | AuthenticationManager 的默认实现 | 大队长:管一群认证专家 |
| AuthenticationProvider | 具体认证方式实现 | 认证专家(用户名密码/JWT/OAuth2) |
| UserDetailsService | 加载用户信息的数据源接口 | 从数据库/内存加载用户信息 |
2.2 认证流程简述
用户提交登录请求
↓
UsernamePasswordAuthenticationFilter 提取用户名密码
↓
AuthenticationManager 验证(ProviderManager 派专家处理)
↓
DaoAuthenticationProvider 查数据库比对密码
↓
认证成功 → SecurityContextHolder 存入 Authentication
↓
后续请求通过 Session/JWT 恢复用户身份
2.3 几种认证方式对比
| 认证方式 | 适用场景 | 特点 |
|---|---|---|
| 表单登录 | 传统 Web 应用 | 通过 Session 维持状态 |
| HTTP Basic | API 简单认证 | 每次请求携带用户名密码(需 HTTPS) |
| JWT 令牌认证 | 前后端分离/微服务 | 无状态,适合分布式 |
| OAuth2 认证 | 第三方授权登录 | 标准授权协议 |
| Remember-Me | 提升用户体验 | 长期有效,有安全风险 |
| Pre-Authentication | 外部系统已认证 | 信任外部系统传递的身份 |
三、认证服务核心实现
3.1 项目依赖配置
xml
<!-- 认证服务 pom.xml 核心依赖 -->
<dependencies>
<!-- Spring Security 6.x + OAuth2 授权服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.2.1</version>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.23</version>
</dependency>
<!-- 服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
注意 :Spring Security 6.x 对 OAuth2 授权服务器进行了重大重构,采用全新的 API,不再使用已废弃的
AuthorizationServerConfigurerAdapter。
3.2 认证服务核心配置
java
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
// 1. 授权服务器过滤器链(优先级最高)
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
return http
.exceptionHandling(exceptions -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.build();
}
// 2. 客户端注册信息
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("web-client")
.clientSecret("{bcrypt}$2a$10$...") // BCrypt 加密
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/web-client")
.scope("read", "write", "openid")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(client);
}
// 3. JWT 签名密钥源(RSA 非对称加密)
@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();
return new ImmutableJWKSet<>(new JWKSet(rsaKey));
}
// 4. JWT 解码器
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
}
3.3 用户信息加载服务
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
// 将角色转换为 GrantedAuthority
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
return User.builder()
.username(user.getUsername())
.password(user.getPassword()) // BCrypt 加密后的密文
.authorities(authorities)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}
3.4 JWT Token 服务
java
@Service
@Slf4j
public class JwtTokenService {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration:3600}")
private Long expiration;
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userDetails.getUsername());
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
claims.put("created", new Date());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secretKey.getBytes(StandardCharsets.UTF_8))
.compact();
}
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token, UserDetails userDetails) {
Claims claims = parseToken(token);
String username = claims.getSubject();
return username.equals(userDetails.getUsername()) && !isTokenExpired(claims);
}
private boolean isTokenExpired(Claims claims) {
Date expiration = claims.getExpiration();
return expiration.before(new Date());
}
}
四、网关层 JWT 鉴权实现
网关作为统一入口,承担 JWT 校验的核心职责,避免每个微服务重复实现鉴权逻辑。
4.1 网关路由配置
yaml
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/auth/**
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
4.2 JWT 全局鉴权过滤器
java
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtTokenService jwtTokenService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().toString();
// 放行认证接口和公共路径
if (path.contains("/auth/token") || path.contains("/auth/refresh") || path.contains("/public")) {
return chain.filter(exchange);
}
// 提取 Token
String token = request.getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return unauthorizedResponse(exchange, "缺少 Token");
}
token = token.substring(7);
try {
// 解析 Token 获取用户信息
Claims claims = jwtTokenService.parseToken(token);
String username = claims.getSubject();
String roles = claims.get("roles", String.class);
// 将用户信息转发给下游服务(通过 Header)
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Name", username)
.header("X-User-Roles", roles)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
} catch (ExpiredJwtException e) {
return unauthorizedResponse(exchange, "Token 已过期");
} catch (Exception e) {
return unauthorizedResponse(exchange, "无效 Token");
}
}
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = String.format("{\"code\":401,\"msg\":\"%s\"}", msg);
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -1; // 优先级最高
}
}
五、资源服务器配置
业务微服务通过验证 JWT 获取用户身份,无需与认证服务持续通信。
java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtDecoder(jwtDecoder())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 从认证服务获取公钥进行验证
return NimbusJwtDecoder.withJwkSetUri("http://auth-service/oauth2/jwks").build();
}
}
在 Controller 中获取用户信息
java
@RestController
@RequestMapping("/api/user")
public class UserController {
// 方式一:通过 SecurityContextHolder 获取
@GetMapping("/me")
public Map<String, Object> getCurrentUser() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return Map.of(
"username", auth.getName(),
"authorities", auth.getAuthorities()
);
}
// 方式二:通过 @AuthenticationPrincipal 获取
@GetMapping("/info")
public Map<String, Object> getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return Map.of(
"username", userDetails.getUsername(),
"authorities", userDetails.getAuthorities()
);
}
// 方式三:从网关转发的 Header 获取
@GetMapping("/from-header")
public Map<String, Object> getFromHeader(@RequestHeader("X-User-Name") String username) {
return Map.of("username", username);
}
}
六、服务间 Token 传递(Feign 拦截器)
微服务间的调用需要自动传递 JWT,使用 Feign 拦截器实现。
java
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor oauth2RequestInterceptor() {
return requestTemplate -> {
// 从 SecurityContext 获取当前 Token
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getCredentials() != null) {
String token = authentication.getCredentials().toString();
requestTemplate.header("Authorization", "Bearer " + token);
}
};
}
}
七、安全实践与最佳建议
7.1 令牌生命周期管理
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Access Token 有效期 | 1-2 小时 | 平衡安全性与用户体验 |
| Refresh Token 有效期 | 7-30 天 | 支持无感续期 |
| 令牌刷新策略 | Refresh Token 机制 | 避免频繁登录 |
7.2 CSRF 防护机制
Spring Security 默认启用 CSRF 防护,原理是通过 Synchronizer Token 模式或 SameSite Cookie 属性实现。
| 防护机制 | 原理 | 适用场景 |
|---|---|---|
| Synchronizer Token | 每个表单包含随机 Token,服务器验证 | 传统 Web 表单提交 |
| SameSite Cookie | 浏览器限制跨站携带 Cookie | 现代浏览器(深度防御) |
注意事项:
- 前后端分离应用(JWT 存于 Header)可酌情禁用 CSRF
- 文件上传场景需特殊处理 CSRF(Token 置于请求体或 URL)
7.3 安全响应头配置
Spring Security 默认添加的安全响应头及作用:
| 响应头 | 作用 | 默认值 |
|---|---|---|
X-Content-Type-Options |
防止内容嗅探攻击 | nosniff |
X-Frame-Options |
防止点击劫持 | DENY |
Strict-Transport-Security |
强制 HTTPS(仅 HTTPS 请求) | max-age=31536000; includeSubDomains |
Cache-Control |
防止缓存敏感信息 | no-cache, no-store, max-age=0 |
7.4 代理服务器配置注意事项
当应用部署在负载均衡器(如 Nginx)后方时,需配置 X-Forwarded-* 头处理:
properties
# Spring Boot 配置
server.use-forward-headers=true
server.forward-headers-strategy=native
否则应用无法感知真实客户端信息,可能导致重定向 URL 错误、CSRF 校验失败等问题。
7.5 注销功能安全实现
Spring Security 默认提供 /logout 端点,执行以下清理动作:
- HttpSession 失效(
SecurityContextLogoutHandler) - SecurityContextHolder 清理
- SecurityContextRepository 清理
- Remember-Me 认证清理
- CSRF Token 清理
- 触发注销成功事件
java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
.addLogoutHandler((request, response, authentication) -> {
// 自定义清理逻辑:如清除缓存
cacheService.evict(authentication.getName());
})
);
return http.build();
}
八、总结
本文从微服务安全挑战出发,系统介绍了基于 Spring Security OAuth2 + JWT 构建统一认证服务的完整方案:
- 认证服务:负责集中认证和 Token 颁发,承载用户信息加载、密码比对、Token 生成等核心职责
- 网关服务:负责请求路由和 JWT 鉴权,作为安全防护的第一道防线
- 资源服务:通过 JWT 验证获取用户身份,专注业务逻辑,零安全负担
这套方案的关键价值在于:将安全逻辑收拢到网关和认证服务,业务服务零安全负担。通过 OAuth2 标准协议 + JWT 无状态令牌,实现了统一认证、服务间安全通信和细粒度权限控制,是构建生产级微服务系统的安全基石。
本文综合了 Spring Security 官方文档最佳实践与生产环境经验,部分代码示例基于 Spring Security 6.2.0-SNAPSHOT 版本。