1、概览 {#1概览}
在许多情况下,可以在 JWT Access Token 添加自定义 Claim,从而在 Token Payload 中包含更多信息。
本文将带你了解如何在 Spring Authorization Server 中为 JWT Access Token 添加资源所有者授权。
2、Spring Authorization Server {#2spring-authorization-server}
Spring Authorization Server 是 Spring 生态系统中的一个新项目,旨在为 Spring 应用提供授权服务器支持。它通过 Spring 编程模型简化 OAuth 2.0 和 OpenID Connect(OIDC) 授权服务器的实现。
2.1、Maven 依赖 {#21maven-依赖}
首先,在 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>
或者,可以在 pom.xml
文件中添加 spring-boot-starter-oauth2-authorization-server
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.2.0</version>
</dependency>
2.2、项目设置 {#22项目设置}
配置 Spring Authorization Server 来签发 Access Token。
为了简单起见,这里使用 "Spring Security Oauth 授权服务器" 应用。
假设我们使用的是 GitHub 上的 授权服务器项目。
3、为 JWT Access Token 添加基本的自定义 Claim {#3为-jwt-access-token-添加基本的自定义-claim}
在基于 Spring Security OAuth2 的应用中,可以通过自定义授权服务器中的令牌创建流程,为 JWT Access Token 添加自定义 Claim。这种类型的 Claim 可用于在 JWT 中注入附加信息,然后由资源服务器或身份认证和授权流程中的其他组件使用。
3.1、添加基本的自定义 Claim {#31添加基本的自定义-claim}
可以使用 OAuth2TokenCustomizer<JWTEncodingContext>
Bean 将自定义 Claim 添加到 Access Token 中。通过使用它,授权服务器签发的每个 Access Token 都将填充自定义 Claim。
在 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
接口是 Spring Security OAuth2 库的一部分,用于自定义 OAuth 2.0 Token。在本例中,它专门用于在编码过程中自定义 JWT Token。
传递给 jwtTokenCustomizer()
Bean 的 lambda 表达式定义了自定义逻辑。context
参数代表 Token 编码过程中的 JwtEncodingContext
。
首先,使用 context.getTokenType()
方法检查正在处理的 Token 是否是 Access Token。然后,使用 context.getClaims()
方法获取与正在构建的 JWT 相关的 Claim。最后,向 JWT 添加自定义 Claim。
在本例中,添加了两个 Claim(claim-1
和 claim-2
)和相应的值(value-1
和 value-2
)。
3.2、测试自定义 Claim {#32测试自定义-claim}
在测试中,使用 client_credentials
Grant Type。
首先,将 AuthorizationServerConfig
中的 client_credentials
Grant Type 定义为 RegisteredClient
对象中的 authorized Grant Type:
@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);
}
然后,在 CustomClaimsConfigurationTest
类中创建一个测试用例:
@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。
- 发送一个 POST 请求到 token 端点,并获取包含
TokenDTO
类的响应。这里,创建了一个 HTTP 请求实体,其中包含 Header(Basic Authentication)和参数(Grant Type)。 - 使用
SignedJWT
类从响应中解析 Access Token。此外,还从 JWT 中提取 Claim 并将其存储在Map<String, Object>
中。 - 使用 JUnit 断言证明 JWT 中的指定 Claim 具有预期值。
该测试确认了 Token 编码流程工作正常,并生成了预期的 Claim。
还可以使用 curl 命令获取 Access Token :
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
如上,凭证编码为包含 client ID 和 client secret 的 Base64
字符串,以单个冒号 :
分隔。
现在,可以使用 Profile basic-claim
运行 Spring Boot 应用。
获得 Access Token 后,在 jwt.io 对其进行解码,可以在 Token Body 中找到测试 claim:
{
"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"
}
可以看到,测试 Claim 的价值符合预期。
4、将 Authorities 作为自定义 Claim 添加到 JWT Access Token 中 {#4将-authorities-作为自定义-claim-添加到-jwt-access-token-中}
将 Authorities(权限)添加为自定义 Claim 到 JWT Access Token 中,通常是在 Spring Boot 应用中确保安全和管理访问的关键方面。权限通常由 Spring Security 中的 GrantedAuthority
对象表示,表示用户被允许执行的操作或角色。通过将这些权限作为自定义 Claim 包含在 JWT Access Token 中,为资源服务器提供了一种方便且标准化的方式来理解用户的权限。
4.1、将 Authorities 添加为自定义 Claim {#41将-authorities-添加为自定义-claim}
使用一个简单的内存用户配置,在 DefaultSecurityConfig
类中设置一组权限:
@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
创建一个用户名为 admin
、密码为 password
、角色为 USER
的用户。
在 Access Token 中用这些 Authorities 填充自定义 Claim:
@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())));
};
}
首先,定义一个实现 OAuth2TokenCustomizer<JwtEncodingContext>
接口的 lambda 函数。该函数会在编码过程中自定义 JWT。
然后,从注入的 UserDetailsService
中获取与当前 Principal(用户)相关的 UserDetails
对象。Principal 的 name 通常是用户名。
然后,检索与该用户相关联的 GrantedAuthority
对象集合。
最后,从 JwtEncodingContext
中检索 JWT Claim 并应用自定义。其中包括在 JWT 中添加名为 authorities
的自定义 Claim。此外,该 Claim 还包含从与用户关联的 GrantedAuthority
对象中获取的权限字符串列表。
4.2、测试 Authorities Claim {#42测试-authorities-claim}
配置了授权服务器后,进行测试。使用 GitHub 上的客户端-服务器项目。
创建一个 REST API 客户端,从 Access Token 中获取 Claim 列表:
@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
注解用于 Spring Boot Controller 方法中,表示该方法希望 OAuth 2.0 授权客户端以指定的 client ID 注册。在本例中,client ID 是 articles-client-authorization-code
。
使用 authority-claim
Profile 运行 Spring Boot 应用。
现在,打开浏览器并尝试访问 http://127.0.0.1:8080/claims
页面,会被自动重定向到 http://auth-server:9000/login
URL 下的 OAuth 服务器登录页面。
提供正确的用户名和密码后,授权服务器会将我们重定向到请求的 URL,即 Claim 列表。
5、总结 {#5总结}
总之,向 JWT Access Token 添加自定义 Claim 的功能提供了一种强大的机制,可根据应用的特定需求定制 Token,并增强身份认证和授权系统的整体安全性和功能。
本文介绍了如何在 Spring Authorization Server 中为 JWT Access Token 添加自定义 Claim 和用户授权(Authorities)。
Ref:https://www.baeldung.com/spring-jwt-access-tokens-authorities-custom-claims