Spring Boot+React+Keycloak重定向登录页遇CORS缺失标头错误
架构可行性说明
你期望的「由Spring Boot完全处理Keycloak认证授权,前端无感知」的架构是可以实现的,但当前遇到的CORS问题源于SPA的AJAX请求特性与传统服务器端重定向认证流程不兼容。
CORS问题根源
当React通过AJAX请求Spring的受保护接口时,Spring返回302重定向到Keycloak,但浏览器的AJAX请求会拦截跨域重定向(React域localhost:3000与Keycloak域localhost:8090跨域),且此时Keycloak的Web Origins配置不会生效——因为Keycloak并未直接接收来自React的请求,而是Spring发起的重定向触发了跨域限制。
两种可行方案
方案一:保留Spring主导认证,调整前端请求逻辑
通过修改前端请求方式和Spring配置,让浏览器通过页面跳转完成认证流程,而非AJAX自动处理重定向:
Spring CORS配置(允许携带凭证)
确保Spring允许前端域的请求携带Cookie(Session Cookie由Spring在认证成功后设置):@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:3000")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("*")); config.setAllowCredentials(true); // 关键:允许携带Cookie UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } // 确保SecurityFilterChain中使用该CORS配置 @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(Customizer.withDefaults()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 指定自定义CORS配置 .exceptionHandling(customizer -> customizer.accessDeniedHandler(customAccessDeniedHandler())) .authorizeHttpRequests(customizer -> customizer .requestMatchers("/oauth2/login", "/oauth2/logout/success", "/oauth2/error").permitAll() .anyRequest().authenticated() ) .oauth2Login(configurer -> { configurer.userInfoEndpoint(customizer -> customizer.userAuthoritiesMapper(customGrantedAuthoritiesMapper())); configurer.loginPage("/oauth2/login"); configurer.defaultSuccessUrl("http://localhost:3000", true); // 登录成功后跳回前端 configurer.failureUrl("/oauth2/error"); }) .build(); }前端请求处理(拦截重定向并跳转)
前端AJAX请求时开启凭证携带,并在收到302响应时手动跳转到重定向地址:import axios from 'axios'; axios.get('http://localhost:8080/api/test', { withCredentials: true }) .catch(error => { if (error.response?.status === 302) { // 手动跳转到Keycloak登录页面 window.location.href = error.response.headers.location; } });登录成功后,Spring会设置Session Cookie,前端后续请求携带该Cookie即可访问受保护接口。
方案二:改为前端处理Keycloak认证,向Spring传递JWT
这是SPA场景的标准最佳实践,避免跨域重定向问题,前端通过Keycloak适配器获取JWT,后端作为资源服务器验证令牌:
Spring配置为资源服务器
修改application.yml和SecurityFilterChain:spring: security: oauth2: resourceserver: jwt: issuerUri: <你的Keycloak issuer URI>@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .csrf(Customizer.withDefaults()) .cors(Customizer.withDefaults()) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .build(); }前端集成Keycloak适配器
使用@react-keycloak/web快速集成:npm install @react-keycloak/web keycloak-js在根组件中配置:
import { ReactKeycloakProvider } from '@react-keycloak/web'; import Keycloak from 'keycloak-js'; const keycloak = new Keycloak({ url: 'http://localhost:8090', realm: '<你的Realm名称>', clientId: '<你的客户端ID>' }); function App() { return ( <ReactKeycloakProvider authClient={keycloak}> {/* 你的应用组件 */} </ReactKeycloakProvider> ); } export default App;请求后端时携带JWT:
import axios from 'axios'; import { useKeycloak } from '@react-keycloak/web'; function TestComponent() { const { keycloak } = useKeycloak(); const fetchData = async () => { const response = await axios.get('http://localhost:8080/api/test', { headers: { Authorization: `Bearer ${keycloak.token}` } }); console.log(response.data); }; return <button onClick={fetchData}>获取数据</button>; }
方案对比
| 方案 | 优势 | 劣势 |
|---|---|---|
| Spring主导认证 | 后端完全控制认证逻辑,前端无需了解Keycloak细节 | 依赖Cookie跨域传递,生产环境需HTTPS配合SameSite配置,前端需处理重定向跳转 |
| 前端处理认证 | 符合SPA最佳实践,无跨域重定向问题,扩展性强(支持多端) | 前端需引入认证逻辑,需维护Keycloak适配器配置 |
两种架构均有成功落地案例,若你坚持后端主导认证,可选择方案一;若追求更适配SPA的架构,方案二更推荐。
内容的提问来源于stack exchange,提问作者grange

