从 JWT 令牌获取 UserInfo 信息并在 Spring Boot OAuth2 自动配置上仅使用 JWK 验证

Posted

技术标签:

【中文标题】从 JWT 令牌获取 UserInfo 信息并在 Spring Boot OAuth2 自动配置上仅使用 JWK 验证【英文标题】:Getting UserInfo information from a JWT token and using only JWK validation on Spring Boot OAuth2 autoconfiguration 【发布时间】:2019-12-15 09:54:39 【问题描述】:

我正在创建一个 RESTful 服务,该服务使用 OAuth2 机制和外部 Keycloak 用户身份验证服务器 (UAA) 对所有传入请求进行身份验证。

该服务充当资源服务器,使用 @EnableResourceServer 并具有以下配置:

@Configuration
@EnableResourceServer
@Order(0)
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    private final ResourceServerTokenServices resourceServerTokenServices;

    @Autowired
    public SecurityConfig(ResourceServerTokenServices resourceServerTokenServices) 
        this.resourceServerTokenServices = resourceServerTokenServices;
    

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.authorizeRequests()
                .antMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated().and().addFilterAfter(oAuth2AuthenticationProcessingFilter(), AbstractPreAuthenticatedProcessingFilter.class);
    

    private OAuth2AuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter() 
        OAuth2AuthenticationProcessingFilter oAuth2AuthenticationProcessingFilter = new OAuth2AuthenticationProcessingFilter();
        oAuth2AuthenticationProcessingFilter.setAuthenticationManager(oauthAuthenticationManager());
        oAuth2AuthenticationProcessingFilter.setStateless(false);

        return oAuth2AuthenticationProcessingFilter;
    

    private AuthenticationManager oauthAuthenticationManager() 
        OAuth2AuthenticationManager oAuth2AuthenticationManager = new OAuth2AuthenticationManager();
        oAuth2AuthenticationManager.setResourceId("country-microservice");
        oAuth2AuthenticationManager.setTokenServices(resourceServerTokenServices);
        oAuth2AuthenticationManager.setClientDetailsService(null);

        return oAuth2AuthenticationManager;
    


我还使用以下依赖项来包含 Spring Security OAuth2:

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
</dependencies>

用户在 UAA 上进行身份验证以获得 JWT 令牌,他们必须使用该令牌来调用我正在创建的服务。 JWT 令牌本身包含用户信息:

 

  ...

  "realm_access": 
    "roles": [
      "user"
    ]
  ,
  "scope": "profile email",
  "email_verified": true,
  "name": "Test Derp",
  "preferred_username": "user1",
  "given_name": "Test",
  "family_name": "Derp",
  "email": "test@test.com"

为避免向 UAA 发出另一个请求,该服务使用 JWK 来验证传入的令牌。我正在使用 Keycloak 的证书端点设置 security.oauth2.resource.jwk.key-set-uri 属性:

security.oauth2.resource.jwk.key-set-uri=http://localhost:9080/auth/realms/dev/protocol/openid-connect/certs

问题在于 Spring 没有获取在 JWT 令牌上找到的用户信息并将其填充到 Authentication 对象中。

我有以下控制器来返回主体信息:

@RestController
@RequestMapping(path = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public class UserController 

    @GetMapping
    public Object getUser(Authentication authentication) 
        if (authentication != null) 
            return authentication.getPrincipal();
        

        return null;
    


Authentication 对象在 getUser 函数中以 null 传递(使用 JWK 验证)。

我尝试使用以下配置自定义JWKTokenStoreJWTAccessTokenConverter,但没有成功:

@Configuration
public class JwkStoreConfig 

    private final ResourceServerProperties resource;

    @Autowired
    public JwkStoreConfig(ResourceServerProperties resource) 
        this.resource = resource;
    

    @Bean
    @Primary
    public JwtAccessTokenConverter accessTokenConverter() 
        return new JwtAccessTokenConverter();
    

    @Bean
    public DefaultTokenServices jwkTokenServices(TokenStore jwkTokenStore) 
        DefaultTokenServices services = new DefaultTokenServices();
        services.setTokenStore(jwkTokenStore);
        return services;
    

    @Bean
    public TokenStore jwkTokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) 
        JwkTokenStore jwkTokenStore = new JwkTokenStore(this.resource.getJwk().getKeySetUri(), jwtAccessTokenConverter);
        return jwkTokenStore;
    


到目前为止,唯一有效的解决方案是忘记使用 JWK 并更改服务以使用 Keycloak 的 UserInfo 来验证传入令牌,使用 security.oauth2.resource.user-info-uri 属性并删除 JWK URI 属性:

security.oauth2.resource.user-info-uri=http://localhost:9080/auth/realms/dev/protocol/openid-connect/userinfo

使用此属性集,Authentication 对象将与用户信息一起传递给控制器​​,但这会使服务在每次需要验证传入令牌时都请求 UAA。

有什么想法吗?

谢谢。 问候。

【问题讨论】:

【参考方案1】:

这方面的文档是 here,虽然对我来说并不完全清楚,至少在我的设置中它不能按预期工作。

我的理解是这两个属性应该一起使用:

security.oauth2.resource.jwt.key-uri: http://localhost:9191/auth/realms/master
security.oauth2.resource.jwk.key-set-uri: http://localhost:9191/auth/realms/master/protocol/openid-connect/certs

不过,这对我来说失败了,日志消息有点不合时宜:

Authentication request failed: error="invalid_token", error_description="Cannot convert access token to JSON"

稍微调试一下 Spring Security 代码,看起来如果由于某种原因它无法在内部解析/创建用于验证签名的公钥,则会出现上述错误。

这对我有用...

如果您将属性 security.oauth2.resource.jwt.key-value 设置为实际的公钥,那么它会验证并从 JWT 解压缩用户信息,而无需回调(或配置)用户信息端点。不利的一面是,必须将公钥复制到代码中,并在更改时随时更新。

请注意,此属性的值必须在开头包含 -----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----

例如

security.oauth2.resource.jwt.key-value: "-----BEGIN PUBLIC KEY-----\nMIIB......\n-----END PUBLIC KEY-----"

我没有尝试过,但我很确定这也适用于 YAML 中的实际换行符:

security.oauth2.resource.jwt.key-value: |
  -----BEGIN PUBLIC KEY-----
  ...
  -----END PUBLIC KEY-----  

认为第一组属性不适用于我的设置的原因是因为从 key-uri 返回的密钥没有这些开始和结束段以及 Spring Security期望他们在那里。

这可能不会直接回答您的问题,但希望仍然有帮助。 Spring 安全日志记录非常稀少,我发现这些属性对于它们改变事物行为的程度非常复杂,没有太多有用的日志记录信息来指示正在发生的事情,即使在 TRACE 级别也是如此。

【讨论】:

以上是关于从 JWT 令牌获取 UserInfo 信息并在 Spring Boot OAuth2 自动配置上仅使用 JWK 验证的主要内容,如果未能解决你的问题,请参考以下文章

如何在spring security oauth2授权服务器中通过jwt令牌获取用户信息?

Spring OAuth/JWT 从访问令牌中获取额外信息

无法访问 Azure 上的 OpenId UserInfo 端点(AADSTS90010:JWT 令牌不能与 UserInfo 端点一起使用)

如何在 Angular 中从服务器获取带有 jwt 令牌的响应标头

从 keycloak jwt 令牌获取 ID(不是 clientId)

如何从 jwt 令牌获取确切的用户以及用户在 laravel 中的确切帖子