我正在尝试让我的授权服务器生成一个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())
但是它什么也没改变,错误仍然相同。
与调试器一起运行,这两个增强器替代都不被调用。
要使用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”。它使我们可以创建一个新的访问令牌并刷新它。 (要使用多种类型的客户端或在其他情况下,您必须实现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": "[email protected]",
"jti": "c95c93a0-18f8-48fc-a3de-5efcf5ab10a9"
}
如果我们在https://jwt.io/上解码此访问令牌,我们可以看到它包含user_email
声明:
{
"user_email": "[email protected]",
"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
相关信息:
如果您共享一个示例项目,则可以更轻松地为您找到确切的修复程序。取而代之的是,您是否在.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\":\"[email protected]\"")))
;
}
随时检查sample project,看看它是否对您有用。