1. 概述
在许多场景中,向JSON Web Token (JWT)访问令牌添加自定义声明至关重要。自定义声明允许我们在令牌载荷中包含额外信息。
本教程将学习如何在Spring Authorization Server中将资源所有者权限添加到JWT访问令牌中。
2. Spring Authorization Server
Spring Authorization Server是Spring生态系统中的新项目,旨在为Spring应用程序提供授权服务器支持。它通过熟悉且灵活的Spring编程模型,简化OAuth 2.0和OpenID Connect (OIDC)授权服务器的实现过程。
2.1 Maven依赖
首先在pom.xml中导入以下依赖:
- spring-boot-starter-web
- spring-boot-starter-security
- spring-boot-starter-test
- spring-security-oauth2-authorization-server
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.5.4</version>
</dependency>
或者添加更简洁的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.2.0</version>
</dependency>
2.2 项目设置
设置Spring Authorization Server用于签发访问令牌。为简化过程,我们将使用Spring Security OAuth Authorization Server应用程序。
假设使用GitHub上可用的授权服务器项目。
3. 向JWT访问令牌添加基本自定义声明
在基于Spring Security OAuth2的应用程序中,可通过自定义授权服务器中的令牌创建流程向JWT访问令牌添加自定义声明。这类声明可用于向JWT注入额外信息,供资源服务器或认证授权流程中的其他组件使用。
3.1 添加基本自定义声明
使用OAuth2TokenCustomizer
在DefaultSecurityConfig类中添加OAuth2TokenCustomizer Bean:
@Bean
@Profile("basic-claim")
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() {
return (context) -> {
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claims((claims) -> {
claims.put("claim-1", "value-1");
claims.put("claim-2", "value-2");
});
}
};
}
关键点说明:
- OAuth2TokenCustomizer接口用于自定义OAuth 2.0令牌
- 通过*context.getTokenType()*判断是否为访问令牌
- 使用*context.getClaims()*获取JWT载荷并添加自定义声明
- 示例添加了两个声明:
claim-1
和claim-2
3.2 测试自定义声明
使用client_credentials授权类型进行测试。首先在RegisteredClient对象中定义授权类型:
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("articles-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/articles-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope("articles.read")
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
创建测试用例:
@ActiveProfiles(value = "basic-claim")
public class CustomClaimsConfigurationTest {
private static final String ISSUER_URL = "http://localhost:";
private static final String USERNAME = "articles-client";
private static final String PASSWORD = "secret";
private static final String GRANT_TYPE = "client_credentials";
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int serverPort;
@Test
public void givenAccessToken_whenGetCustomClaim_thenSuccess() throws ParseException {
String url = ISSUER_URL + serverPort + "/oauth2/token";
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(USERNAME, PASSWORD);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", GRANT_TYPE);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
ResponseEntity<TokenDTO> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, TokenDTO.class);
SignedJWT signedJWT = SignedJWT.parse(response.getBody().getAccessToken());
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
Map<String, Object> claims = claimsSet.getClaims();
assertEquals("value-1", claims.get("claim-1"));
assertEquals("value-2", claims.get("claim-2"));
}
static class TokenDTO {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("token_type")
private String tokenType;
@JsonProperty("expires_in")
private String expiresIn;
@JsonProperty("scope")
private String scope;
public String getAccessToken() {
return accessToken;
}
}
}
测试流程:
- 构建OAuth2令牌接口URL
- 使用Basic认证和授权类型发起POST请求
- 解析返回的访问令牌
- 验证自定义声明值是否正确
✅ 测试确认令牌编码流程正常工作,声明按预期生成!
也可使用curl命令获取令牌:
curl --request POST \
--url http://localhost:9000/oauth2/token \
--header 'Authorization: Basic YXJ0aWNsZXMtY2xpZW50OnNlY3JldA==' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials
使用basic-claim
配置启动应用后,通过jwt.io解码令牌可见:
{
"sub": "articles-client",
"aud": "articles-client",
"nbf": 1704517985,
"scope": [
"articles.read",
"openid"
],
"iss": "http://auth-server:9000",
"exp": 1704518285,
"claim-1": "value-1",
"iat": 1704517985,
"claim-2": "value-2"
}
4. 向JWT访问令牌添加权限作为自定义声明
将权限作为自定义声明添加到JWT访问令牌,是Spring Boot应用安全管理的核心环节。Spring Security中的权限通常由GrantedAuthority对象表示,指示用户允许执行的操作或角色。通过将这些权限作为自定义声明包含在JWT中,可为资源服务器提供理解用户权限的标准化方式。
4.1 添加权限作为自定义声明
创建测试用户:
@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
现在将用户权限填充到访问令牌的自定义声明中:
@Bean
@Profile("authority-claim")
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(@Qualifier("users") UserDetailsService userDetailsService) {
return (context) -> {
UserDetails userDetails = userDetailsService.loadUserByUsername(context.getPrincipal().getName());
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
context.getClaims().claims(claims ->
claims.put("authorities", authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList())));
};
}
实现要点:
- 通过UserDetailsService获取当前用户的UserDetails
- 提取用户的GrantedAuthority集合
- 将权限列表添加到JWT的
authorities
声明中 - 使用Stream API将权限对象转换为字符串列表
4.2 测试权限声明
使用GitHub上的客户端-服务器项目进行测试。创建REST接口获取声明:
@GetMapping(value = "/claims")
public String getClaims(
@RegisteredOAuth2AuthorizedClient("articles-client-authorization-code") OAuth2AuthorizedClient authorizedClient
) throws ParseException {
SignedJWT signedJWT = SignedJWT.parse(authorizedClient.getAccessToken().getTokenValue());
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
Map<String, Object> claims = claimsSet.getClaims();
return claims.get("authorities").toString();
}
@RegisteredOAuth2AuthorizedClient注解指示方法需要指定ID的OAuth 2.0授权客户端(此处为articles-client-authorization-code
)。
使用authority-claim
配置启动应用后:
- 访问
http://127.0.0.1:8080/claims
- 自动重定向到
http://auth-server:9000/login
登录页 - 输入用户名
admin
和密码password
- 授权服务器重定向回原URL,显示权限列表
⚠️ 确保用户存在且配置正确,否则会触发认证失败
5. 结论
向JWT访问令牌添加自定义声明的能力,为定制令牌以满足应用特定需求提供了强大机制,同时增强了认证授权系统的安全性和功能性。
本文学习了在Spring Authorization Server中向JWT访问令牌添加自定义声明和用户权限的方法。
完整源代码请参考GitHub仓库。