如何生成包含一些自定义声明的 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
,其中包含tokenEnhancer
和tokenConverter
。按此顺序添加它们很重要。第一个使用附加声明(在我们的例子中是用户电子邮件)增强了访问令牌。第二个创建 JWT 令牌。 endpoints
设置有一个简单的JwtTokenStore
、我们的TokenEnhancerChain
和authenticationManager
。
注意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-rest
https://github.com/stephaneeybert/user-data
https://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 访问令牌?的主要内容,如果未能解决你的问题,请参考以下文章