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

在自定义Spring Security认证系统中集成Keycloak实现双认证流

解决方案

一、同时保留自定义认证与Keycloak认证两种独立流程

Spring Security完全支持多认证流程并行,核心是配置多个SecurityFilterChain,为不同路径分配对应认证逻辑:

  • 自定义认证FilterChain:指定匹配路径(如/custom/login/**),沿用原有的LoginFilter、LoginProvider,保留Login表的用户校验逻辑不变。
  • Keycloak认证FilterChain:使用Spring Security的OAuth2 Client模块对接Keycloak,指定匹配路径(如/oauth2/**、/login/oauth2/**),配置Keycloak客户端信息,让用户通过Keycloak跳转至Azure AD完成登录。
  • 统一用户映射:两种认证成功后都转换为Spring Security的UserDetails对象,确保后续权限逻辑无需修改。可自定义UserDetailsService,从Login表加载用户信息,关联同一登录记录。

示例代码片段:

// 自定义认证的FilterChain
@Bean
public SecurityFilterChain customLoginFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/custom/login/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .addFilterBefore(new LoginFilter(), UsernamePasswordAuthenticationFilter.class)
        .authenticationProvider(new LoginProvider());
    return http.build();
}

// Keycloak认证的FilterChain
@Bean
public SecurityFilterChain keycloakFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/oauth2/**", "/login/oauth2/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2Login(oauth2 -> oauth2
            .clientRegistrationRepository(clientRegistrationRepository())
            .authorizedClientService(authorizedClientService())
            .userInfoEndpoint(userInfo -> userInfo.userService(customOidcUserService())));
    return http.build();
}

// Keycloak客户端注册配置
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
    return new InMemoryClientRegistrationRepository(keycloakClientRegistration());
}

private ClientRegistration keycloakClientRegistration() {
    return ClientRegistration.withRegistrationId("keycloak")
        .clientId("你的Keycloak客户端ID")
        .clientSecret("你的Keycloak客户端密钥")
        .authorizationUri("https://你的Keycloak域名/auth/realms/你的领域/protocol/openid-connect/auth")
        .tokenUri("https://你的Keycloak域名/auth/realms/你的领域/protocol/openid-connect/token")
        .userInfoUri("https://你的Keycloak域名/auth/realms/你的领域/protocol/openid-connect/userinfo")
        .redirectUri("{baseUrl}/login/oauth2/code/keycloak")
        .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
        .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
        .scope("openid", "profile", "email")
        .userNameAttributeName(IdTokenClaimNames.SUB)
        .build();
}

二、将Keycloak令牌存入Login对象,适配现有认证流程

这种方案更贴合现有架构,无需大幅改动原有逻辑,步骤如下:

  1. 自定义OidcUserService同步用户与令牌
    Keycloak认证成功后,提取用户唯一标识(如邮箱),查询Login表:存在则更新令牌,不存在则创建新记录,将Keycloak的ID Token存入Login对象的新增字段(如keycloakToken)。
@Service
public class CustomOidcUserService extends OidcUserService {
    private final LoginRepository loginRepository;

    public CustomOidcUserService(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    @Override
    public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
        OidcUser oidcUser = super.loadUser(userRequest);
        String email = oidcUser.getEmail();
        
        Login login = loginRepository.findByEmail(email)
                .orElseGet(() -> {
                    Login newLogin = new Login();
                    newLogin.setEmail(email);
                    newLogin.setUsername(oidcUser.getFullName());
                    return newLogin;
                });
        
        login.setKeycloakToken(oidcUser.getIdToken().getTokenValue());
        loginRepository.save(login);
        
        return new CustomOidcUser(oidcUser, login);
    }
}
  1. 适配原有认证逻辑
    修改LoginProvider,新增令牌校验分支:若请求携带Keycloak令牌,从Login表查询对应用户,调用Keycloak的令牌校验端点验证有效性,通过后生成Authentication对象。

  2. 自定义登出逻辑
    实现LogoutSuccessHandler,登出时:清理Login表中的Keycloak令牌,重定向至Keycloak登出端点销毁外部会话,最后销毁本地应用会话。

@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
    private final LoginRepository loginRepository;
    private final String keycloakLogoutUri = "https://你的Keycloak域名/auth/realms/你的领域/protocol/openid-connect/logout";

    public CustomLogoutSuccessHandler(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (authentication.getPrincipal() instanceof UserDetails userDetails) {
            Login login = loginRepository.findByUsername(userDetails.getUsername()).orElse(null);
            if (login != null && login.getKeycloakToken() != null) {
                login.setKeycloakToken(null);
                loginRepository.save(login);
                
                String redirectUri = UriComponentsBuilder.fromHttpUrl(keycloakLogoutUri)
                        .queryParam("redirect_uri", request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/")
                        .build().toUriString();
                response.sendRedirect(redirectUri);
                return;
            }
        }
        response.sendRedirect("/login");
    }
}

// 在Security配置中绑定登出处理器
http.logout(logout -> logout
        .logoutSuccessHandler(customLogoutSuccessHandler())
        .invalidateHttpSession(true)
        .deleteCookies("JSESSIONID"));

注意事项

  • 多流程方案需控制会话隔离或共享,避免权限冲突;
  • 存储Keycloak令牌时建议加密,防止明文泄露;
  • 定时清理Login表中过期的令牌;
  • Keycloak对接Azure AD需在Keycloak后台添加Azure AD为身份提供者(选择OpenID Connect类型,填入Azure AD的端点信息即可)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 01:27:04