如何为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

