自定义JWT信息后,OAuth2资源服务器取公钥时getDecodedDetails返回null
getDecodedDetails()返回null 我自定义了OAuth2授权服务器,为JWT添加了自定义信息,期望资源服务器通过授权服务器的/oauth/token_key端点动态获取验证公钥,但调用OAuth2AuthenticationDetails.getDecodedDetails()始终返回null。
现有代码结构
授权服务器相关代码
自定义Token增强器
public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oauth2AccessToken, OAuth2Authentication oauth2Authentication) { var customToken = new DefaultOAuth2AccessToken(oauth2AccessToken); Map<String, Object> customInfo = Map.of("generatedIn", "Year " + LocalDateTime.now().getYear()); customToken.setAdditionalInformation(customInfo); return customToken; } }
授权服务器配置类
@Configuration @EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter{ @Value("${password}") private String password; @Value("${privateKey}") private String privateKey; @Value("${alias}") private String alias; @Autowired private AuthenticationManager authenticationManager; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client") .secret("secret") .authorizedGrantTypes("password", "refresh_token") .scopes("read") .and() .withClient("resourceserver") .secret("resourceserversecret"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); var tokenEnhancers = List.of(new CustomTokenEnhancer(), jwtAccessTokenConverter()); tokenEnhancerChain.setTokenEnhancers(tokenEnhancers); endpoints.authenticationManager(authenticationManager) .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("isAuthenticated()"); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new JwtAccessTokenConverter(); KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory( new ClassPathResource(privateKey), password.toCharArray() ); converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias)); return converter; } }
授权服务器application.properties
password = somepassword privateKey =key.jks alias = somekey
资源服务器初始配置
资源服务器配置类
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { }
资源服务器application.properties
server.port = 9090 security.oauth2.resource.jwt.key-uri=http://localhost:8080/oauth/token_key security.oauth2.client.client-id=resourceserver security.oauth2.client.client-secret=resourceserversecret
受保护端点
@RestController public class HelloController { @GetMapping("/hello") public String hello(OAuth2Authentication authentication) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); return details.getDecodedDetails().toString(); } }
执行curl -H "Authorization:Bearer e1yhrjkkkfk....." http://localhost:9090/hello时,details.getDecodedDetails()返回null。
已知可行但不符合需求的方案
在资源服务器本地配置公钥,并自定义JwtAccessTokenConverter将JWT claims设置到authentication details时,代码能正常工作,但我不想在资源服务器存储公钥,希望通过端点动态获取。
可行但不满足需求的资源服务器配置
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter{ @Value("${publicKey}") private String publicKey; @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { var converter = new OtherAccessTokenConverter(); converter.setVerifierKey(publicKey); return converter; } }
自定义AccessTokenConverter
public class OtherAccessTokenConverter extends JwtAccessTokenConverter { @Override public OAuth2Authentication extractAuthentication(Map<String, ?> map) { var authentication = super.extractAuthentication(map); authentication.setDetails(map); return authentication; } }
解决方案
核心原理
当配置security.oauth2.resource.jwt.key-uri时,Spring Security会自动通过端点获取公钥,但默认的JwtAccessTokenConverter不会将JWT的claims映射到OAuth2AuthenticationDetails中。我们只需要自定义转换器保留claims映射逻辑,同时复用Spring的动态公钥获取机制即可。
步骤1:自定义JwtAccessTokenConverter
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter { @Override public OAuth2Authentication extractAuthentication(Map<String, ?> claims) { OAuth2Authentication authentication = super.extractAuthentication(claims); // 将JWT的所有claims设置到authentication的details中 authentication.setDetails(claims); return authentication; } }
步骤2:配置资源服务器使用自定义转换器
只需注册自定义的JwtAccessTokenConverter Bean,Spring会自动结合key-uri配置的端点获取公钥:
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { return new CustomJwtAccessTokenConverter(); } }
步骤3:验证端点逻辑
保持原端点代码不变,现在details.getDecodedDetails()会返回包含自定义信息的JWT claims:
@RestController public class HelloController { @GetMapping("/hello") public String hello(OAuth2Authentication authentication) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); Map<String, Object> decodedDetails = (Map<String, Object>) details.getDecodedDetails(); return "Generated in: " + decodedDetails.get("generatedIn"); } }
内容的提问来源于stack exchange,提问作者Chinedu

