基于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
相关产品推荐
相关产品推荐

