如何用自定义对象替代KeycloakPrincipal适配Spring非Boot REST API
解决方案:替换KeycloakPrincipal为自定义CustomObject
完全可以实现,既不用修改原有业务代码,也能避免直接耦合Keycloak API。最佳实现方式不是单纯使用Filter,而是扩展Spring Security的AuthenticationProvider——这更贴合Spring Security的认证生命周期,逻辑更严谨。
方式一:扩展KeycloakAuthenticationProvider(推荐)
Keycloak的Spring Security适配器默认使用KeycloakAuthenticationProvider生成包含KeycloakPrincipal的认证对象。我们可以扩展这个Provider,在认证成功后将KeycloakPrincipal转换为你的CustomObject:
public class CustomKeycloakAuthenticationProvider extends KeycloakAuthenticationProvider { @Override protected Authentication createSuccessfulAuthentication(HttpServletRequest request, KeycloakAuthenticationToken token) { // 先让父类完成基础的令牌验证 Authentication originalAuth = super.createSuccessfulAuthentication(request, token); // 从KeycloakPrincipal中提取用户信息 KeycloakPrincipal<KeycloakSecurityContext> keycloakPrincipal = (KeycloakPrincipal<KeycloakSecurityContext>) originalAuth.getPrincipal(); AccessToken accessToken = keycloakPrincipal.getKeycloakSecurityContext().getToken(); // 转换为你的CustomObject,根据业务需求填充字段 CustomObject customObject = new CustomObject(); customObject.setId(accessToken.getSubject()); // Keycloak用户唯一ID customObject.setUsername(accessToken.getPreferredUsername()); customObject.setRoles(accessToken.getRealmAccess().getRoles()); // 补充其他自定义字段,比如从accessToken.getOtherClaims()获取自定义Claims // 生成新的认证对象,替换Principal为CustomObject return new UsernamePasswordAuthenticationToken( customObject, originalAuth.getCredentials(), originalAuth.getAuthorities() ); } }
然后在Spring Security配置类中替换默认的Provider:
@Configuration @EnableWebSecurity public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { CustomKeycloakAuthenticationProvider customProvider = new CustomKeycloakAuthenticationProvider(); // 可选:配置角色映射,比如将Keycloak的ROLE_前缀转为Spring Security的格式 customProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper()); auth.authenticationProvider(customProvider); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http.authorizeRequests() .antMatchers("/api/**").authenticated(); } @Bean public KeycloakConfigResolver keycloakConfigResolver() { // 加载classpath下的keycloak.json配置 return new KeycloakSpringConfigResolver(); } }
方式二:自定义后置Filter(快速改造方案)
如果不想扩展Provider,也可以通过自定义Filter在认证完成后修改SecurityContext中的Principal。注意要把这个Filter放在Keycloak认证Filter之后执行:
public class CustomPrincipalConversionFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 仅处理Keycloak认证后的令牌 if (auth instanceof KeycloakAuthenticationToken) { KeycloakPrincipal<KeycloakSecurityContext> keycloakPrincipal = (KeycloakPrincipal<KeycloakSecurityContext>) auth.getPrincipal(); AccessToken accessToken = keycloakPrincipal.getKeycloakSecurityContext().getToken(); // 构建CustomObject CustomObject customObject = new CustomObject(); customObject.setId(accessToken.getSubject()); customObject.setUsername(accessToken.getPreferredUsername()); // 填充其他字段... // 替换SecurityContext中的认证对象 Authentication newAuth = new UsernamePasswordAuthenticationToken( customObject, auth.getCredentials(), auth.getAuthorities() ); SecurityContextHolder.getContext().setAuthentication(newAuth); } filterChain.doFilter(request, response); } }
在Security配置中注册Filter:
@Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); // 放在KeycloakAuthenticationProcessingFilter之后执行 http.addFilterAfter(new CustomPrincipalConversionFilter(), KeycloakAuthenticationProcessingFilter.class) .authorizeRequests() .antMatchers("/api/**").authenticated(); }
方案对比
- 扩展Provider:完全融入Spring Security的认证流程,在认证成功的第一时间完成转换,逻辑更可靠,是推荐的方案。后续更换令牌提供商时,只需替换这个Provider的实现即可,业务代码完全不受影响。
- 自定义Filter:实现简单,适合快速验证,但需要注意Filter的执行顺序,避免在认证未完成时执行转换。
注意事项
- 所有Keycloak相关的转换逻辑都要封装在扩展类/Filter中,业务代码不要直接引用Keycloak的API,确保解耦。
- 根据你的
CustomObject结构,正确从Keycloak的AccessToken中提取信息,包括标准Claims和自定义Claims(通过accessToken.getOtherClaims().get("custom-claim")获取)。
内容的提问来源于stack exchange,提问作者Pieter
相关产品推荐
相关产品推荐

