使用Spring OAuth2授权服务器1.1.0(Spring Boot 3.1.0)自定义OAuth2令牌。

huangapple go评论53阅读模式
英文:

Customize OAuth2 Token with Spring OAuth2 Authorization Server 1.1.0 (Spring Boot 3.1.0)

问题

我已经创建了一个Spring OAuth2授权服务器,使用了Spring Boot 3.1.0和Spring OAuth2授权服务器1.1.0。它工作得很好,我可以使用授权授予类型接收到一个访问令牌,看起来像下面这样:

{
    "access_token": "eyJraWQiOiJiZGM5MTVlMy01ZjliLTQzYWItOTU5Yi0zYzJmNTE3YTI0NDgiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDEiLCJuYmYiOjE2ODU1NDEwOTYsInNjb3BlIjpbInJlYWQiLCJvcGVuaWQiXSwiaXNzIjoiaHR0cDovL2F1dGgtc2VydmVyOjgwMDAiLCJleHAiOjE2ODU1NDEzOTYsImlhdCI6MTY4NTU0MTA5Nn0.JtQSGR4LlYIZU0NV16ht4-LU0fRDUs9yD33NHFY1nItEc3NUs6vbV8SeSPGbmdMpxUMcr1_Xd1FpSkKrWbPPBZC10hortVrA1k550wGLVrZcknsc7sW10G718dLlJvL7qJGj4sqrqLIP1vVR8Ft3M7CdoT34Z7z6-JcHKRgmnXOP-tyvdWhRtn_OVb1o_29pTumJQ9GPSHU_Z6miOrDvOgUllWUwypw9Cg6aJJyl403P0Cl2wYye4HvP0gfosq6qbNy5OTZ4yiG0HrxrsYvNux9JIvYGbxMUhp9pNF84d3NOzvc24aDxD_VkerBV3zlfrOgLOtSstRdwLxaJ7dc-4Q",
    "refresh_token": "VIAvsVUef8ljKBBPvv9gi1-DU48U77h8lZ0OBh0HO57fyGxJTppazUMOlAfnAsCrvMGc5XFVlX1Lii04YltVjF4dk-vrJWHEplhKtIehxxZXEX3HmTqaSL63pYQq9cGr",
    "scope": "read openid",
    "id_token": "eyJraWQiOiJiZDCustomize9PQQ2BgLczf9DJdW3cSh0wo6u",
    "token_type": "Bearer",
    "expires_in": 299
}

如您所见,这是一个包含最少信息的令牌,例如用户角色、用户电子邮件等。为此,我希望自定义令牌的创建。

为了实现这一目标,我尝试使用OAuth2TokenCustomizer类。但由于某种原因,它没有起作用。

我创建了以下Bean:

@Bean
public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
    return context -> {
        var userName = context.getPrincipal().getName();
        StringBuilder roles = new StringBuilder();
        var authorities = context.getPrincipal().getAuthorities();
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            context.getClaims().claims(claims -> {
                claims.put("userName", userName);
                claims.put("testVal", "This is a test string");
                for (var auth : authorities) {
                    if (roles.isEmpty()) {
                        roles.append(auth.getAuthority());
                    } else {
                        roles.append(" ").append(auth.getAuthority());
                    }
                }
                claims.put("roles", roles);
            });
        }
    };
}

并且在OAuth2TokenGenerator Bean中使用了这个Bean,如下所示:

@Bean
public OAuth2TokenGenerator<OAuth2Token> tokenGenerator() {
    JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
    JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
    OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
    accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer());
    OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
    return new DelegatingOAuth2TokenGenerator(
            jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}

但这并不起作用。我尝试了一些其他选项,如此处给出的选项https://github.com/spring-projects/spring-authorization-server/issues/925(2022年10月25日jgrandja的答案),但当我这样做时,我的应用程序无法自动装配OAuth2AuthorizationService Bean。

我不确定我在这里漏掉了什么。如果有人可以帮助,我将非常感激。我的两个配置类文件如下:

AuthorizationServerConfig.java

// 请参考原文提供的完整代码

WebSecurityConfig.java

// 请参考原文提供的完整代码

如果需要更多信息,请告诉我。提前致谢。


<details>
<summary>英文:</summary>
I have created a Spring OAuth2 Authorization Server using Spring boot 3.1.0 and Spring OAuth2 Authorization Server 1.1.0. That works fine and I receive an access token using Authorization Grant Type that looks something like below:
{
&quot;access_token&quot;: &quot;eyJraWQiOiJiZGM5MTVlMy01ZjliLTQzYWItOTU5Yi0zYzJmNTE3YTI0NDgiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDEiLCJuYmYiOjE2ODU1NDEwOTYsInNjb3BlIjpbInJlYWQiLCJvcGVuaWQiXSwiaXNzIjoiaHR0cDovL2F1dGgtc2VydmVyOjgwMDAiLCJleHAiOjE2ODU1NDEzOTYsImlhdCI6MTY4NTU0MTA5Nn0.JtQSGR4LlYIZU0NV16ht4-LU0fRDUs9yD33NHFY1nItEc3NUs6vbV8SeSPGbmdMpxUMcr1_Xd1FpSkKrWbPPBZC10hortVrA1k550wGLVrZcknsc7sW10G718dLlJvL7qJGj4sqrqLIP1vVR8Ft3M7CdoT34Z7z6-JcHKRgmnXOP-tyvdWhRtn_OVb1o_29pTumJQ9GPSHU_Z6miOrDvOgUllWUwypw9Cg6aJJyl403P0Cl2wYye4HvP0gfosq6qbNy5OTZ4yiG0HrxrsYvNux9JIvYGbxMUhp9pNF84d3NOzvc24aDxD_VkerBV3zlfrOgLOtSstRdwLxaJ7dc-4Q&quot;,
&quot;refresh_token&quot;: &quot;VIAvsVUef8ljKBBPvv9gi1-DU48U77h8lZ0OBh0HO57fyGxJTppazUMOlAfnAsCrvMGc5XFVlX1Lii04YltVjF4dk-vrJWHEplhKtIehxxZXEX3HmTqaSL63pYQq9cGr&quot;,
&quot;scope&quot;: &quot;read openid&quot;,
&quot;id_token&quot;: &quot;eyJraWQiOiJiZGM5MTVlMy01ZjliLTQzYWItOTU5Yi0zYzJmNTE3YTI0NDgiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDEiLCJhenAiOiJjbGllbnQxIiwiYXV0aF90aW1lIjoxNjg1NTQxMDY3LCJpc3MiOiJodHRwOi8vYXV0aC1zZXJ2ZXI6ODAwMCIsImV4cCI6MTY4NTU0Mjg5NiwiaWF0IjoxNjg1NTQxMDk2LCJzaWQiOiJrUllCUjFBNWJPbDZJR0FGYnZTNHJZSDc0dG5ncHhCemh2eWNCV0ZLR1dnIn0.LIjMH6ONDGSBE2pO3sDUsPmDsstJhvQb6NPRrDZO8TAClyNpwMMRkCmPociU2Jv_rjQq8Y-zXrj016WchkGgCeakCyItzCvpTmqUDjM9tHwpG7FWuDC_GBsFstLwHqussVOG23vvy2KyNi6h8EMtbIR_aqFbDfzvknXQkAK-8Hl2ICqPfbzDkcZeomvV9J07ScqCL6iMkWw3g8ISJfvmWtiymuQ3tGa_9qJXA-JcgcZJhYGpSCbd052AxerZTMpJC4tN1afJDCfJy0HrgnChdX1wp_r9QXLKbNb1SEGRd8IUWzOLRHkOiJKqlgFx-AzuQ7sVINYjHHE1A8yHSGqGSQ&quot;,
&quot;token_type&quot;: &quot;Bearer&quot;,
&quot;expires_in&quot;: 299
}
Now, As you see that it is a token with very minimum info. For example, user roles, user email, whatsoever. For that purpose I want to customise the creation of the token. 
To achieve this, I am trying to use [OAuth2TokenCustomizer][1] class. But for some reason it is not working. 
I have created the following Bean 
@Bean
public OAuth2TokenCustomizer&lt;OAuth2TokenClaimsContext&gt; accessTokenCustomizer() {
return context -&gt; {
var userName = context.getPrincipal().getName();
StringBuilder roles = new StringBuilder();
var authorities =  context.getPrincipal().getAuthorities();
if(OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claims(claims -&gt; {
claims.put(&quot;userName&quot;, userName);
claims.put(&quot;testVal&quot;, &quot;This is a test string&quot;);
for (var auth:authorities) {
if(roles.isEmpty()){
roles.append(auth.getAuthority());
} else {
roles.append(&quot; &quot;).append(auth.getAuthority());
}
}
claims.put(&quot;roles&quot;, roles);
});
} };
}
And this Bean is used in [OAuth2TokenGenerator][2] Bean like below 
@Bean
public OAuth2TokenGenerator&lt;OAuth2Token&gt; tokenGenerator() {
JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer());
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
This is not working. I tried few other options like the one given here https://github.com/spring-projects/spring-authorization-server/issues/925 (The answer by jgrandja on Oct 25, 2022), but when I do that, my app is unable to Autowire OAuth2AuthorizationService Bean  
I am not sure what am I missing here. If anyone could help, I would really be grateful. My two configuration class files are below:
**AuthorizationServerConfig.java**
package com.auth.config;
import java.util.UUID;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.smarc.auth.config.keys.KeyManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class AuthorizationServerConfig
{
private final KeyManager keyManager;
public AuthorizationServerConfig(KeyManager keyManager)
{
this.keyManager = keyManager;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain customSecurityFilterChainOAuth(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenGenerator(tokenGenerator());
return http.formLogin(Customizer.withDefaults()).build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId(&quot;client1&quot;)
.clientSecret(&quot;{noop}myClientSecretValue&quot;)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri(&quot;http://127.0.0.1:8080/login/oauth2/code/users-client-oidc&quot;)
.redirectUri(&quot;http://127.0.0.1:8080/authorized&quot;)
.scope(OidcScopes.OPENID)
.scope(&quot;read&quot;)
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public ClientSettings clientSettings() {
return ClientSettings.builder()
.requireAuthorizationConsent(false)
.requireProofKey(false)
.build();
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer(&quot;http://localhost:8080&quot;).build();
}
@Bean
public JWKSource&lt;SecurityContext&gt; jwkSource() {
JWKSet set = new JWKSet(keyManager.rsaKey());
return (j, jc) -&gt; j.select(set);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource&lt;SecurityContext&gt; jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public OAuth2TokenGenerator&lt;OAuth2Token&gt; tokenGenerator() {
JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer());
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
@Bean
public OAuth2TokenCustomizer&lt;OAuth2TokenClaimsContext&gt; accessTokenCustomizer() {
return context -&gt; {
var userName = context.getPrincipal().getName();
StringBuilder roles = new StringBuilder();
var authorities =  context.getPrincipal().getAuthorities();
if(OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
context.getClaims().claims(claims -&gt; {
claims.put(&quot;userName&quot;, userName);
claims.put(&quot;testVal&quot;, &quot;This is a test string&quot;);
for (var auth:authorities) {
if(roles.isEmpty()){
roles.append(auth.getAuthority());
} else {
roles.append(&quot; &quot;).append(auth.getAuthority());
}
}
claims.put(&quot;roles&quot;, roles);
});
} };
}
}
**WebSecurityConfig.java**
package com.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig
{
@Bean
public SecurityFilterChain customSecurityFilterChain(HttpSecurity http) throws Exception{
return http
.authorizeHttpRequests(authorizeRequests -&gt; authorizeRequests.anyRequest().authenticated())
.formLogin(Customizer.withDefaults()).build();
}
@Bean
public UserDetailsService userDetailsService() {
var externalUser = User.withUsername(&quot;external&quot;).password(passwordEncoder().encode(&quot;12345&quot;))
.roles(&quot;read&quot;).build();
var internalUser = User.withUsername(&quot;internal&quot;).password(passwordEncoder().encode(&quot;12345&quot;))
.roles(&quot;read&quot;, &quot;write&quot;).build();
var admin = User.withUsername(&quot;admin&quot;).password(passwordEncoder().encode(&quot;12345&quot;))
.roles(&quot;read&quot;).build();
var userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(externalUser);
userDetailsService.createUser(internalUser);
userDetailsService.createUser(admin);
return userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Please let me know if any more info needed. Thanks in advance.
[1]: https://docs.spring.io/spring-authorization-server/docs/current/reference/html/core-model-components.html#oauth2-token-customizer
[2]: https://docs.spring.io/spring-authorization-server/docs/current/reference/html/core-model-components.html#oauth2-token-generator
</details>
# 答案1
**得分**: 1
你注册的Bean(`OAuth2TokenCustomizer<OAuth2TokenClaimsContext>`)用于自定义不透明令牌(令牌格式=引用),但看起来你正在使用默认的JWT令牌格式(令牌格式=自包含)。如果你想使用不透明令牌(我假设你不想),请查看文档以了解如何为每个客户端自定义令牌格式。
根据[参考文档][1],你可以简单地注册一个类型为`OAuth2TokenCustomizer<JwtEncodingContext>`的Bean来自定义JWT。
此外,文档还指出:
> 如果未将`OAuth2TokenGenerator`提供为`@Bean`或未通过`OAuth2AuthorizationServerConfigurer`进行配置,则将自动使用`JwtGenerator`配置`OAuth2TokenCustomizer<JwtEncodingContext>`的`@Bean`。
这意味着,如果你只想自定义令牌声明而不是生成令牌的整个过程,你可以简单地发布一个`OAuth2TokenCustomizer`的`@Bean`,并省略`OAuth2TokenGenerator`的`@Bean`。
因此,你的配置应该简单地包括:
```java
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> accessTokenCustomizer() {
return (context) -> {
...
};
}

我认为文档中应该更清晰地说明这一点,因为示例同时演示了如何自定义两者,但说明指出两者都不是必需的。

英文:

The bean that you’ve registered (OAuth2TokenCustomizer&lt;OAuth2TokenClaimsContext&gt;) is for customizing opaque tokens (token format = reference) but it looks like you’re using the default token format which is jwt (token format = self contained). If you want to use opaque tokens (which I'm assuming you are not), check the docs for how to customize the token format for each client.

As stated in the reference, for your case you can simply register a bean of type OAuth2TokenCustomizer&lt;JwtEncodingContext&gt; to customize a JWT.

Additionally, the docs also note that:

> If the OAuth2TokenGenerator is not provided as a @Bean or is not configured through the OAuth2AuthorizationServerConfigurer, an OAuth2TokenCustomizer&lt;JwtEncodingContext&gt; @Bean will automatically be configured with a JwtGenerator.

Which means that if you only want to customize the token claims and not the entire process of generating tokens, you can simply publish an OAuth2TokenCustomizer @Bean, and omit the OAuth2TokenGenerator @Bean.

Therefore, your configuration should simply have:

@Bean
public OAuth2TokenCustomizer&lt;JwtEncodingContext&gt; accessTokenCustomizer() {
    return (context) -&gt; {
        ...
    };
}

I think this point could be made a little clearer in the docs since the example demonstrates customizing both at the same time, but the note specifies that both are not required.

huangapple
  • 本文由 发表于 2023年6月1日 03:02:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76376548.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定