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」
- Name:
- 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」
- Name:
- given_name 映射器:
2. 确保客户端包含所需Claims
- 进入目标客户端 → 「Client Scopes」标签页,确认「email」和「profile」内置Client Scope已添加。
- 若需要自定义,可在客户端「Mappers」中添加映射,将Keycloak用户属性直接映射到访问令牌Claims。
完成以上配置后,Spring OAuth2授权服务器会返回包含独立given_name和email的JWT与用户信息,Keycloak会将这些Claims导入并生成符合需求的访问令牌。
内容的提问来源于stack exchange,提问作者Viachaslau Kamkou
相关产品推荐
相关产品推荐

