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

基于Spring OAuth2与Keycloak实现多认证方式的方案咨询

Spring OAuth2 + Keycloak 混合认证实现方案

需求概述

  • 现有Spring Boot项目基于Spring OAuth2实现认证,保留原有令牌存储与提供者
  • 新增Keycloak认证支持,部分用户的账号密码存储在Keycloak,由Keycloak提供访问令牌与刷新令牌
  • 角色、权限管理仍由Spring负责,从数据库加载,Keycloak仅作为部分用户的令牌提供者

现有核心代码

1. WebSecurityConfig

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientDetailsService clientDetailsService;

    private AccessDecisionManager accessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().accessDecisionManager(accessDecisionManager)
                .antMatchers("/service/*").fullyAuthenticated()
                .anyRequest().permitAll().and().httpBasic().and().csrf().disable();
    }

    @Override
    @Bean(name = "authenticationManagerBean")
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    @Bean
    @Autowired
    public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
        TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
        handler.setTokenStore(tokenStore);
        handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
        handler.setClientDetailsService(clientDetailsService);
        return handler;
    }

    @Bean
    @Autowired
    public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
        TokenApprovalStore store = new TokenApprovalStore();
        store.setTokenStore(tokenStore);
        return store;
    }

    @Bean
    public AffirmativeBased accessDecisionManager() {
        List<AccessDecisionVoter<?>> accessDecisionVoters = new ArrayList<>();
        accessDecisionVoters.add(new ScopeVoter());
        accessDecisionVoters.add(new RoleVoter());
        accessDecisionVoters.add(new AuthenticatedVoter());

        AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters);
        return accessDecisionManager;
    }
}

2. CustomClientService

@Component
public class CustomClientService implements ClientDetailsService {

    private static Map<String, BaseClientDetails> cache = new ConcurrentHashMap<>();
    
    @Autowired
    UserService userService;
    
    @Autowired
    AccessRightsService accessRightsService;
    
    
    @Override
    public ClientDetails loadClientByClientId(String paramString) throws ClientRegistrationException {
        // 原有客户端加载逻辑
        ...
    }
}

3. MyTokenServices

public class MyTokenServices extends DefaultTokenServices {
    
    private static Logger log = LoggerFactory.getLogger(MyTokenServices.class);
    
    public UserService userService;
    
    public AccessRightsService accessRightService;

    private TokenStore my_tokenStore;
    
    @Override
    public void setTokenStore(TokenStore tokenStore) {
        super.setTokenStore(tokenStore);
        my_tokenStore = tokenStore;
    }
    
    @Override
    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2AccessToken retVal= super.createAccessToken(authentication);
        if(retVal instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken defRetVal =  (DefaultOAuth2AccessToken)retVal;
            log.info("New loging request"+ defRetVal.toString());
            my_tokenStore.storeAccessToken(defRetVal, authentication);
        }
        return retVal;
         
    }
    
    @Override
    public OAuth2Authentication loadAuthentication(String accessTokenValue)
            throws AuthenticationException, InvalidTokenException {
        OAuth2Authentication retVal = super.loadAuthentication(accessTokenValue);
        OAuth2Request oldRequest = retVal.getOAuth2Request();
        
        User user = userService.getUserByUsername(oldRequest.getClientId());
        if(changeAutheticator(retVal, user)) {
            HashSet<GrantedAuthority> authorities = new HashSet<>();
            user.getRoles().forEach(a->authorities.add(new SimpleGrantedAuthority(a.getRoleName())));
            
            Set<String> accessRights = accessRightService.getUserAccessRights(user);
            if(accessRights != null) {
                accessRights.forEach(right->{
                    authorities.add(new SimpleGrantedAuthority(right));
                });
            }
            OAuth2Request newRequest  = new OAuth2Request(retVal.getOAuth2Request().getRequestParameters(),
                oldRequest.getClientId(), authorities, oldRequest.isApproved(), oldRequest.getScope(), 
                oldRequest.getResourceIds(), oldRequest.getRedirectUri(), oldRequest.getResponseTypes(), oldRequest.getExtensions());
        
            retVal = new OAuth2Authentication(newRequest, retVal.getUserAuthentication());
        }
        return retVal;
        
    }
    
    private boolean changeAutheticator(OAuth2Authentication auth, User user) {
        if(user == null) return false;
        if(user != null ) {
            if(user.getRoles() != null) {
                if(auth.getOAuth2Request()!=null && auth.getOAuth2Request().getAuthorities() != null){
                    for(Role role:user.getRoles()){
                        if(!auth.getOAuth2Request().getAuthorities().stream().anyMatch(a->a.getAuthority().equals(role.getRoleName())))
                            return true;
                        }
                    }
                    for(GrantedAuthority ga : auth.getOAuth2Request().getAuthorities()) {
                        if(!user.getRoles().stream().anyMatch(a->a.getRoleName().equals(ga.getAuthority())))
                            return true;
                        }
                    }
                }
            }
        return false;
    }
}

实现步骤

1. 引入Keycloak依赖

在pom.xml中添加Keycloak Spring Boot Starter:

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-spring-boot-starter</artifactId>
    <version>22.0.5</version> <!-- 匹配你的Keycloak版本 -->
</dependency>

2. 配置Keycloak连接

在application.yml中配置Keycloak服务器信息:

keycloak:
  realm: your-realm-name
  auth-server-url: http://your-keycloak-domain:8080/realms/your-realm-name
  resource: your-client-id
  credentials:
    secret: your-client-secret
  use-resource-role-mappings: false # 禁用Keycloak角色映射,使用Spring自身权限体系

3. 实现复合认证提供者

创建CompositeAuthenticationProvider,根据用户类型选择Spring OAuth2或Keycloak认证:

@Component
public class CompositeAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private AuthenticationProvider springOauthAuthProvider; // 原有Spring OAuth2认证提供者
    @Autowired
    private KeycloakAuthenticationProvider keycloakAuthProvider;

    @Autowired
    private UserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        User user = userService.getUserByUsername(username);
        
        // 假设User实体有isKeycloakUser字段标记用户是否存储在Keycloak
        if (user != null && user.isKeycloakUser()) {
            return keycloakAuthProvider.authenticate(authentication);
        } else {
            return springOauthAuthProvider.authenticate(authentication);
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return springOauthAuthProvider.supports(authentication) 
                || keycloakAuthProvider.supports(authentication);
    }
}

4. 修改WebSecurityConfig集成Keycloak

更新安全配置,替换认证管理器为复合提供者,并应用Keycloak配置:

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientDetailsService clientDetailsService;
    @Autowired
    private CompositeAuthenticationProvider compositeAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(compositeAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.apply(KeycloakSpringBootConfigurer.init(this))
                .and()
                .authorizeRequests()
                .accessDecisionManager(accessDecisionManager())
                .antMatchers("/service/*").fullyAuthenticated()
                .anyRequest().permitAll()
                .and()
                .httpBasic()
                .and()
                .csrf().disable();
    }

    @Override
    @Bean(name = "authenticationManagerBean")
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    // 保留原有Bean定义
    @Bean
    @Autowired
    public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore) {
        TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
        handler.setTokenStore(tokenStore);
        handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
        handler.setClientDetailsService(clientDetailsService);
        return handler;
    }

    @Bean
    @Autowired
    public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
        TokenApprovalStore store = new TokenApprovalStore();
        store.setTokenStore(tokenStore);
        return store;
    }

    @Bean
    public AffirmativeBased accessDecisionManager() {
        List<AccessDecisionVoter<?>> accessDecisionVoters = new ArrayList<>();
        accessDecisionVoters.add(new ScopeVoter());
        accessDecisionVoters.add(new RoleVoter());
        accessDecisionVoters.add(new AuthenticatedVoter());

        AffirmativeBased accessDecisionManager = new AffirmativeBased(accessDecisionVoters);
        return accessDecisionManager;
    }
}

5. 适配MyTokenServices支持Keycloak令牌

修改令牌服务,兼容Keycloak颁发的令牌,加载Spring侧权限:

public class MyTokenServices extends DefaultTokenServices {
    
    private static Logger log = LoggerFactory.getLogger(MyTokenServices.class);
    
    @Autowired
    public UserService userService;
    
    @Autowired
    public AccessRightsService accessRightService;

    private TokenStore my_tokenStore;
    
    @Override
    public void setTokenStore(TokenStore tokenStore) {
        super.setTokenStore(tokenStore);
        my_tokenStore = tokenStore;
    }
    
    @Override
    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2AccessToken retVal= super.createAccessToken(authentication);
        if(retVal instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken defRetVal =  (DefaultOAuth2AccessToken)retVal;
            log.info("New loging request"+ defRetVal.toString());
            my_tokenStore.storeAccessToken(defRetVal, authentication);
        }
        return retVal;
         
    }
    
    @Override
    public OAuth2Authentication loadAuthentication(String accessTokenValue)
            throws AuthenticationException, InvalidTokenException {
        OAuth2Authentication retVal = null;
        try {
            // 优先从Spring TokenStore加载令牌
            retVal = super.loadAuthentication(accessTokenValue);
        } catch (InvalidTokenException e) {
            // 尝试验证Keycloak令牌
            KeycloakSecurityContext keycloakContext = KeycloakSecurityContextHolder.getContext();
            if (keycloakContext != null) {
                String username = keycloakContext.getToken().getPreferredUsername();
                User user = userService.getUserByUsername(username);
                if (user != null) {
                    // 构建Spring侧权限集合
                    HashSet<GrantedAuthority> authorities = new HashSet<>();
                    user.getRoles().forEach(role -> authorities.add(new SimpleGrantedAuthority(role.getRoleName())));
                    
                    Set<String> accessRights = accessRightService.getUserAccessRights(user);
                    if (accessRights != null) {
                        accessRights.forEach(right -> authorities.add(new SimpleGrantedAuthority(right)));
                    }
                    
                    // 构建OAuth2Authentication对象
                    OAuth2Request oAuth2Request = new OAuth2Request(
                            null, 
                            clientDetailsService.loadClientByClientId(keycloakContext.getClientId()), 
                            authorities, 
                            true, 
                            null, 
                            null, 
                            null, 
                            null, 
                            null
                    );
                    UsernamePasswordAuthenticationToken authToken = 
                            new UsernamePasswordAuthenticationToken(username, null, authorities);
                    retVal = new OAuth2Authentication(oAuth2Request, authToken);
                } else {
                    throw new InvalidTokenException("User not found in Spring database");
                }
            } else {
                throw e;
            }
        }
        
        // 原有权限更新逻辑
        if (retVal != null) {
            User user = userService.getUserByUsername(retVal.getName());
            if(changeAutheticator(retVal, user)) {
                HashSet<GrantedAuthority> authorities = new HashSet<>();
                user.getRoles().forEach(a->authorities.add(new SimpleGrantedAuthority(a.getRoleName())));
                
                Set<String> accessRights = accessRightService.getUserAccessRights(user);
                if(accessRights != null) {
                    accessRights.forEach(right->{
                        authorities.add(new SimpleGrantedAuthority(right));
                    });
                }
                OAuth2Request newRequest  = new OAuth2Request(retVal.getOAuth2Request().getRequestParameters(),
                    retVal.getOAuth2Request().getClientId(), authorities, retVal.getOAuth2Request().isApproved(), 
                    retVal.getOAuth2Request().getScope(), retVal.getOAuth2Request().getResourceIds(), 
                    retVal.getOAuth2Request().getRedirectUri(), retVal.getOAuth2Request().getResponseTypes(), 
                    retVal.getOAuth2Request().getExtensions());
        
                retVal = new OAuth2Authentication(newRequest, retVal.getUserAuthentication());
            }
        }
        return retVal;
        
    }
    
    private boolean changeAutheticator(OAuth2Authentication auth, User user) {
        if(user == null) return false;
        if(user != null ) {
            if(user.getRoles() != null) {
                if(auth.getOAuth2Request()!=null && auth.getOAuth2Request().getAuthorities() != null){
                    for(Role role:user.getRoles()){
                        if(!auth.getOAuth2Request().getAuthorities().stream().anyMatch(a->a.getAuthority().equals(role.getRoleName())))
                            return true;
                        }
                    }
                    for(GrantedAuthority ga : auth.getOAuth2Request().getAuthorities()) {
                        if(!user.getRoles().stream().anyMatch(a->a.getRoleName().equals(ga.getAuthority())))
                            return true;
                        }
                    }
                }
            }
        return false;
    }
}

6. 配置Keycloak刷新令牌

在Keycloak控制台的客户端配置中,开启Offline Access权限,并确保客户端的Access Type为confidential。前端请求令牌时携带offline_access scope,即可获取刷新令牌,后续通过Keycloak的/realms/{realm}/protocol/openid-connect/token接口,用刷新令牌换取新的访问令牌。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 15:40:48