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

时间:2019-01-11 15:44:35

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

我正在尝试让我的授权服务器生成一个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())

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

与调试器一起运行时,这两个增强器替代都不会被调用。

2 个答案:

答案 0 :(得分:4)

如果您共享一个示例项目,则可以更轻松地找到适合您的修复程序。 取而代之的是,您是否在.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,看看它是否对您有用。

答案 1 :(得分:3)

要使用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来进行测试。它与以下简单的“用户”对象和实现Role接口的GrantedAuthority枚举一起使用。 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。它仅包含一个客户端,该客户端具有“客户端”名称,空白密码以及授予的类型“密码”和“ refresh_token”。它使我们可以创建一个新的访问令牌并刷新它。 (要与多种类型的客户端一起使用,或在其他情况下,您必须实现ClientDetailsService的{​​{3}}(可能是持久的变体)。)

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

JwtTokenStore的注意事项-我认为在实际情况下,必须使用商店的持久变体。更多信息是more complex

这里的最后一件事是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"
}

如果我们在here上解码此访问令牌,我们可以看到它包含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")
        );
    }
}

我的工作演示:https://jwt.io/

相关信息: