如何生成包含一些自定义声明的 JWT 访问令牌?

Posted

技术标签:

【中文标题】如何生成包含一些自定义声明的 JWT 访问令牌?【英文标题】:How to generate a JWT access token with some custom claims in it? 【发布时间】:2019-06-06 13:34:27 【问题描述】:

我正在尝试让我的授权服务器生成一个 JWT 访问令牌,其中包含一些自定义声明。

授权服务器/auth/token端点返回的Bearer令牌如下所示:51aea31c-6b57-4c80-9d19-a72e15cb2bb7

我发现这个令牌有点短,无法作为 JWT 令牌并包含我的自定义声明...

并且在对资源服务器的后续请求中使用它时,它会抱怨错误:Cannot convert access token to JSON

我正在使用以下依赖项:

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.1.RELEASE</version>
    <relativePath/>
  </parent>

  <dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
  </dependency>

授权服务器是这样配置的:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
  endpoints
  .tokenServices(defaultTokenServices())
  .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
  .accessTokenConverter(jwtAccessTokenConverter())
  .userDetailsService(userDetailsService);

  endpoints
  .pathMapping("/oauth/token", RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN);

  TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
  endpoints
  .tokenStore(tokenStore())
  .tokenEnhancer(tokenEnhancerChain)
  .authenticationManager(authenticationManager);


@Bean
@Primary
public DefaultTokenServices defaultTokenServices() 
  DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
  defaultTokenServices.setTokenStore(tokenStore());
  defaultTokenServices.setSupportRefreshToken(true);
  return defaultTokenServices;


@Bean
public TokenStore tokenStore() 
  return new JwtTokenStore(jwtAccessTokenConverter());


@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() 
  JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
  jwtAccessTokenConverter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getSslKeystoreFilename()), jwtProperties.getSslKeystorePassword().toCharArray()).getKeyPair(jwtProperties.getSslKeyPair()));
return jwtAccessTokenConverter;


@Bean
public TokenEnhancer tokenEnhancer() 
  return new CustomTokenEnhancer();

它正在使用类:

class CustomTokenEnhancer implements TokenEnhancer 

  @Autowired
  private TokenAuthenticationService tokenAuthenticationService;

  // Add user information to the token
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) 
    User user = (User) authentication.getPrincipal();
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
    info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
    info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
    info.put("organization", authentication.getName());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
    return customAccessToken;
  


我也有课:

@Configuration
class CustomOauth2RequestFactory extends DefaultOAuth2RequestFactory 

  @Autowired
  private TokenStore tokenStore;

  @Autowired
  private UserDetailsService userDetailsService;

  public CustomOauth2RequestFactory(ClientDetailsService clientDetailsService) 
    super(clientDetailsService);
  

  @Override
  public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) 
    if (requestParameters.get("grant_type").equals("refresh_token")) 
      OAuth2Authentication authentication = tokenStore
          .readAuthenticationForRefreshToken(tokenStore.readRefreshToken(requestParameters.get("refresh_token")));
      SecurityContextHolder.getContext()
          .setAuthentication(new UsernamePasswordAuthenticationToken(authentication.getName(), null,
              userDetailsService.loadUserByUsername(authentication.getName()).getAuthorities()));
    
    return super.createTokenRequest(requestParameters, authenticatedClient);
  


更新:我还尝试了指定自定义声明的替代方法:

@Component
class CustomAccessTokenConverter extends JwtAccessTokenConverter 

    @Autowired
  private TokenAuthenticationService tokenAuthenticationService;

  @Override
  public OAuth2Authentication extractAuthentication(Map<String, ?> claims) 
    OAuth2Authentication authentication = super.extractAuthentication(claims);
    authentication.setDetails(claims);
    return authentication;
  

  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) 
    User user = (User) authentication.getPrincipal();
    Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
    info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
    info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
    info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
    info.put("organization", authentication.getName());
    DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
    customAccessToken.setAdditionalInformation(info);
    customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
    return super.enhance(customAccessToken, authentication);
  


它被称为:

endpoints
.tokenStore(tokenStore())
.tokenEnhancer(jwtAccessTokenConverter())
.accessTokenConverter(jwtAccessTokenConverter())

但它什么也没改变,错误仍然相同。

使用调试器运行,这两个增强器覆盖均未调用。

【问题讨论】:

【参考方案1】:

要使用 OAuth2、JWT 和额外声明构建 Spring Boot 服务器,我们应该:

1) 给项目添加依赖:

<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

2) 添加Web安全配置(发布AuthenticationManager bean - 将在下一步使用),例如:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    @Override
    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(username -> AuthUser.with()
                .username(username)
                .password("noop" + username)
                .email(username + "@mail.com")
                .authority(AuthUser.Role.values()[ThreadLocalRandom.current().nextInt(2)])
                .build()
        );
    

    @Bean
    public PasswordEncoder passwordEncoder() 
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    

这里实现了一个简单的UserDetailsService 用于测试目的。它适用于以下简单的“用户”对象和实现GrantedAuthority 接口的Role 枚举。 AuthUser 只有一个附加属性 email 将作为声明添加到 JWT 令牌中。

@Value
@EqualsAndHashCode(callSuper = false)
public class AuthUser extends User 

    private String email;

    @Builder(builderMethodName = "with")
    public AuthUser(final String username, final String password, @Singular final Collection<? extends GrantedAuthority> authorities, final String email) 
        super(username, password, authorities);
        this.email = email;
    

    public enum Role implements GrantedAuthority 
        USER, ADMIN;

        @Override
        public String getAuthority() 
            return this.name();
        
    

3) 配置授权服务器并启用资源服务器:

@Configuration
@EnableAuthorizationServer
@EnableResourceServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter 

    public static final String TOKEN_KEY = "abracadabra";

    private final AuthenticationManager authenticationManager;

    public AuthServerConfig(final AuthenticationManager authenticationManager) 
        this.authenticationManager = authenticationManager;
    

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsService) throws Exception 
        clientDetailsService.inMemory()
                .withClient("client")
                .secret("noop")
                .scopes("*")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(60 * 2) // 2 min
                .refreshTokenValiditySeconds(60 * 60); // 60 min
    

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) 
        TokenEnhancerChain chain = new TokenEnhancerChain();
        chain.setTokenEnhancers(List.of(tokenEnhancer(), tokenConverter()));
        endpoints
                .tokenStore(tokenStore())
                .reuseRefreshTokens(false)
                .tokenEnhancer(chain)
                .authenticationManager(authenticationManager);
    

    @Bean
    public TokenStore tokenStore() 
        return new JwtTokenStore(tokenConverter());
    

    @Bean
    public JwtAccessTokenConverter tokenConverter() 
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(TOKEN_KEY);
        converter.setAccessTokenConverter(authExtractor());
        return converter;
    

    private TokenEnhancer tokenEnhancer() 
        return (accessToken, authentication) -> 
                if (authentication != null && authentication.getPrincipal() instanceof AuthUser) 
                    AuthUser authUser = (AuthUser) authentication.getPrincipal();
                    Map<String, Object> additionalInfo = new HashMap<>();
                    additionalInfo.put("user_email", authUser.getEmail());
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
                
                return accessToken;
            ;
    

    @Bean
    public DefaultAccessTokenConverter authExtractor() 
        return new DefaultAccessTokenConverter() 
            @Override
            public OAuth2Authentication extractAuthentication(Map<String, ?> claims) 
                OAuth2Authentication authentication = super.extractAuthentication(claims);
                authentication.setDetails(claims);
                return authentication;
            
        ;
    

这里实现了一个简单的ClientDetailsService。它仅包含一个客户端,具有“客户端”名称、空白密码和授予类型“密码”和“刷新令牌”。它使我们有可能创建一个新的访问令牌并刷新它。 (要使用多种类型的客户端或在其他情况下,您必须实现 more complex,并且可能是持久的,ClientDetailsService 的变体。)

授权端点配置有TokenEnhancerChain,其中包含tokenEnhancertokenConverter。按此顺序添加它们很重要。第一个使用附加声明(在我们的例子中是用户电子邮件)增强了访问令牌。第二个创建 JWT 令牌。 endpoints 设置有一个简单的JwtTokenStore、我们的TokenEnhancerChainauthenticationManager

注意JwtTokenStore - 如果您决定实现商店的持久变体,您可以找到更多信息here。

这里的最后一件事是authExtractor,它使我们能够从传入请求的 JWT 令牌中提取声明。

然后一切都设置好了,我们可以请求我们的服务器获取访问令牌:

curl -i \
--user client: \
-H "Content-Type: application/x-www-form-urlencoded" \
-X POST \
-d "grant_type=password&username=user&password=user&scope=*" \
http://localhost:8080/oauth/token

回复:


    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImV4cCI6MTU0Nzc2NDIzOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiYzk1YzkzYTAtMThmOC00OGZjLWEzZGUtNWVmY2Y1YWIxMGE5IiwiY2xpZW50X2lkIjoiY2xpZW50In0.RWSGMC0w8tNafT28i2GLTnPnIiXfAlCdydEsNNZK-Lw",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjoidXNlckBtYWlsLmNvbSIsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyIqIl0sImF0aSI6ImM5NWM5M2EwLTE4ZjgtNDhmYy1hM2RlLTVlZmNmNWFiMTBhOSIsImV4cCI6MTU0Nzc2NzcxOCwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiZDRhNGU2ZjUtNDY2Mi00NGZkLWI0ZDgtZWE5OWRkMDJkYWI2IiwiY2xpZW50X2lkIjoiY2xpZW50In0.m7XvxwuPiTnPaQXAptLfi3CxN3imfQCVKyjmMCIPAVM",
    "expires_in": 119,
    "scope": "*",
    "user_email": "user@mail.com",
    "jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9"

如果我们在https://jwt.io/ 上解码此访问令牌,我们可以看到它包含user_email 声明:


  "user_email": "user@mail.com",
  "user_name": "user",
  "scope": [
    "*"
  ],
  "exp": 1547764238,
  "authorities": [
    "ADMIN"
  ],
  "jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9",
  "client_id": "client"

要从传入请求的 JWT 令牌中提取此类声明(和其他数据),我们可以使用以下方法:

@RestController
public class DemoController 

    @GetMapping("/demo")
    public Map demo(OAuth2Authentication auth) 

        var details = (OAuth2AuthenticationDetails) auth.getDetails();
        //noinspection unchecked
        var decodedDetails = (Map<String, Object>) details.getDecodedDetails();

        return Map.of(
                "name", decodedDetails.get("user_name"),
                "email", decodedDetails.get("user_email"),
                "roles", decodedDetails.get("authorities")
        );
    

我的工作演示:sb-jwt-oauth-demo

相关信息:

OAuth2 Autoconfig https://www.baeldung.com/spring-security-oauth https://projects.spring.io/spring-security-oauth/docs/oauth2.html

【讨论】:

不错的全面教程。除此之外,您是否在我的配置中看到任何可以解释我的问题的内容? @Stephane Oauth 是一个相当复杂的话题。正因为如此,要找出别人的代码不起作用的原因并不容易。因此,我提供了一个最小的工作示例,以便您可以将其与您的进行比较并找出问题的原因(或仅以此示例为基础)。需要注意的要点是tokenConverter(它创建JWT令牌)、tokenEnhancer(它向令牌添加声明)和组合它们的TokenEnhancerChain 是的,我试图查看我们的代码或您的代码之间的区别,但我没有实现,但目前,我仍然没有看到任何解决方案。 @Stephane 我认为您应该以我的代码为基础,然后使用您的代码逐步扩展它。这样你就可以确定问题的地方了。 @Cepr0 - 此示例添加到 JWT 有效负载和 json。这对我来说似乎不对。它应该只在有效载荷中?【参考方案2】:

如果您共享了一个示例项目,则更容易为您找到确切的修复方法。 取而代之的是,您是否在.tokenEnhancer(tokenEnhancerChain) 处设置了断点并触发了它?

我创建了一个超级简单的sample project,它显示了tokenEnhancer 是如何被调用的

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 

    @Bean
    public PasswordEncoder passwordEncoder()
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    

    @Bean //by exposing this bean, password grant becomes enabled
    public AuthenticationManager authenticationManager() throws Exception 
        return super.authenticationManager();
    

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService());
    

    @Bean
    public UserDetailsService userDetailsService() 
        return new InMemoryUserDetailsManager(
            builder()
                .username("user")
                .password("bcrypt$2a$10$C8c78G3SRJpy268vInPUFu.3lcNHG9SaNAPdSaIOy.1TJIio0cmTK") //123
                .roles("USER")
                .build(),
            builder()
                .username("admin")
                .password("bcrypt$2a$10$XvWhl0acx2D2hvpOPd/rPuPA48nQGxOFom1NqhxNN9ST1p9lla3bG") //password
                .roles("ADMIN")
                .build()
        );
    

    @EnableAuthorizationServer
    public static class Oauth2SecurityConfig extends AuthorizationServerConfigurerAdapter 
        private final PasswordEncoder passwordEncoder;
        private final AuthenticationManager authenticationManager;

        public Oauth2SecurityConfig(PasswordEncoder passwordEncoder,
                                    AuthenticationManager authenticationManager) 
            this.passwordEncoder = passwordEncoder;
            this.authenticationManager = authenticationManager;
        

        @Bean
        public TokenEnhancer tokenEnhancer() 
            return new CustomTokenEnhancer();
        

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
            endpoints
                .tokenEnhancer(tokenEnhancer())
                .authenticationManager(authenticationManager)
            ;

        

        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception 
            InMemoryClientDetailsService clientDetails = new InMemoryClientDetailsService();
            BaseClientDetails client = new BaseClientDetails(
                "testclient",
                null,
                "testscope,USER,ADMIN",
                "password",
                null
            );
            client.setClientSecret(passwordEncoder.encode("secret"));
            clientDetails.setClientDetailsStore(
                Collections.singletonMap(
                    client.getClientId(),
                    client
                )
            );
            clients.withClientDetails(clientDetails);
        

    


在这个示例中,还有一个单元测试

@Test
@DisplayName("perform a password grant")
void passwordGrant() throws Exception 
    mvc.perform(
        post("/oauth/token")
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
            .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
            .param("username", "admin")
            .param("password", "password")
            .param("grant_type", "password")
            .param("response_type", "token")
            .param("client_id", "testclient")
            .header("Authorization", "Basic "+ Base64.encodeBase64String("testclient:secret".getBytes()))
    )
        .andExpect(status().isOk())
        .andExpect(content().string(containsString("\"full_name\":\"Joe Schmoe\"")))
        .andExpect(content().string(containsString("\"email\":\"Joe@Schmoe.Com\"")))
    ;

随时查看sample project,看看它是否适合您。

【讨论】:

是的,调试器确实停在.tokenEnhancer(tokenEnhancerChain) 行,但它不会停在我的两个不同的enhancer 方法处。 我有完整的项目https://github.com/stephaneeybert/user-resthttps://github.com/stephaneeybert/user-datahttps://github.com/stephaneeybert/toolbox我明天试试你的示例项目。 我检查了第一个项目,mvn clean package 失败。缺少依赖项。几乎就像它期望依赖自己一样The POM for com.thalasoft:user-data:jar:0.0.1-SNAPSHOT is missing, no dependency information available我的建议是让别人更容易使用你的代码。获取示例,您将克隆它,导入它,所有项目都将在您的 IDE 中显示为可运行 您是否先构建了toolbox,然后构建了user-data 项目? 嗨斯蒂芬,我没有。您是否有理由不只有一个 repo,一个包含三个模块的父 POM,它们全部构建在一起?我可能仍然会这样做,但由于设置的复杂性,我不得不降低审查代码的优先级。

以上是关于如何生成包含一些自定义声明的 JWT 访问令牌?的主要内容,如果未能解决你的问题,请参考以下文章

如何根据用户输入在 Jwt Token 上添加自定义声明?

JWT 如何添加自定义声明和解码声明

向 ID 令牌 OpenID 添加自定义声明

如何从 .NET Core 6.0 中的 JWT 安全令牌中读取自定义声明值

Spring JWT - 添加自定义声明

如何向 azure b2c 访问令牌添加自定义声明?