前后端分离架构下,Token 认证几乎是现代 Web 开发的事实标准。但选哪种 Token——JWT(自包含)还是 Opaque(不透明)——从来不是一道轻松的选择题。Dante Cloud 4.0.6.3 的核心改进,就是让这两种 Token 的动态切换变得更丝滑,不再需要重启服务或硬编码认证方式。
两种 Token,两种哲学
先看本质区别:
| 维度 | JWT Token | Opaque Token |
|---|---|---|
| 结构 | 结构化,可解码读取 claims | 无规则随机字符串,本身不含信息 |
| 信息获取 | 本地解码即可,无需远程调用 | 每次必须向授权服务器 introspect |
| 性能 | 无网络开销,解码快 | 每次校验都有网络往返 |
| 安全性 | 一旦签发无法撤销(除非短过期+黑名单) | 随时可撤销,服务器端完全掌控 |
| 体积 | claims 多时 token 较长 | 固定长度,通常很短 |
一句话总结:JWT 用空间换时间,Opaque 用时间换安全。
生产环境里,这两种需求经常同时存在——内部高频调用偏好 JWT 的零网络开销,对外暴露的 API 更需要 Opaque 的即时撤销能力。问题来了:Spring Security OAuth2 的资源服务器配置,通常在启动时就绑定了 jwt() 或 opaqueToken(),想换?改配置、重启、上线。这在多租户或需要灰度切换的场景下,代价太高。
动态切换的难点在哪
Spring Authorization Server 的资源服务器认证过滤器链,默认是这样配置的:
http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
或者:
http.oauth2ResourceServer(oauth2 -> oauth2.opaqueToken(Customizer.withDefaults()));
两者共用同一个 BearerTokenAuthenticationFilter,但后续的认证处理器完全不同——JWT 走 JwtAuthenticationProvider,Opaque 走 OpaqueTokenAuthenticationProvider。Spring Security 的 DSL 不允许你同时注册两个,因为内部会用 AuthenticationManager 解析 token,而 AuthenticationManager 只认一个 provider 优先级。
所以"动态切换"不是简单加个 if-else,而是要在运行时根据请求上下文(比如请求头、租户标识、路由规则)决定走哪条认证链路,并且两条链路都得活着。
Dante Cloud 4.0.6.3 怎么做的
Dante Cloud 的方案核心思路:自定义 AuthenticationManager,根据 token 特征自动路由到对应的 Provider。
具体来说:
- Token 特征识别:JWT 有明确的结构特征——三段式 Base64,中间段解码后含
iss、exp等 claims;Opaque Token 则是随机字符串,无法解码出结构化数据。 - 双 Provider 共存:同时注册
JwtAuthenticationProvider和OpaqueTokenAuthenticationProvider,但不在 DSL 层面绑定,而是手动组装到自定义AuthenticationManager中。 - 路由决策:在
AuthenticationManager的authenticate()方法里,先尝试解析 token 结构,判断是 JWT 还是 Opaque,再委托给对应 Provider。
下面是一个简化实现,可以直接改造用于你的项目:
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenAuthenticationProvider;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector;
/**
* 动态路由 AuthenticationManager —— 根据 token 结构自动选择认证方式
*/
public class DynamicTokenAuthenticationManager implements AuthenticationManager {
private final AuthenticationProvider jwtProvider;
private final AuthenticationProvider opaqueProvider;
public DynamicTokenAuthenticationManager(JwtDecoder jwtDecoder,
OpaqueTokenIntrospector introspector) {
this.jwtProvider = new JwtAuthenticationProvider(jwtDecoder);
this.opaqueProvider = new OpaqueTokenAuthenticationProvider(introspector);
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
if (!(authentication instanceof BearerTokenAuthenticationToken)) {
return null;
}
String token = ((BearerTokenAuthenticationToken) authentication).getToken();
// 判断是否为 JWT:尝试按三段式拆分并解码中间段
if (looksLikeJwt(token)) {
try {
return jwtProvider.authenticate(authentication);
} catch (AuthenticationException jwtEx) {
// JWT 解码失败,可能是格式伪装的 Opaque token,降级尝试
return opaqueProvider.authenticate(authentication);
}
}
// 不像 JWT,直接走 Opaque 认证
return opaqueProvider.authenticate(authentication);
}
/**
* 简易 JWT 结构判断:三段 Base64,中间段解码后含 JSON 特征
*/
private boolean looksLikeJwt(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) return false;
try {
// 第二段是 payload,Base64 解码后应含 JSON 标记
String payload = new String(
java.util.Base64.getUrlDecoder().decode(parts[1]));
return payload.contains("\"") && payload.contains(":");
} catch (Exception e) {
return false;
}
}
}
注册到 Security 配置中:
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
JwtDecoder jwtDecoder,
OpaqueTokenIntrospector introspector) throws Exception {
AuthenticationManager dynamicAuthManager =
new DynamicTokenAuthenticationManager(jwtDecoder, introspector);
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(
// 对所有请求使用同一个动态 AuthenticationManager
request -> dynamicAuthManager));
return http.build();
}
}
关键点在于 authenticationManagerResolver——Spring Security 从 5.4 开始支持按请求动态选择 AuthenticationManager,这正是 Dante Cloud 利用的扩展点。上面的例子对所有请求用同一个 manager,你也可以按请求路径或租户头返回不同的 manager 实例,实现更细粒度的路由。
实际运行需要注意什么
1. Introspect 端点性能
一旦走 Opaque 路径,每次请求都要远程 introspect。高频场景下这是瓶颈。建议:
- 内部服务间调用强制走 JWT
- 只对外网暴露的网关层启用 Opaque
- introspect 结果做短时间缓存(30-60 秒),用
SpringCacheOpaqueTokenIntrospector包装
@Component
public class CachedOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate;
private final Cache cache; // 使用 Caffeine 或 Redis
public CachedOpaqueTokenIntrospector(OpaqueTokenIntrospector delegate, CacheManager cm) {
this.delegate = delegate;
this.cache = cm.getCache("tokenIntrospect");
}
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal cached = cache.get(token, () -> delegate.introspect(token));
return cached;
}
}
2. JWT 撤销的补丁
选了 JWT 不等于放弃撤销能力。常见补丁方案:
- 短过期时间(5-15 分钟)+ refresh token
- 维护一个 token 黑名单(Redis SET),在
JwtDecoder校验链中加一步黑名单检查
@Bean
public JwtDecoder jwtDecoder(RedisTemplate<String, String> redis) {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator(
List.of(
new JwtTimestampValidator(),
new JwtIssuerValidator(issuerUri),
new JwtClaimValidator("jti", jti -> {
// 黑名单检查
return !redis.hasKey("jwt:blacklist:" + jti);
})
)
));
return decoder;
}
3. 降级策略
DynamicTokenAuthenticationManager 里 JWT 失败后降级到 Opaque,这看起来稳妥,但要警惕:如果一个合法 JWT 因为密钥轮换暂时无法解码,降级到 introspect 可能返回"token 不存在"(因为授权服务器根本没签发过这个 opaque token)。所以降级路径更适合"token 格式不确定"的场景,而非"JWT 校验失败"的容错。生产中建议降级时直接返回 401,而不是静默尝试另一条链路。
什么时候该切换
| 场景 | 推荐 Token | 原因 |
|---|---|---|
| 内部微服务间调用 | JWT | 雑网络开销,延迟敏感 |
| 前端 SPA → 后端 API | JWT(短过期) | 浏览器每次请求都带 token,introspect 延迟不可接受 |
| 第三方合作方接入 | Opaque | 需要即时撤销、权限变更实时生效 |
| 管理后台 / 高权限操作 | Opaque | 安全优先,宁可多一次网络调用 |
| 移动端 App | JWT + refresh | 网络不稳定,减少依赖授权服务器可用性 |
Dante Cloud 4.0.6.3 的价值不是替你做选择,而是让选择不再是单选题——你可以按租户、按路由、按运行时策略灵活切换,而不用改配置重启服务。
上手清单
- 确认你的 Spring Security 版本 ≥ 5.4(支持
authenticationManagerResolver) - 同时配置
JwtDecoder和OpaqueTokenIntrospector两个 Bean - 实现
DynamicTokenAuthenticationManager或直接参考 Dante Cloud 源码中的实现 - 在
oauth2ResourceServerDSL 中用authenticationManagerResolver替代jwt()/opaqueToken() - 对 introspect 结果加缓存,对 JWT 加黑名单——两种 Token 的短板都要补
- 灰度发布时,先让内部服务走 JWT 验证稳定后,再逐步开放 Opaque 路径