1. Overview
1.概述
Adding custom claims to JSON Web Token (JWT) access tokens can be crucial in many scenarios. Custom claims allow us to include additional information in the token payload.
在 JSON Web 令牌 (JWT) 中添加自定义声称在许多应用场景中至关重要。自定义声明允许我们在令牌有效载荷中包含更多信息。
In this tutorial, we’ll learn how to add resource owner authorities to a JWT access token in the Spring Authorization Server.
在本教程中,我们将学习如何在 Spring 授权服务器中为 JWT 访问令牌添加资源所有者授权。
2. Spring Authorization Server
2.Spring 授权服务器
The Spring Authorization Server is a new project in the Spring ecosystem designed to provide Authorization Server support to Spring applications. It aims to simplify the process of implementing OAuth 2.0 and OpenID Connect (OIDC) authorization servers using the familiar and flexible Spring programming model.
Spring 授权服务器是 Spring 生态系统中的一个新项目,旨在为 Spring 应用程序提供授权服务器支持。它旨在使用熟悉而灵活的 Spring 编程模型简化实施 OAuth 2.0 和 OpenID Connect (OIDC) 授权服务器的过程。
2.1. Maven Dependencies
2.1.Maven 依赖项
Let’s start by importing the spring-boot-starter-web, spring-boot-starter-security, spring-boot-starter-test, and spring-security-oauth2-authorization-server dependencies to the pom.xml:
首先,让我们将 spring-boot-starter-web、spring-boot-starter-security、spring-boot-starter-test 和 spring-security-oauth2-authorization-server依赖项导入到 pom.xml 中:
<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>
Alternatively, we can add the spring-boot-starter-oauth2-authorization-server dependency to our pom.xml file:
或者,我们可以将 spring-boot-starter-oauth2-authorization-server 依赖关系添加到 pom.xml 文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
<version>3.2.0</version>
</dependency>
2.2. Project Setup
2.2.项目设置
Let’s set up the Spring Authorization Server for issuing access tokens. To keep things simple, we’ll be using the Spring Security OAuth Authorization Server application.
让我们来设置 Spring 授权服务器,以签发访问令牌。为了简单起见,我们将使用Spring Security OAuth 授权服务器应用程序。
Let’s assume that we’re using the authorization server project available on GitHub.
3. Add Basic Custom Claims to JWT Access Tokens
3.为 JWT 访问令牌添加基本自定义声明
In a Spring Security OAuth2-based application, we can add custom claims to JWT access tokens by customizing the token creation process in the Authorization Server. This type of claim can be useful for injecting additional information into JWTs, which can then be used by resource servers or other components in the authentication and authorization flow.
在基于 Spring Security OAuth2 的应用程序中,我们可以通过在授权服务器中自定义令牌创建流程,为 JWT 访问令牌添加自定义声明。这种类型的声明对于向 JWT 中注入额外信息非常有用,资源服务器或其他组件可在身份验证和授权流程中使用这些信息。
3.1. Add Basic Custom Claims
3.1.添加基本自定义索赔
We can add our custom claims to an access token using the OAuth2TokenCustomizer<JWTEncodingContext> bean. By using it, every access token that is issued by the authorization server will have the custom claims populated.
我们可以使用 OAuth2TokenCustomizer<JWTEncodingContext> Bean 向访问令牌添加自定义声明。通过使用它,授权服务器签发的每个访问令牌都将填充自定义声明。
Let’s add the OAuth2TokenCustomizer bean in the DefaultSecurityConfig class:
让我们在 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");
});
}
};
}
The OAuth2TokenCustomizer interface is part of the Spring Security OAuth2 library and is used to customize OAuth 2.0 tokens. In this case, it specifically customizes JWT tokens during the encoding process.
OAuth2TokenCustomizer 接口是 Spring Security OAuth2 库的一部分,用于自定义 OAuth 2.0 令牌。在本例中,它专门用于在编码过程中自定义 JWT 令牌。
The lambda expression passed to the jwtTokenCustomizer() bean defines the customization logic. The context parameter represents the JwtEncodingContext during the token encoding process.
传递给 jwtTokenCustomizer() bean 的 lambda 表达式定义了自定义逻辑。context 参数表示标记编码过程中的 JwtEncodingContext。
First, we use the context.getTokenType() method to check whether the token being processed is an access token. Then, we obtain the claims associated with the JWT being constructed by using the context.getClaims() method. Finally, we add custom claims to the JWT.
首先,我们使用 context.getTokenType() 方法检查正在处理的令牌是否为访问令牌。然后,我们使用 context.getClaims() 方法获取与正在构建的 JWT 相关联的权利要求。最后,我们将向 JWT 添加自定义声称。
In this example, two claims (“claim-1” and “claim-2“) with corresponding values (“value-1” and “value-2“) are added.
在本例中,添加了两个索赔(”claim-1“和”claim-2“)和相应的值(”value-1“和”value-2“)。
3.2. Test the Custom Claims
3.2.测试自定义索赔
For testing, we are going to use the client_credentials grant type.
为进行测试,我们将使用 client_credentials 授权类型。
First, we’ll define the client_credentials grant type from AuthorizationServerConfig as an authorized grant type in the RegisteredClient object:
首先,我们将把 AuthorizationServerConfig 中的 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);
}
Then, let’s create a test case in the CustomClaimsConfigurationTest class:
然后,让我们在 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;
}
}
}
Let’s walk through the key parts of our test to understand what is going on:
让我们通过测试的关键部分来了解发生了什么:
- Start by constructing a URL for the OAuth2 token endpoint.
- Retrieve a response containing the TokenDTO class from a POST request to the token endpoint. Here, we create an HTTP request entity with headers (basic authentication) and parameters (grant type).
- Parse the Access Token from the response using the SignedJWT class. Also, we extract claims from the JWT and store them in the Map<String, Object>.
- Assert that specific claims in the JWT have expected values using JUnit assertions.
This test confirms that our token encoding process is working properly and our claims are being generated as expected. Awesome!
In addition, we can get the access token using the curl command:
该测试确认了我们的令牌编码过程工作正常,并按预期生成了请求。太棒了!
此外,我们还可以使用 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
Here, the credentials are encoded as a Base64 string of the client ID and client secret, delimited by a single colon “:”.
在这里,凭据被编码为一个 Base64 字符串,其中包含客户端 ID 和客户端秘密,以单个冒号”: “分隔。
Now, we can run our Spring Boot application with the profile basic-claim.
现在,我们可以使用配置文件 basic-claim. 运行 Spring Boot 应用程序。
If we obtain an access token and decode it using jwt.io, we find the test claims in the token’s body:
如果我们获得一个访问令牌,并使用 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"
}
As we can see, the value of the test claims is as expected.
我们可以看到,测试索赔的价值符合预期。
We’ll discuss adding authority as claims to access tokens in the following section.
我们将在下一节讨论将权限添加为访问令牌的权利要求。
4. Add Authorities as Custom Claims to JWT Access Tokens
4.在 JWT 访问令牌中添加授权作为自定义声明
Adding authorities as custom claims to JWT access tokens is often a crucial aspect of securing and managing access in a Spring Boot application. Authorities, typically represented by GrantedAuthority objects in Spring Security, indicate what actions or roles a user is allowed to perform. By including these authorities as custom claims in JWT access tokens, we provide a convenient and standardized way for resource servers to understand the user’s permissions.
在 JWT 访问令牌中添加授权作为自定义声明通常是在 Spring Boot 应用程序中保护和管理访问的一个重要方面。授权通常由 Spring Security 中的 GrantedAuthority 对象表示,它指示允许用户执行哪些操作或角色。通过将这些授权作为自定义声明纳入 JWT 访问令牌,我们为资源服务器了解用户权限提供了一种便捷和标准化的方式。
4.1. Add Authorities as Custom Claims
4.1.将授权添加为自定义索赔
First, we use a simple in-memory user configuration with a set of authorities in the DefaultSecurityConfig class:
首先,我们在 DefaultSecurityConfig 类中使用一个简单的内存用户配置和一组权限:
@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
A single user with the username “admin” password “password” and role “USER” is created.
创建一个用户名为”admin“、密码为”password“、角色为”USER“的用户。
Now, let’s populate a custom claim in the access token with those authorities:
现在,让我们在访问令牌中用这些权限填充自定义请求:
@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())));
};
}
First, we define a lambda function that implements the OAuth2TokenCustomizer<JwtEncodingContext> interface. This function customizes the JWT during the encoding process.
首先,我们定义了一个实现 OAuth2TokenCustomizer<JwtEncodingContext> 接口的 lambda 函数。该函数在编码过程中自定义 JWT。
Then, we retrieve the UserDetails object associated with the current principal (user) from the injected UserDetailsService. The principal’s name is typically the username.
然后,我们从注入的 UserDetailsService 中获取与当前委托人(用户)相关联的 UserDetails 对象。委托人的名称通常是用户名。
After that, we retrieve the collection of GrantedAuthority objects associated with the user.
然后,我们将检索与用户相关联的 GrantedAuthority 对象集合。
Finally, we retrieve the JWT claims from the JwtEncodingContext and apply customizations. It includes adding a custom claim named “authorities” to the JWT. Also, this claim contains a list of authority strings obtained from the GrantedAuthority objects associated with the user.
最后,我们从 JwtEncodingContext 中检索 JWT 索赔并应用自定义。其中包括在 JWT 中添加名为”authorities“的自定义声明。此外,该声明还包含从与用户关联的 GrantedAuthority 对象中获取的授权字符串列表。
4.2. Test the Authorities Claims
4.2.检验授权声明
Now that we’ve configured the authorization server, let’s test it. For that, we’ll use the client-server project available on GitHub.
既然我们已经配置了授权服务器,那就来测试一下吧。为此,我们将使用 GitHub 上的客户服务器项目 。
Let’s create a REST API client that will fetch the list of claims from the access token:
让我们创建一个 REST API 客户端,从访问令牌中获取索赔列表:
@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();
}
The @RegisteredOAuth2AuthorizedClient annotation is used in a Spring Boot controller method to indicate that the method expects an OAuth 2.0 authorized client to be registered with the specified client ID. In this case, the client ID is “articles-client-authorization-code“.
Spring Boot 控制器方法中使用了 @RegisteredOAuth2AuthorizedClient 注解,表示该方法希望 OAuth 2.0 授权客户端以指定的客户端 ID 注册。在本例中,客户端 ID 是”articles-client-authorization-code“。
Let’s run our Spring Boot application with the profile authority-claim.
让我们使用配置文件 authority-claim 运行 Spring Boot 应用程序。
Now when we go into the browser and try to access the http://127.0.0.1:8080/claims page, we’ll be automatically redirected to the OAuth server login page under http://auth-server:9000/login URL.
现在,当我们进入浏览器并尝试访问 http://127.0.0.1:8080/claims 页面时,我们将被自动重定向到 http://auth-server:9000/login URL 下的 OAuth 服务器登录页面。
After providing the proper username and password, the authorization server will redirect us back to the requested URL, the list of claims.
提供正确的用户名和密码后,授权服务器将把我们重定向到请求的 URL,即索赔列表。
5. Conclusion
5.结论
Overall, the ability to add custom claims to JWT access tokens provides a powerful mechanism for tailoring tokens to the specific needs of our application and enhancing the overall security and functionality of our authentication and authorization system.
总之,向 JWT 访问令牌添加自定义声明的功能提供了一种强大的机制,可根据我们应用程序的特定需求定制令牌,并增强我们身份验证和授权系统的整体安全性和功能。
In this article, we learned how to add custom claims and user authorities to a JWT access token in the Spring Authorization Server.
在本文中,我们学习了如何在 Spring 授权服务器中为 JWT 访问令牌添加自定义声明和用户授权。
As always, the full source code is available over on GitHub.
一如既往,您可以在 GitHub 上获取完整的源代码。