在微服务进入生产环境之后,安全问题往往不再是“是否要做”,而是“做到什么层次才足够”。很多团队一开始只做了登录鉴权,后来才发现真正的风险来自更复杂的场景:网关被绕过、内部服务被伪装、JWT无法撤销、服务间调用身份丢失、证书过期导致全链路雪崩、认证中心成为吞吐瓶颈。
这篇文章不会仅仅停留在“怎么配通”的入门教程,而是从架构师和一线工程实践的角度,完整拆解一套可落地、可扩展、可审计、可压测的Spring Cloud微服务安全体系。核心技术栈包括:
- OAuth2:解决统一授权框架问题
- JWT:解决分布式无状态令牌问题
- Spring Cloud Gateway:解决统一入口与边界防护问题
- mTLS:解决服务到服务的强身份认证与链路加密问题
文章目标有四个:
- 讲清楚原理,不只讲配置
- 讲清楚架构,不只讲单点组件
- 讲清楚工程化,不只讲Demo代码
- 讲清楚生产问题,不只讲理想路径
一、为什么微服务安全不能只靠JWT
很多团队对“微服务安全”的第一反应是“给接口加个JWT校验就行”。这在单体或低复杂度系统里可能勉强够用,但在真正的微服务环境中,JWT只能解决一部分问题。
1.1 单点JWT方案的常见缺陷
- 只能证明“请求里带了合法令牌”,不能证明“调用方服务本身可信”
- 只能保护应用层身份,不能保护传输链路不被中间人攻击
- 无法天然解决服务间身份透传、权限收敛、令牌撤销、密钥轮转
- 下游服务如果都各自解析JWT,会产生重复逻辑和实现漂移
- 一旦内部网络被突破,纯Header/JWT模式容易被横向移动攻击利用
换句话说,JWT更像“数字门票”,但生产级微服务安全需要的是一整套“门禁系统”。
1.2 生产环境的真实威胁模型
设计方案之前,先明确威胁来自哪里。微服务安全通常至少要覆盖以下风险:
- 外部未授权请求直接打到网关
- 攻击者伪造或重放访问令牌
- 内部服务被假冒,伪装成合法服务访问核心接口
- 敏感接口被越权访问,例如普通用户访问管理员资源
- 网关后的服务被直接暴露,绕过统一鉴权入口
- 令牌或证书泄露后,短时间内造成大规模滥用
- Auth Server、JWKS、Redis黑名单等安全基础设施本身成为瓶颈
因此,安全体系必须至少同时解决四类问题:
- 谁在访问:身份认证
- 能访问什么:权限控制
- 调用链是否可信:传输与服务身份认证
- 出问题能否追踪:审计与可观测性
二、总体架构:边界鉴权 + 服务认证 + 最小权限
先给出一套适用于大多数企业Spring Cloud项目的参考架构。
用户 / App / Web / 第三方系统
|
[ API Gateway ]
|
+--------------+--------------+
| |
JWT 校验 / 限流 / 审计 Token Relay / Header 透传
| |
+--------------+--------------+
|
[ Business Service A ]
|
Feign / WebClient
|
[ Business Service B ]
|
DB / Cache / MQ
旁路安全基础设施:
- Authorization Server:签发 JWT、发布 JWKS、管理客户端
- Redis:JWT 黑名单、幂等键、限流计数
- Vault / KMS:私钥、client_secret、证书材料管理
- Prometheus / Grafana / ELK:安全指标与审计日志
- CA / cert-manager / Istio:证书签发、轮换、mTLS 治理
2.1 各组件职责分工
| 组件 |
责任边界 |
| API Gateway |
统一入口、认证校验、路由、限流、黑白名单、审计、令牌中继 |
| Authorization Server |
用户认证、客户端认证、JWT签发、JWK发布、Token生命周期管理 |
| Business Service |
聚焦业务逻辑,只消费身份上下文,不重复造鉴权轮子 |
| Redis |
黑名单、幂等、防重放、热点权限缓存 |
| mTLS 层 |
服务间链路加密、双向身份认证、阻断内部伪装调用 |
| 配置与密钥中心 |
私钥、client_secret、truststore、keystore动态管理 |
2.2 设计原则
- 零信任:不默认信任任何网络位置,内外都认证
- 分层防御:网关、应用、链路三层联动
- 最小权限:Scope、Role、Audience都按需收敛
- 短令牌生命周期:降低泄露后的风险窗口
- 密钥与证书可轮转:设计时就考虑轮换,不要靠人工救火
- 安全基础设施高可用:Auth Server、Redis、CA都不能是单点
三、认证与授权原理:OAuth2、JWT、Scope、Audience到底在解决什么
3.1 OAuth2解决的是“授权委托”,不是“权限存储”
OAuth2的核心价值是把“用户认证”“客户端身份”“资源访问”这几件事拆开,通过标准令牌流转把访问权交给资源服务器去验证。
在微服务中最常见的是两类授权流:
- 用户访问网关:授权码模式 + PKCE
- 服务间调用:
client_credentials
可以这样理解:
- 用户令牌代表“某个用户”
- 服务令牌代表“某个服务客户端”
- 网关负责接住外部令牌
- 下游资源服务负责消费令牌中的声明
OAuth2不直接替你设计权限模型。真正的权限控制通常还要落到:
scope:粗粒度授权范围
roles:角色
permissions:细粒度动作权限
aud:令牌目标受众
tenant_id:多租户隔离
3.2 JWT的本质:可验证、可携带、可分发的声明集合
JWT由三部分组成:
- Header:签名算法、密钥标识
kid
- Payload:声明,例如
sub、scope、aud、exp
- Signature:签名,保证令牌未被篡改
3.2.1 为什么JWT适合微服务
- 无状态,本地即可校验
- 不依赖中心Session,天然适合集群和弹性扩缩容
- 可携带上下文,减少服务间反查用户中心
- 跨语言兼容,适合异构服务体系
3.2.2 为什么JWT也有明显边界
- 自包含不等于可撤销
- Payload默认可读,不适合存敏感信息
- 令牌越大,网络和网关开销越高
- 权限变更不是实时生效,需要配合短TTL或撤销机制
3.2.3 生产级JWT声明建议
推荐至少包含以下标准字段:
| 字段 |
作用 |
iss |
签发者,防止伪造来源 |
sub |
主体,用户ID或服务ID |
aud |
目标服务,避免一个令牌到处乱用 |
exp |
过期时间 |
iat |
签发时间 |
jti |
唯一令牌ID,便于撤销与审计 |
scope |
OAuth2范围 |
业务字段建议控制在最小集合,例如:
tenant_id
roles
client_id
trace_id 不建议直接写入JWT,应该放链路追踪头
3.3 自校验JWT与Introspection的架构取舍
很多文章只说“JWT自校验快”,但没有讲清楚为什么。
3.3.1 Token Introspection
每次请求都把Token发给授权服务器验证:
- 优点:令牌撤销实时、控制集中
- 缺点:每个请求都依赖网络I/O,Auth Server容易成为瓶颈
3.3.2 JWT本地自校验
资源服务器通过JWKS获取公钥,本地完成签名校验:
- 优点:性能高、低耦合、适合高并发
- 缺点:撤销不天然实时,需要黑名单或短TTL补足
3.3.3 生产选型建议
绝大多数高并发微服务系统采用:
- 访问令牌:JWT本地校验
- 高风险操作:补充黑名单、二次校验或一次性凭证
- 管理端/后台:权限变更后缩短TTL或强制登出
一句话总结:高吞吐走本地校验,强实时撤销靠补充机制。
四、生产级架构设计:为什么要Gateway + Resource Server + mTLS三层协同
如果只在Gateway校验JWT,下游服务会默认信任来自网关的Header;如果内部网络被攻破,这种信任可能失效。反过来,如果每个服务都做完整OAuth2流程,又会造成逻辑重复和性能浪费。
更合理的模式是“三层协同”:
4.1 第一层:Gateway做统一边界防护
负责:
- JWT校验
- 路由
- 限流
- 黑白名单
- 安全响应头
- 审计日志
- 令牌中继
4.2 第二层:业务服务做资源级授权
负责:
- 基于
scope / roles / tenant_id 的接口授权
- 方法级鉴权
- 数据级权限控制
4.3 第三层:mTLS做服务身份认证
负责:
- 确认“调用方服务是谁”
- 阻止伪造服务直连核心服务
- 为内部调用提供强加密链路
这一层组合的关键价值在于:
- JWT回答“用户/客户端有没有权限”
- mTLS回答“调用这个接口的服务本身是否可信”
- Gateway回答“请求是否从合法入口进入”
三者各管一件事,不能互相替代。
五、Authorization Server生产实践
下面以Spring Authorization Server为例,给出更接近生产环境的配置思路。
5.1 Maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
</dependencies>
5.2 客户端注册与令牌策略
@Configuration
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
RegisteredClient gatewayClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("gateway")
.clientSecret(passwordEncoder.encode("gateway-secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("order.read")
.scope("order.write")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofMinutes(15))
.reuseRefreshTokens(false)
.build())
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.build())
.build();
return new InMemoryRegisteredClientRepository(gatewayClient);
}
}
这段代码能跑,但生产环境还要进一步升级:
RegisteredClientRepository 存DB,不要只放内存
client_secret 不落代码,走Vault / KMS注入
- 不同服务不同
scope,不要共用万能客户端
- Token TTL按业务分级,高风险接口更短
5.3 RSA / EC密钥与JWKS发布
生产环境不要用对称密钥把所有服务绑死在一起,更推荐非对称签名:
- Auth Server用私钥签发JWT
- Gateway / Resource Server用公钥校验
- 通过JWKS统一发布多把公钥,支持密钥轮换
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (selector, context) -> selector.select(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
5.4 自定义JWT声明
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claim("tenant_id", "t_1001");
context.getClaims().claim("roles", List.of("ROLE_USER"));
context.getClaims().claim("client_id", context.getRegisteredClient().getClientId());
context.getClaims().audience(List.of("order-service"));
context.getClaims().id(UUID.randomUUID().toString());
}
};
}
5.5 认证中心的工程化关注点
- 多实例部署,前面挂负载均衡
- JWKS响应可缓存,但要有合理刷新策略
- 登录、签发、失败次数纳入审计
- 针对
/oauth2/token 做限流与防暴力破解
- 数据库存储OAuth2授权信息时要做索引设计
六、Gateway:高并发入口的安全与性能设计
Spring Cloud Gateway适合做统一安全入口,不只是因为“它能路由”,而是因为它建立在Reactor Netty之上,适合I/O密集和大规模并发连接场景。
6.1 为什么网关适合做统一鉴权
如果20个微服务都各自解析Token、查权限、做黑名单校验,会出现:
统一放在Gateway处理的收益很直接:
- 后端服务只关注业务
- 鉴权规则集中治理
- 审计日志统一采集
- 非法请求尽早拦截,减少后端资源消耗
6.2 Gateway的生产级安全链路
推荐执行顺序如下:
- 基础黑白名单判断
- 限流与IP风险控制
- JWT提取与校验
- 黑名单 / 撤销检查
- 构建用户上下文并透传
- 路由到下游
- 响应头加固与审计输出
6.3 Resource Server配置
优先使用Spring Security原生Resource Server,而不是自己手写完整JWT验证逻辑。
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
这背后会自动处理:
- 基于
issuer-uri 拉取OIDC / OAuth2元数据
- 获取JWKS
- 校验签名、过期时间、发行者
6.4 自定义JWT转换器,把声明转成Spring Security权限
@Configuration
public class GatewaySecurityConfig {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthConverter) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/actuator/health", "/public/**").permitAll()
.pathMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_order.read")
.pathMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("SCOPE_order.write")
.anyExchange().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter)))
.build();
}
@Bean
Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthConverter() {
JwtGrantedAuthoritiesConverter scopeConverter = new JwtGrantedAuthoritiesConverter();
scopeConverter.setAuthorityPrefix("SCOPE_");
scopeConverter.setAuthoritiesClaimName("scope");
return jwt -> {
Collection<GrantedAuthority> authorities = new ArrayList<>(scopeConverter.convert(jwt));
List<String> roles = jwt.getClaimAsStringList("roles");
if (roles != null) {
roles.forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
}
return Mono.just(new JwtAuthenticationToken(jwt, authorities, jwt.getSubject()));
};
}
}
6.5 用户上下文透传,而不是把原始JWT到处乱发
在很多业务系统中,下游服务并不需要完整JWT,只需要最小身份上下文。可以在网关里把必要信息透传成内部Header。
@Component
public class UserContextRelayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return exchange.getPrincipal()
.cast(JwtAuthenticationToken.class)
.flatMap(auth -> {
Jwt jwt = auth.getToken();
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", jwt.getSubject())
.header("X-Tenant-Id", Objects.toString(jwt.getClaim("tenant_id"), ""))
.header("X-Client-Id", Objects.toString(jwt.getClaim("client_id"), ""))
.build();
return chain.filter(exchange.mutate().request(request).build());
})
.switchIfEmpty(chain.filter(exchange));
}
@Override
public int getOrder() {
return -50;
}
}
这里有一个重要原则:
- 对外:信任JWT
- 对内:信任网关透传的最小身份头,但前提是内网有mTLS和入口隔离
6.6 JWT黑名单与强制登出
JWT无状态不等于无法撤销。常见做法是将 jti 或Token Hash放入Redis黑名单。
@Service
public class TokenRevocationService {
private final StringRedisTemplate redisTemplate;
public TokenRevocationService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void revoke(String jti, Duration ttl) {
redisTemplate.opsForValue().set("security:blacklist:" + jti, "1", ttl);
}
public boolean isRevoked(String jti) {
Boolean exists = redisTemplate.hasKey("security:blacklist:" + jti);
return Boolean.TRUE.equals(exists);
}
}
配合Gateway过滤器:
@Component
public class JwtBlacklistFilter implements GlobalFilter, Ordered {
private final TokenRevocationService tokenRevocationService;
public JwtBlacklistFilter(TokenRevocationService tokenRevocationService) {
this.tokenRevocationService = tokenRevocationService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return exchange.getPrincipal()
.cast(JwtAuthenticationToken.class)
.flatMap(auth -> {
String jti = auth.getToken().getId();
if (jti != null && tokenRevocationService.isRevoked(jti)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
})
.switchIfEmpty(chain.filter(exchange));
}
@Override
public int getOrder() {
return -40;
}
}
6.7 高并发场景下网关的关键优化
这部分是很多文章最缺的内容。
1. 不要在网关里做阻塞调用
- 不要同步查数据库
- 不要同步调用权限中心
- 不要阻塞式读写Redis
网关是高并发入口,任何阻塞点都会被放大。
2. JWKS要缓存
如果每次都远程拉公钥,网关性能会非常差。应使用Spring Security默认缓存能力,并合理设置刷新策略。
3. 黑名单检查要轻量
高频接口下,每次Redis查询也有成本。可以做两级缓存:
4. 路由与鉴权规则配置化
不要把所有白名单、权限映射都硬编码在Java里。应支持:
5. 做好背压与连接池配置
spring:
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 5s
pool:
type: elastic
max-connections: 2000
acquire-timeout: 3000
routes:
- id: order-service
uri: lb://ORDER-SERVICE
predicates:
- Path=/api/orders/**
filters:
- TokenRelay=
6. 失败要快速失败,不要拖垮线程池
JWT校验失败、黑名单命中、mTLS握手失败都应该尽快返回,不要进入业务链路后再失败。
七、下游业务服务:从“重复鉴权”升级到“消费身份上下文”
网关做统一入口后,业务服务的职责应该从“完整鉴权”降级为“资源授权与数据保护”。
7.1 Resource Server依然建议保留最基本的验证能力
原因很简单:
- 防止服务被绕过网关后完全裸奔
- 便于关键服务独立暴露时仍有基础安全保障
- 方法级鉴权仍然依赖认证上下文
@Configuration
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/internal/orders/**").hasAuthority("SCOPE_order.read")
.requestMatchers(HttpMethod.POST, "/internal/orders/**").hasAuthority("SCOPE_order.write")
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
7.2 方法级鉴权
@RestController
@RequestMapping("/internal/orders")
public class OrderController {
@GetMapping("/{orderId}")
@PreAuthorize("hasAuthority('SCOPE_order.read')")
public OrderDetailVO detail(@PathVariable Long orderId) {
return OrderDetailVO.demo(orderId);
}
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_order.write') and hasRole('USER')")
public Long create(@RequestBody CreateOrderCommand command) {
return 10001L;
}
}
7.3 数据权限控制比接口权限更重要
很多系统只做到了“接口可访问”,却没有做到“只能访问自己的数据”。
例如订单查询接口即使通过了 SCOPE_order.read,仍然应继续校验:
- 当前租户是否匹配
- 当前用户是否属于订单所有者
- 当前角色是否有跨组织查询权限
也就是说:
- 接口鉴权解决“能不能调用这个API”
- 数据鉴权解决“能看到哪条数据”
八、服务间调用:Token Relay、Feign透传与身份收敛
微服务最容易出问题的一环,是调用链条上的身份信息如何传递。
8.1 三种常见方案
| 方案 |
说明 |
适用场景 |
| 原始用户Token透传 |
用户JWT一路传到底 |
需要链路保持最终用户身份 |
| 网关头透传 |
仅透传最小身份信息 |
大多数内部聚合服务 |
| 服务换发服务Token |
下游由服务身份访问 |
高安全、高隔离场景 |
生产环境里,推荐采用“按场景组合”:
- 普通查询链路:最小身份头透传
- 高风险写操作:服务换发受众受限的服务Token
- 审计敏感链路:保留原始调用主体与代理主体双身份
8.2 Feign透传示例
@Component
public class UserContextFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return;
}
HttpServletRequest request = attrs.getRequest();
copyHeader(request, template, "X-User-Id");
copyHeader(request, template, "X-Tenant-Id");
copyHeader(request, template, "X-Client-Id");
copyHeader(request, template, "X-Trace-Id");
}
private void copyHeader(HttpServletRequest request, RequestTemplate template, String name) {
String value = request.getHeader(name);
if (StringUtils.hasText(value)) {
template.header(name, value);
}
}
}
8.3 更推荐的异步友好方案:WebClient
在响应式项目中,优先使用 WebClient 和Reactor Context,而不是ThreadLocal。
@Bean
public WebClient orderWebClient(WebClient.Builder builder) {
return builder
.filter((request, next) -> ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.cast(JwtAuthenticationToken.class)
.defaultIfEmpty(null)
.flatMap(authentication -> {
ClientRequest.Builder mutated = ClientRequest.from(request);
if (authentication != null) {
mutated.header("X-User-Id", authentication.getName());
mutated.header("X-Client-Id", Objects.toString(authentication.getToken().getClaim("client_id"), ""));
}
return next.exchange(mutated.build());
}))
.build();
}
九、mTLS:真正建立“服务可信身份”
如果说OAuth2/JWT解决的是“令牌可信”,那么mTLS解决的是“连接双方可信”。
9.1 为什么内部网络也必须做mTLS
“内网天然可信”是很多系统的安全幻觉。真实情况是:
- K8s集群内部服务数量多,攻击面大
- 一台容器被攻陷后,攻击者可能横向访问其他服务
- 仅靠内网IP白名单难以证明调用方真实身份
mTLS的价值不是“再加一层HTTPS”,而是通过双向证书认证,让服务A能确认服务B的身份,服务B也能确认服务A的身份。
9.2 单向TLS与mTLS的本质区别
| 模式 |
服务端身份校验 |
客户端身份校验 |
| TLS |
是 |
否 |
| mTLS |
是 |
是 |
9.3 Spring Boot服务端mTLS配置
server:
port: 8443
ssl:
enabled: true
key-store: classpath:tls/server-keystore.p12
key-store-password: changeit
key-store-type: PKCS12
key-alias: order-service
trust-store: classpath:tls/server-truststore.p12
trust-store-password: changeit
trust-store-type: PKCS12
client-auth: need
9.4 Spring客户端mTLS配置
如果网关或服务通过 WebClient 调用下游,可以配置双向证书:
spring:
cloud:
gateway:
httpclient:
ssl:
key-store: classpath:tls/gateway-keystore.p12
key-store-password: changeit
key-store-type: PKCS12
trust-store: classpath:tls/gateway-truststore.p12
trust-store-password: changeit
trust-store-type: PKCS12
handshake-timeout: 10s
9.5 基于证书主体做服务授权
对核心服务来说,仅要求“有证书”还不够,更进一步要识别调用方服务身份。
@Bean
SecurityFilterChain mtlsSecurityFilterChain(HttpSecurity http) throws Exception {
http
.x509(x509 -> x509.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(username -> User
.withUsername(username)
.password("")
.authorities("ROLE_SERVICE")
.build()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/internal/payment/**").hasRole("SERVICE")
.anyRequest().authenticated());
return http.build();
}
9.6 证书治理的生产要点
- 使用企业CA或
cert-manager 自动签发
- 证书短周期轮换,不要一年一换靠人工操作
- 监控证书过期时间,提前告警
- 信任链、CRL、OCSP策略要可治理
- 不要把keystore明文提交仓库
9.7 服务网格模式
如果系统已经运行在Kubernetes且服务很多,mTLS可以交给服务网格处理:
- Istio / Linkerd负责Sidecar注入
- 自动证书分发与轮换
- PeerAuthentication / AuthorizationPolicy做细粒度治理
这种模式把“证书分发、握手、轮换”的复杂度从业务代码中拿了出去,尤其适合中大型集群。
十、案例:订单中心 + 支付中心的完整安全链路
下面用一个真实业务场景串起整套设计。
10.1 场景描述
用户在App下单后,需要调用订单服务,订单服务再调用支付服务完成预下单。
安全要求:
- 用户必须已登录
- 普通用户只能访问自己的订单
- 支付服务不允许任何非可信服务直接调用
- 被踢下线的用户Token要尽快失效
- 整条链路需要可审计
10.2 调用链设计
App
-> Gateway
-> Order Service
-> Payment Service
安全动作:
1. Gateway 校验用户 JWT
2. Gateway 检查 Redis 黑名单
3. Gateway 透传 X-User-Id / X-Tenant-Id / X-Trace-Id
4. Gateway 到 Order Service 走 mTLS
5. Order Service 做接口鉴权 + 数据权限校验
6. Order Service 到 Payment Service 走 mTLS
7. Payment Service 校验证书主体,只接受 order-service 调用
8. 全链路输出审计日志
10.3 订单服务的业务鉴权示例
@Service
public class OrderQueryService {
public OrderDetailVO queryOrder(Long orderId, JwtAuthenticationToken authentication) {
String currentUserId = authentication.getName();
String tenantId = authentication.getToken().getClaimAsString("tenant_id");
OrderEntity order = loadOrder(orderId);
if (!Objects.equals(order.getTenantId(), tenantId)) {
throw new AccessDeniedException("tenant mismatch");
}
if (!Objects.equals(order.getUserId(), currentUserId)) {
throw new AccessDeniedException("order does not belong to current user");
}
return toVO(order);
}
private OrderEntity loadOrder(Long orderId) {
return new OrderEntity(orderId, "u1001", "t1001");
}
private OrderDetailVO toVO(OrderEntity order) {
return new OrderDetailVO(order.getOrderId(), order.getUserId(), order.getTenantId());
}
}
record OrderEntity(Long orderId, String userId, String tenantId) {}
record OrderDetailVO(Long orderId, String userId, String tenantId) {}
这段代码体现的是一个常被忽略的重点:真正的授权通常发生在业务数据上,而不是只发生在网关。
10.4 支付服务的服务身份校验
支付服务对外不暴露,只允许订单服务通过mTLS调用:
- 证书主体必须是
CN=order-service
- 请求还必须带有受众为
payment-service 的服务令牌
这就形成了双重校验:
十一、工程化升级:高并发、可扩展、可运维
安全方案在生产环境成败的关键,往往不在“能不能认证成功”,而在“高并发时会不会把系统拖垮”。
11.1 高并发瓶颈通常出现在哪
- 网关同步做远程鉴权
- 每次请求都查Redis黑名单
- Auth Server过于中心化
- 证书握手与连接复用策略不合理
- 审计日志同步落库
11.2 可扩展设计建议
1. Auth Server水平扩展
- 多实例部署
- 无状态化
- 授权数据存MySQL/PostgreSQL
- 前置LB或Ingress
2. JWKS与公钥缓存
- Gateway / Resource Server缓存公钥
- 密钥轮换时通过
kid 平滑切换
- 新老公钥并存一段时间,避免全量失效
3. 黑名单分层缓存
- 一级本地缓存挡热点
- 二级Redis保证一致性
- 令牌TTL到期后黑名单键自动过期
4. 审计异步化
- 登录成功/失败、权限拒绝、Token撤销等事件投递MQ
- 审计系统异步消费,避免同步写数据库拖慢主链路
5. 权限模型配置化
- Scope、Role、路由白名单放配置中心
- 配置变更支持灰度发布
- 关键权限变更带审批与审计
11.3 推荐的安全指标
至少监控这些指标:
- JWT校验失败次数
- 黑名单命中次数
- Token签发QPS / RT
- JWKS拉取失败次数
- mTLS握手失败次数
- 403拒绝率
- 按客户端维度的请求量与错误率
- 证书剩余有效期
11.4 推荐的审计日志字段
traceId
userId
clientId
tenantId
jti
requestPath
httpMethod
decision
reason
sourceIp
targetService
costMs
11.5 灰度与应急预案
生产环境一定要提前设计降级与开关:
- 黑名单Redis故障时,是否走本地兜底策略
- Auth Server异常时,是否允许已缓存公钥继续验签
- 某个路由权限配置错误时,是否支持紧急回滚
- 证书批量到期时,是否有自动轮转和应急替换流程
十二、JWT、Gateway、mTLS的常见误区
12.1 误区一:网关验过JWT,下游就可以裸奔
错。只要服务可能被直连,就仍然需要最基本的Resource Server或网络隔离能力。
12.2 误区二:JWT里放的字段越多越方便
错。JWT应该最小化,只放认证和授权真正需要的内容。
12.3 误区三:内网调用不需要mTLS
错。零信任架构的核心就是不默认相信内网。
12.4 误区四:黑名单一查Redis就万事大吉
错。高并发下Redis本身也会成为开销来源,要考虑缓存和分层设计。
12.5 误区五:只做接口权限,不做数据权限
错。真正的数据泄露,往往就发生在“查到了不该看的那条数据”。
十三、生产落地清单
如果你要把这套方案真正落地,建议按下面的清单推进:
13.1 基础安全层
- Gateway统一接入
- HTTPS全面开启
- Auth Server独立部署
- JWT使用非对称签名
- JWKS对外发布
13.2 鉴权层
- Scope / Role / Audience模型设计
- Resource Server基础授权
- 方法级鉴权
- 数据级权限校验
13.3 服务身份层
- 内部服务启用mTLS
- 证书自动签发与轮换
- 核心服务基于证书主体做调用方授权
13.4 工程化层
- Redis黑名单
- 审计日志异步化
- 安全配置集中治理
- Prometheus / Grafana安全指标
- 应急开关与回滚能力
十四、结语
Spring Cloud微服务安全从来不是“某一个组件配好就结束”的问题,而是一套体系化工程。真正成熟的方案,通常具备以下特征:
- OAuth2负责标准授权框架
- JWT负责高性能无状态令牌
- Gateway负责统一入口与边界防护
- Resource Server负责资源和数据级授权
- mTLS负责服务身份与链路安全
- Redis、Vault、配置中心、监控审计共同构成工程化底座
如果只选一个结论,那就是:
生产级微服务安全,不是单点技术选型,而是“令牌可信 + 服务可信 + 链路可信 + 审计可信”的协同设计。
当你把这四件事都做对,系统才真正具备从Demo走向生产的安全能力。
如果你在实践微服务架构时遇到其他安全挑战,欢迎在云栈社区交流讨论。