【问题标题】:How to generate a JWT access token with some custom claims in it?如何生成包含一些自定义声明的 JWT 访问令牌?
【发布时间】: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())

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

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

【问题讨论】:

    标签: spring-boot spring-security jwt spring-security-oauth2


    【解决方案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

    相关信息:

    【讨论】:

    • 不错的全面教程。除此之外,您是否在我的配置中看到任何可以解释我的问题的内容?
    • @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,它们全部构建在一起?我可能仍然会这样做,但由于设置的复杂性,我不得不降低审查代码的优先级。
    猜你喜欢
    • 2020-01-23
    • 1970-01-01
    • 2019-06-24
    • 1970-01-01
    • 2019-11-20
    • 2021-12-27
    • 2019-11-29
    • 2017-03-23
    • 2021-03-19
    相关资源
    最近更新 更多