You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring OAuth2授权服务器3.2.4自定义授权码Claims对接Keycloak

解决方案:拆分Spring OAuth2授权服务器到Keycloak的用户Claims

一、Spring OAuth2 Authorization Server 端调整

核心是让授权服务器返回独立的given_name和email Claims,而非将用户信息合并到preferred_username中。

1. 改造User类实现UserDetails

让用户类适配Spring Security的UserDetails规范,避免依赖toString()传递用户信息:

@Getter
@Setter
@AllArgsConstructor
public class User implements UserDetails {
    private String givenName;
    private String email;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    @Override
    public String getPassword() {
        return "";
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

2. 自定义JWT Claims转换器

在生成JWT时,将用户信息拆分为独立Claims:

public class CustomJwtClaimsSetConverter implements Converter<JwtClaimsSetContext, JwtClaimsSet> {
    private final JwtClaimsSetConverter delegate = new DefaultJwtClaimsSetConverter();

    @Override
    public JwtClaimsSet convert(JwtClaimsSetContext context) {
        JwtClaimsSet claimsSet = delegate.convert(context);
        Authentication authentication = context.getAuthentication();
        
        if (authentication.getPrincipal() instanceof User user) {
            claimsSet.getClaims().put("given_name", user.getGivenName());
            claimsSet.getClaims().put("email", user.getEmail());
            claimsSet.getClaims().put("preferred_username", user.getEmail());
        }
        
        return claimsSet;
    }
}

3. 配置授权服务器核心组件

更新SecurityConfig,添加JWT生成、用户信息映射等配置:

@Configuration
public class SecurityConfig {

    @Bean
    @Order(1)
    SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(oidc -> oidc
                        .userInfoEndpoint(userInfo -> userInfo
                                .userInfoMapper(oidcUserInfoMapper())
                        )
                        .withDefaults()
                )
                .tokenGenerator(tokenGenerator());

        http
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .jwt(withDefaults()));

        return http.build();
    }

    @Bean
    public OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator() {
        JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder());
        jwtGenerator.setJwtClaimsSetConverter(jwtClaimsSetConverter());
        OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
        OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
        return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
    }

    @Bean
    public JwtClaimsSetConverter jwtClaimsSetConverter() {
        return new CustomJwtClaimsSetConverter();
    }

    @Bean
    public OidcUserInfoMapper oidcUserInfoMapper() {
        return (userInfoContext) -> {
            Authentication authentication = userInfoContext.getAuthentication();
            if (authentication.getPrincipal() instanceof User user) {
                OidcUserInfo userInfo = new OidcUserInfo();
                userInfo.setGivenName(user.getGivenName());
                userInfo.setEmail(user.getEmail());
                userInfo.setPreferredUsername(user.getEmail());
                return userInfo;
            }
            return new OidcUserInfo();
        };
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = generateRsaKey();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    private static RSAKey generateRsaKey() {
        KeyPair keyPair = generateRsaKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    private static KeyPair generateRsaKeyPair() {
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            return keyPairGenerator.generateKeyPair();
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }
}

4. 更新自定义认证提供者

确保返回的Authentication使用User类的权限:

@Service
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        User user = this.getUserDetails();
        return new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private User getUserDetails() {
        // 替换为实际的用户获取逻辑
        return new User("markus", "email01@noreply.com");
    }
}

二、Keycloak 端配置

让Keycloak接收并映射授权服务器返回的独立Claims:

1. 配置身份提供者的Claims映射

  • 登录Keycloak控制台,进入目标Realm → 「Identity Providers」→ 选择你的Spring OAuth2授权服务器(OIDC类型)。
  • 切换到「Mappers」标签页,点击「Create」添加两个映射器:
    • given_name 映射器:
      • Name: given_name
      • Mapper Type: Attribute Importer
      • Attribute Name: given_name
      • User Attribute Name: given_name
      • Claim JSON Type: String
      • 勾选「Add to ID token」和「Add to access token」
    • email 映射器:
      • Name: email
      • Mapper Type: Attribute Importer
      • Attribute Name: email
      • User Attribute Name: email
      • Claim JSON Type: String
      • 勾选「Add to ID token」和「Add to access token」

2. 确保客户端包含所需Claims

  • 进入目标客户端 → 「Client Scopes」标签页,确认「email」和「profile」内置Client Scope已添加。
  • 若需要自定义,可在客户端「Mappers」中添加映射,将Keycloak用户属性直接映射到访问令牌Claims。

完成以上配置后,Spring OAuth2授权服务器会返回包含独立given_name和email的JWT与用户信息,Keycloak会将这些Claims导入并生成符合需求的访问令牌。

内容的提问来源于stack exchange,提问作者Viachaslau Kamkou

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 01:57:03