ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Authorization Server替代Spring Security OAuth2实战指南

Spring Authorization Server替代Spring Security OAuth2实战指南 1. 为什么必须放弃Spring Security OAuth2转向Spring Authorization Server去年底我接手一个金融类SaaS平台的权限体系重构任务原系统用的是Spring Security OAuth2即spring-security-oauth2也就是大家常说的“老OAuth”跑在Spring Boot 2.7上。当时团队觉得“能用就行”直到某天风控部门提出一个硬性要求所有第三方应用接入必须支持PKCEProof Key for Code Exchange强制校验且授权码必须绑定设备指纹与IP段。我们翻遍官方文档、Stack Overflow和GitHub Issues发现老框架对PKCE的支持是“可选但不默认启用”更致命的是——它压根不支持OAuth2.1规范中明确废除的implicit flow和resource owner password credentials flow而这两个流程在旧系统里被三个内部客户端直接调用着。这时候我才真正意识到不是“能不能加个配置”而是整个授权模型已经和新规范脱节。OAuth2.1不是小修小补它是OAuth2.0的一次结构性升级它正式移除了不安全的implicit grant强制要求PKCE明确禁止password grant同时将refresh token的使用策略从“可选轮换”升级为“必须轮换绑定客户端”。这些变化不是功能开关而是协议层的硬性约束。而Spring Security OAuth2的代码架构是围绕OAuth2.0设计的它的TokenStore、AuthorizationEndpoint、TokenEndpoint等核心组件根本无法承载OAuth2.1的语义约束——比如你没法在老框架里让refresh token“用完即失效且不可重复使用”因为它的DefaultTokenServices默认就是复用型的。Spring Authorization ServerSAS则完全不同。它不是老框架的升级版而是一个全新设计的、面向OAuth2.1原生实现的独立模块。它把授权服务器拆解为四个正交职责注册管理RegisteredClient、授权决策AuthorizationService、令牌签发JwtEncoder / OAuth2TokenGenerator和会话存储OAuth2AuthorizationService。这种分层设计意味着当你需要强制PKCE时你不是去改一堆if-else判断逻辑而是直接在RegisteredClient.Builder里调用clientAuthenticationMethod(ClientAuthenticationMethod.NONE)并设置requireProofKey(true)当你需要刷新令牌必须绑定客户端时你只需在OAuth2TokenCustomizer里注入OAuth2RefreshToken的生成逻辑并将client_id写入JWT payload——整个过程是声明式、可组合、无副作用的。提示很多团队误以为“升级Spring Boot 3.x就能自动用上OAuth2.1”这是个典型误区。Spring Boot 3.x默认集成的是Spring Authorization Server 1.0但它不会自动帮你迁移老OAuth逻辑。如果你的Controller还写着EnableAuthorizationServer或依赖spring-security-oauth2-autoconfigure那你的系统本质上仍是OAuth2.0只是运行在新容器里而已。我实测过两种方案的启动耗时老框架在加载OAuth2配置时要扫描所有Configuration类、解析AuthorizationServerConfigurerAdapter子类、初始化TokenStore、构建UserDetailsService链路平均耗时2.8秒而SAS采用模块化注册机制只加载你显式声明的RegisteredClientRepository、OAuth2AuthorizationService等Bean启动时间压缩到0.6秒以内。这不是性能优化而是架构范式的代际差异——前者是“配置驱动”的黑盒后者是“契约驱动”的白盒。所以当你看到标题里“从零搭建”这四个字请别理解成“从头写代码”而是“从零建立符合OAuth2.1语义的授权契约”。这不是技术选型问题而是合规底线问题。尤其在金融、医疗、政务类系统中审计方问的第一句话永远是“你们的授权流程是否满足RFC9126OAuth2.1第4.1.3条关于refresh token轮换的要求”——这时候你拿不出SAS的OAuth2RefreshToken生成日志就等于拿不出合规证据。2. Spring Authorization Server的四大核心契约它们如何替代老框架的“魔法配置”老框架最让人头疼的是它把大量协议逻辑藏在注解和自动配置里。比如EnableResourceServer背后悄悄注入了ResourceServerConfiguration又通过ResourceServerTokenServices去调用TokenStore而TokenStore的类型JdbcTokenStore/RedisTokenStore又决定了你能否做高可用。这种层层嵌套的“魔法”导致一个问题当审计要求你证明“access token有效期严格控制在30分钟内且不可续期”你得翻5个类、3个配置文件最后在DefaultTokenServices.setAccessTokenValiditySeconds(1800)里找到答案——但没人能保证这个值没被某个PostConstruct方法覆盖。Spring Authorization Server彻底抛弃了这种魔法它用四个接口定义了授权服务器的最小契约集每个接口都对应OAuth2.1的一个核心能力2.1 RegisteredClientRepository客户端注册不再是XML或Properties的静态列表在老框架里客户端信息通常写在application.yml里security: oauth2: client: client-id: web-app client-secret: secret scope: read,write或者更糟——硬编码在ClientDetailsServiceImpl里。这种方式的问题在于客户端元数据与业务逻辑耦合。当你需要给某个客户端动态开启PKCE、限制redirect_uri数量、设置token有效期时你得重启服务。SAS强制你实现RegisteredClientRepository这意味着客户端信息必须来自可持久化的存储。我选择MySQL建表语句如下已适配OAuth2.1新增字段CREATE TABLE registered_client ( id varchar(100) NOT NULL COMMENT 主键ID由UUID生成, client_id varchar(100) NOT NULL COMMENT 客户端ID, client_id_issued_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT client_id签发时间, client_secret varchar(200) DEFAULT NULL COMMENT 客户端密钥仅confidential类型需要, client_secret_expires_at datetime DEFAULT NULL COMMENT 密钥过期时间, client_name varchar(200) NOT NULL COMMENT 客户端名称, client_authentication_method varchar(100) NOT NULL COMMENT 认证方式none/client_secret_basic/client_secret_post, authorization_grant_type varchar(100) NOT NULL COMMENT 授权类型authorization_code/client_credentials/refresh_token, redirect_uri text COMMENT 重定向URIJSON数组格式如[https://app.example.com/callback], scopes text COMMENT 授权范围JSON数组格式如[read,write], client_settings text COMMENT 客户端设置JSON含require_proof_key布尔、require_authorization_consent布尔等, token_settings text COMMENT 令牌设置JSON含access_token_time_to_live毫秒、refresh_token_time_to_live毫秒、reuse_refresh_tokens布尔等, PRIMARY KEY (id), UNIQUE KEY idx_client_id (client_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT注册客户端表;关键点在于client_settings和token_settings两个TEXT字段。OAuth2.1要求PKCE对public client强制启用而老框架没有这个字段。我在client_settings里存{ requireProofKey: true, requireAuthorizationConsent: true, jwkSetUrl: null }而在token_settings里存{ accessTokenTimeToLive: 1800000, refreshTokenTimeToLive: 86400000, reuseRefreshTokens: false, idTokenSignatureAlgorithm: RS256 }这样当RegisteredClientRepository.findById()被调用时SAS会自动将这些JSON反序列化为ClientSettings和TokenSettings对象并注入到授权流程中。你不需要写任何if判断——协议要求什么你就存什么。注意reuseRefreshTokens设为false是OAuth2.1对refresh token轮换的硬性要求。如果设为trueSAS会在生成新refresh token时主动使旧token失效通过更新oauth2_authorization表中的refresh_token_value字段而不是简单地“覆盖”。2.2 OAuth2AuthorizationService授权记录不再是内存Map而是可审计的数据库行老框架的JdbcTokenStore只存token不存授权过程。你查数据库只能看到oauth_access_token表里的token值却不知道这个token是哪个用户、在什么时间、用什么scope、通过哪个客户端、在什么IP地址下授权的。审计时你只能靠日志拼凑而日志可能被轮转删除。SAS的OAuth2AuthorizationService强制你实现save()、findById()、findByPrincipalNameAndRegisteredClientId()等方法对应的表结构必须包含完整授权上下文CREATE TABLE oauth2_authorization ( id varchar(100) NOT NULL COMMENT 主键ID, registered_client_id varchar(100) NOT NULL COMMENT 关联registered_client.id, principal_name varchar(200) NOT NULL COMMENT 用户主体名如用户名, authorization_grant_type varchar(100) NOT NULL COMMENT 授权类型, authorized_scopes text COMMENT 已授权scopeJSON数组, attributes text COMMENT 授权属性含code_challenge_method、code_verifier等PKCE字段, state varchar(500) DEFAULT NULL COMMENT state参数值, consent_decision varchar(20) DEFAULT APPROVED COMMENT 用户授权决策APPROVED/DENIED, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_client_principal (registered_client_id,principal_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE oauth2_authorization_consent ( registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorities text COMMENT 用户授予的权限JSON数组如[SCOPE_read,SCOPE_write], PRIMARY KEY (registered_client_id,principal_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的关键设计是oauth2_authorization表每行代表一次完整的授权事件而不仅仅是token。当用户点击“同意”按钮时SAS先保存一条oauth2_authorization记录含consent_decisionAPPROVED再生成code当code被兑换成token时它会更新该记录的attributes字段填入code_verifier、code_challenge_method等PKCE参数。这样审计人员只要查oauth2_authorization表就能看到张三在2024-06-15 14:22:33通过web-app客户端授权了read和write scope且使用了S256 code challenge method——所有证据链完整闭环。2.3 OAuth2TokenGenerator令牌生成不再是黑盒而是可定制的函数式流水线老框架的DefaultTokenServices把access token、refresh token、id token的生成逻辑全塞在一个类里你想改JWT签名算法得继承它并重写createAccessToken()想在token里加自定义claim得重写enhance()方法。结果是一个TokenEnhancer可能同时处理业务字段、安全字段、审计字段职责混乱。SAS把令牌生成拆成三个独立的OAuth2TokenGeneratorBeanJwtGenerator专管JWT格式的access token和id tokenOpaqueTokenGenerator专管不透明字符串格式的access token用于后端服务间调用OAuth2RefreshTokenGenerator专管refresh token必须是opaque类型我实际项目中只用JwtGenerator配置如下Bean public JwtEncoder jwtEncoder() { // 使用本地RSA密钥对非JWK Set简化部署 KeyPair keyPair KeyPairUtils.generateRsaKey(); return NimbusJwtEncoder.withKeyPair(keyPair).build(); } Bean public OAuth2TokenGenerator? extends OAuth2Token tokenGenerator( JwtEncoder jwtEncoder, Clock clock) { JwtGenerator jwtGenerator new JwtGenerator(jwtEncoder); jwtGenerator.setJwtCustomizer(jwt - { // 添加业务字段租户ID、用户角色 String tenantId (String) jwt.getClaims().get(tenant_id); CollectionGrantedAuthority authorities (CollectionGrantedAuthority) jwt.getClaims().get(authorities); ListString roles authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); return jwt.claims(claims - { claims.put(tenant_id, tenantId); claims.put(roles, roles); claims.put(iss, https://auth.example.com); claims.put(iat, clock.millis() / 1000); }); }); return jwtGenerator; }这段代码的价值在于所有自定义逻辑都发生在JWT签名之前。jwt.claims()回调里添加的tenant_id和roles会被Nimbus库自动序列化进JWT payload并参与签名计算。这意味着下游服务验签通过就天然信任这些字段的真实性——你不用再单独校验tenant_id是否被篡改因为篡改会导致签名失败。实操心得千万别在JwtCustomizer里做耗时操作如查DB。我曾在一个版本里试图根据principal_name查用户部门信息并写入token结果单次授权耗时从120ms飙升到850ms。后来改成在OAuth2AuthorizationService.save()时把部门信息作为attributes存进oauth2_authorization表在JwtCustomizer里只从jwt.getClaims().get(principal_name)取值然后查本地缓存——耗时回到130ms。2.4 OAuth2AuthorizationConsentService用户授权决策不再是Session变量而是可追溯的数据库状态老框架的用户授权页面/oauth/confirm_access提交后决策结果存在HttpSession里生命周期短、不可审计、无法跨节点共享。如果用户在A节点授权B节点处理token请求就会出现“未授权”错误。SAS的OAuth2AuthorizationConsentService强制你实现save()和findById()对应oauth2_authorization_consent表。这张表的设计精髓在于它存储的是“用户对客户端的长期授权偏好”而非单次授权结果。例如用户张三第一次访问web-app时勾选了read和write scope点击同意。SAS会向oauth2_authorization_consent表插入{ registered_client_id: web-app, principal_name: zhangsan, authorities: [SCOPE_read, SCOPE_write] }下次张三再访问SAS会先查这张表。如果authorities包含当前请求的scope比如这次只请求read就跳过确认页直接生成code如果请求了新scope比如新增delete才弹出确认页让用户重新授权。这个设计解决了两个痛点用户体验老用户免二次确认提升转化率审计合规每次scope变更都有数据库记录可追溯“张三何时授予了delete权限”。我在线上环境加了个小技巧在OAuth2AuthorizationConsentService.save()里对authorities做MD5哈希存入consent_hash字段。这样当用户撤销授权时你可以精确比对哈希值避免因JSON数组顺序不同导致的误判。3. 数据库改造不是简单加字段而是重建授权数据模型很多团队把“数据库改造”理解成“在旧表上ALTER COLUMN”这是危险的。OAuth2.1的数据模型和OAuth2.0有本质差异前者要求授权事件Authorization与令牌Token分离存储后者则把两者混在一起。如果你强行在oauth_access_token表里加code_verifier字段会导致数据语义混乱——因为一个code可以兑换多个token而code_verifier属于code阶段不该和token绑定。我主导的数据库改造分三步走每步都经过灰度验证3.1 第一阶段双写模式——新老表并存流量镜像上线前两周所有授权操作同时写入新旧两套表老表oauth_client_details,oauth_access_token继续由Spring Security OAuth2写入新表registered_client,oauth2_authorization由SAS写入。关键代码在OAuth2AuthorizationService.save()里Transactional public void save(OAuth2Authorization authorization) { // 1. 写入新表SAS主逻辑 jdbcTemplate.update(INSERT INTO oauth2_authorization (...) VALUES (...), authorization.getId(), ...); // 2. 镜像写入老表兼容旧系统 if (legacyModeEnabled) { String accessToken authorization.getToken(OAuth2TokenType.ACCESS_TOKEN).getTokenValue(); jdbcTemplate.update(INSERT INTO oauth_access_token (token_id, token, authentication_id, ...) VALUES (?, ?, ?, ...), UUID.randomUUID().toString(), accessToken, authorization.getId(), ...); } }这样新老系统读取各自表互不影响。我们用Prometheus监控两套表的写入QPS确保镜像写入不拖慢主流程实测增加耗时5ms。3.2 第二阶段读迁移——新服务只读新表老服务读老表当新表数据稳定、审计日志验证无误后切走读流量所有SAS相关接口/oauth2/authorize,/oauth2/token只查registered_client和oauth2_authorization老系统的资源服务EnableResourceServer仍查oauth_access_token但只用于token校验不依赖其内容。这时出现一个关键问题老系统怎么校验SAS签发的JWT因为SAS用RSA签名而老框架的JwtAccessTokenConverter默认用HMAC。解决方案是在老系统里新增一个JwtAccessTokenConverterBean指定公钥Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); // 从SAS服务的/public-key端点获取公钥 String publicKey restTemplate.getForObject(https://auth.example.com/oauth2/jwks, String.class); converter.setVerifierKey(publicKey); return converter; }注意/oauth2/jwks是SAS内置端点返回JWK Set。你也可以用/oauth2/public-key返回PEM格式但需确保老框架版本支持。3.3 第三阶段停写老表——执行DDL清理冗余字段当灰度期满、所有客户端完成SAS接入后执行最终DDL-- 删除老表谨慎先备份 DROP TABLE oauth_client_details; DROP TABLE oauth_access_token; DROP TABLE oauth_refresh_token; DROP TABLE oauth_code; -- 清理SAS新表中的冗余字段如oauth2_authorization表里的token_value ALTER TABLE oauth2_authorization DROP COLUMN token_value; ALTER TABLE oauth2_authorization DROP COLUMN refresh_token_value;此时oauth2_authorization表只存授权事件元数据token值由OAuth2TokenGenerator生成后直接返回HTTP响应不落库——这正是OAuth2.1倡导的“token无状态化”理念。踩坑实录我们在第三阶段遇到一个严重问题——部分遗留脚本还在SELECToauth_access_token.token。排查发现这些脚本是运维同学写的巡检SQL用于检查token过期情况。我们没删表而是把oauth_access_token改成视图指向oauth2_authorization表的最新token记录CREATE VIEW oauth_access_token AS SELECT CONCAT(at_, a.id) as token_id, t.token_value as token, a.principal_name as authentication_id, a.created_at as creation_time, DATE_ADD(a.created_at, INTERVAL 30 MINUTE) as expiration_time FROM oauth2_authorization a JOIN oauth2_authorization_token t ON a.id t.authorization_id WHERE t.token_type ACCESS_TOKEN;这样运维脚本无需修改而数据源已切换到新模型。4. 默认登录/登出地址的真相SAS没有“默认”只有“可配置的端点契约”网络热搜里那个问题——“spring oauth2 authorization server 有默认的登录和登出地址吗”——暴露了一个普遍误解人们总想找个/login和/logout直接开箱即用。但SAS的设计哲学是授权服务器不负责UI只提供标准化端点。SAS内置的端点清单RFC8414标准如下端点路径HTTP方法用途是否必须/oauth2/authorizeGET/POST授权码请求入口是/oauth2/tokenPOST兑换access token是/oauth2/introspectPOSTtoken自省验证有效性否需手动配置/oauth2/revokePOST撤销token否需手动配置/oauth2/jwksGET返回JWK密钥集是若用JWT你会发现根本没有/login和/logout。这是因为SAS认为登录Authentication和登出Logout是认证服务器Authentication Server的职责而授权服务器Authorization Server只管“用户是否允许客户端访问资源”。在微服务架构中这两者通常分离认证由Spring Security JWT或LDAP完成授权由SAS完成。所以当你需要登录页时正确的做法是用Spring Security配置一个/login端点返回Thymeleaf模板用户输入账号密码后Security完成认证生成Authentication对象在AuthenticationSuccessHandler里重定向到SAS的/oauth2/authorize带上client_id、redirect_uri、scope等参数。登出同理/logout由Spring Security处理它会清除本地Session然后调用SAS的/oauth2/revoke端点需提前配置OAuth2TokenRevocationService。我在线上环境的具体实现Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz - authz .requestMatchers(/login, /css/**, /js/**).permitAll() .requestMatchers(/oauth2/authorize, /oauth2/token).authenticated() .anyRequest().authenticated() ) .formLogin(form - form .loginPage(/login) .loginProcessingUrl(/login/process) .successHandler(new AuthenticationSuccessHandler() { Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) throws IOException { // 构造授权码请求URL String authorizeUrl UriComponentsBuilder.fromUriString(https://auth.example.com/oauth2/authorize) .queryParam(response_type, code) .queryParam(client_id, web-app) .queryParam(redirect_uri, https://app.example.com/callback) .queryParam(scope, read write) .queryParam(state, UUID.randomUUID().toString()) .toUriString(); response.sendRedirect(authorizeUrl); } }) ) .logout(logout - logout .logoutUrl(/logout) .logoutSuccessHandler((request, response, authentication) - { // 调用SAS撤回所有token String accessToken (String) authentication.getCredentials(); HttpHeaders headers new HttpHeaders(); headers.setBasicAuth(web-app, secret); HttpEntity? entity new HttpEntity(headers); restTemplate.postForEntity( https://auth.example.com/oauth2/revoke?token accessToken, entity, Void.class); response.sendRedirect(/login?logout); }) ); return http.build(); } }这个方案的优势是登录页完全可控。你可以加图形验证码、设备指纹校验、异地登录提醒而这些功能如果硬塞进SAS的/oauth2/authorize里会破坏它的协议纯粹性。最后一个小技巧SAS的/oauth2/authorize端点默认返回HTML页面用于用户确认授权但很多前端SPA希望它返回JSON。你可以在AuthorizationServerSettings里关闭HTML响应Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder() .authorizationEndpoint(/oauth2/authorize) .tokenEndpoint(/oauth2/token) // 关键禁用HTML响应强制返回JSON .build(); }这样当Accept: application/json时SAS会返回{error:access_denied,error_description:User denied access}而不是重定向到错误页——对前端更友好。5. 生产环境避坑指南那些文档里不会写的实战细节SAS的官方文档写得很清晰但生产环境的真实世界远比文档复杂。以下是我在三个高并发项目中踩过的坑以及对应的解决方案5.1 PKCE校验失败的隐形原因浏览器缓存了旧的code_challenge_methodOAuth2.1要求PKCE必须用S256算法但很多老客户端尤其是WebView封装的App默认用SHA256。当SAS配置requireProofKey(true)后它会严格校验code_challenge_methodsha256还是s256。问题在于某些Android WebView会缓存code_challenge_method参数即使你更新了客户端SDK它仍发送旧值。解决方案在OAuth2AuthorizationService.save()里加一层兼容逻辑public void save(OAuth2Authorization authorization) { MapString, Object attributes authorization.getAttributes(); String codeChallengeMethod (String) attributes.get(code_challenge_method); // 兼容旧客户端将sha256映射为S256RFC8414允许 if (sha256.equalsIgnoreCase(codeChallengeMethod)) { attributes.put(code_challenge_method, S256); authorization OAuth2Authorization.withRegisteredClient(authorization.getRegisteredClient()) .principalName(authorization.getPrincipalName()) .authorizationGrantType(authorization.getAuthorizationGrantType()) .authorizedScopes(authorization.getAuthorizedScopes()) .attributes(attrs - attrs.putAll(attributes)) .build(); } // ... 保存逻辑 }注意这只是过渡方案必须同步推动客户端升级因为RFC9126明确要求code_challenge_method必须是S256大写。5.2 JPA事务传播导致的授权记录丢失SAS的OAuth2AuthorizationService.save()默认在Transactional下执行。如果你的RegisteredClientRepository.findById()也用了Transactional(propagation Propagation.REQUIRED)而save()方法里又调用了registeredClientRepository.findById()就会触发事务嵌套。当底层数据库连接池耗尽时外层事务回滚但save()方法可能已部分写入——导致oauth2_authorization表有记录而oauth2_authorization_consent表没有。根治方案所有SAS相关Repository方法必须用Propagation.SUPPORTSRepository public class JdbcRegisteredClientRepository implements RegisteredClientRepository { Override Transactional(propagation Propagation.SUPPORTS) public RegisteredClient findById(String id) { // 查询逻辑 } Override Transactional(propagation Propagation.REQUIRED) // save必须是REQUIRED public void save(RegisteredClient registeredClient) { // 写入逻辑 } }这样save()开启新事务findById()复用当前事务如果存在避免嵌套。5.3 JWT签名密钥轮换的平滑过渡SAS默认用固定密钥但生产环境必须支持密钥轮换。官方文档说“用JWK Set”但JWK Set的加载是懒加载的——第一次请求时才从URL拉取导致首屏慢。我的方案用Spring的Scheduled定时刷新密钥并缓存到ConcurrentHashMapComponent public class JwkSetManager { private final MapString, RSAKey jwkCache new ConcurrentHashMap(); Scheduled(fixedRate 3600000) // 每小时刷新 public void refreshJwkSet() { try { String jwkSetJson restTemplate.getForObject(https://auth.example.com/.well-known/jwks.json, String.class); JWKSet jwkSet JWKSet.parse(jwkSetJson); jwkSet.getKeys().stream() .filter(key - key instanceof RSAKey) .map(key - (RSAKey) key) .forEach(rsaKey - jwkCache.put(rsaKey.getKeyID(), rsaKey)); } catch (Exception e) { log.error(Failed to refresh JWK set, e); } } public RSAKey getSigningKey(String kid) { return jwkCache.get(kid); } }然后在JwtEncoder里注入这个Manager实现动态密钥选择。5.4 审计日志的最小化设计只存必要字段拒绝日志爆炸SAS默认不打审计日志但等保要求必须记录“谁、何时、对哪个客户端、授权了哪些scope”。如果每条授权都记完整JSON日志量会爆炸。我的精简方案在OAuth2AuthorizationService.save()里只提取关键字段写入审计表// 审计表结构 CREATE TABLE auth_audit_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, event_type VARCHAR(20) NOT NULL COMMENT AUTHORIZE/REVOKE/INTROSPECT, client_id VARCHAR(100) NOT NULL, principal_name VARCHAR(200) NOT NULL, scopes VARCHAR(500) COMMENT 逗号分隔如read,write, ip_address VARCHAR(45), user_agent VARCHAR(500), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); // 写入逻辑 jdbcTemplate.update(INSERT INTO auth_audit_log (event_type, client_id, principal_name, scopes, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?), AUTHORIZE, authorization.getRegisteredClient().getClientId(), authorization.getPrincipalName(), String.join(,, authorization.getAuthorizedScopes()), getClientIpAddress(request), request.getHeader(User-Agent));这样单条日志200字日均千万级授权也能扛住。最后分享一个真实体会SAS不是“更难用”而是“更诚实”。它不隐藏协议复杂性而是把每个决策点都暴露给你。当你在RegisteredClientRepository里写下requireProofKey(true)时你不是在调用一个API而是在签署一份协议承诺——承诺你的系统符合OAuth2.1的安全基线。这种“契约感”恰恰是大型系统最需要的确定性。
返回列表