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

如何为Spring Boot Rest应用集成Keycloak(自定义登录表单+OAuth2-JWT)

问题描述

我有一个基于Basic Auth的Spring Boot Rest应用,希望集成Keycloak并将认证方式切换为使用Keycloak签发JWT令牌的OAuth2。我对实现方式存在疑惑:应该使用OAuth2资源服务(但我想要自定义登录和注册表单)、Keycloak Admin Client还是Keycloak REST API?

以下是我的WebSecurityConfig.java代码:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests(request -> request
                .requestMatchers("/webhook").permitAll()
                .requestMatchers("/swagger-ui/**").permitAll()
                .requestMatchers("/api-docs/**").permitAll()
                .requestMatchers("/").permitAll()
                .requestMatchers("/home").permitAll()
                .requestMatchers("/products").permitAll()
                .requestMatchers("/products_html").permitAll()
                .requestMatchers("/confirm-email").permitAll()
                .requestMatchers("/register").permitAll()
                .requestMatchers("/login").permitAll()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/admin/**").hasRole(String.valueOf(CustomerRole.ADMIN))
                .requestMatchers("/subscriptions/**").authenticated()
                .requestMatchers("/customers/**").authenticated()
                .requestMatchers("/profile").authenticated()
                .requestMatchers("/profile/**").authenticated()
                .anyRequest().authenticated());

        http.httpBasic(withDefaults())
                .csrf(AbstractHttpConfigurer::disable)
                .headers(c -> c
                        .frameOptions()
                        .disable());

        http.formLogin()
                .loginPage("/login")
                .usernameParameter("email")
                .passwordParameter("password")
                .defaultSuccessUrl("/", true)
                .failureUrl("/login?error")
                .permitAll()
                .and()
                .logout()
                .logoutUrl("/logout");

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService(CustomerRepository customerRepository, CustomerService mapper) {
        return email -> customerRepository.findByEmailIgnoreCase(email)
                .map(mapper::toUserDetails).orElseThrow(() -> new UsernameNotFoundException(email + " not found"));
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.US);
        return localeResolver;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");
        return interceptor;
    }

    @Bean(name = "messageSource")
    public ResourceBundleMessageSource bundleMessageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }
}
方案选择分析

1. OAuth2资源服务(核心推荐)

你的应用作为OAuth2资源服务器,负责验证Keycloak签发的JWT令牌,这是标准的OAuth2架构模式,完全适配REST API场景。默认情况下Keycloak会提供自带登录页,但要保留自定义登录注册表单,只需调整流程:

  • 保留自定义/login和/register页面,将表单提交逻辑改为调用Keycloak的开放接口,而非本地验证;
  • Spring Security侧仅负责校验请求头中的JWT令牌,无需处理用户密码验证逻辑;
  • 优势:遵循OAuth2标准,无需自行维护令牌签发、过期、刷新等逻辑,Keycloak全权负责用户身份存储、权限管控。

2. Keycloak Admin Client

这是Keycloak官方提供的Java客户端,主要用于管理Keycloak内部资源(创建用户、分配角色、修改权限等),不适合直接处理前端登录请求。你可以用它实现自定义注册流程:在/register端点接收用户信息后,调用Admin Client创建Keycloak用户,同时可同步到本地CustomerRepository(如果需要保留本地用户数据)。但登录流程仍需结合Keycloak的Token端点完成,单独使用Admin Client无法实现认证。

3. Keycloak REST API

直接调用Keycloak的原生REST接口(如Token端点、Admin API),和Admin Client的区别是无需引入官方依赖,自行封装HTTP请求即可。适合对依赖包大小有严格要求的场景,但需要自行处理请求签名、错误捕获、参数序列化等细节,开发效率不如Admin Client。

具体实现步骤建议

1. 添加资源服务器依赖

在pom.xml中引入Spring Security OAuth2资源服务器依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

2. 配置Keycloak JWT验证

在application.yml中添加Keycloak的JWT校验配置:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://你的Keycloak地址/auth/realms/你的Realm名
          jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs

3. 修改WebSecurityConfig

调整安全链配置,移除Basic Auth和默认表单登录,启用JWT验证:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(request -> request
            .requestMatchers("/webhook", "/swagger-ui/**", "/api-docs/**", "/", "/home", "/products", "/products_html", "/confirm-email", "/register", "/login").permitAll()
            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults()))
        .csrf(AbstractHttpConfigurer::disable)
        .headers(c -> c.frameOptions().disable());

    // 关闭Spring Security默认的表单登录和Basic Auth,用自定义逻辑替代
    http.formLogin(form -> form.disable());
    http.httpBasic(basic -> basic.disable());

    return http.build();
}

4. 实现自定义登录逻辑

创建AuthController处理登录请求,调用Keycloak Token端点获取JWT:

@RestController
public class AuthController {
    private final RestTemplate restTemplate;

    public AuthController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestParam String email, @RequestParam String password) {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "password");
        params.add("client_id", "你的客户端ID");
        params.add("client_secret", "你的客户端密钥"); // 仅保密客户端需要
        params.add("username", email);
        params.add("password", password);

        String tokenUrl = "http://你的Keycloak地址/auth/realms/你的Realm名/protocol/openid-connect/token";
        try {
            ResponseEntity<Map> response = restTemplate.postForEntity(tokenUrl, params, Map.class);
            return ResponseEntity.ok(response.getBody());
        } catch (HttpClientErrorException e) {
            return ResponseEntity.status(e.getStatusCode()).body("登录失败:" + e.getResponseBodyAsString());
        }
    }
}

5. 实现自定义注册逻辑

用Keycloak Admin Client创建用户(需先配置Admin Client参数):

@Service
public class AuthService {
    private final Keycloak keycloak;

    public AuthService(Keycloak keycloak) {
        this.keycloak = keycloak;
    }

    public void registerUser(String email, String password, String firstName, String lastName) {
        UserRepresentation user = new UserRepresentation();
        user.setEmail(email);
        user.setUsername(email);
        user.setFirstName(firstName);
        user.setLastName(lastName);
        user.setEnabled(true);

        CredentialRepresentation credential = new CredentialRepresentation();
        credential.setType(CredentialRepresentation.PASSWORD);
        credential.setValue(password);
        credential.setTemporary(false);
        user.setCredentials(List.of(credential));

        keycloak.realm("你的Realm名").users().create(user);
    }
}
总结
  • 核心采用OAuth2资源服务处理JWT验证,符合标准架构;
  • 自定义登录/注册表单通过调用Keycloak的Token端点和Admin API实现,可选Keycloak Admin Client简化开发;
  • 无需放弃现有自定义页面,只需将原表单提交逻辑从本地验证改为调用Keycloak接口即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 23:45:55