Angular集成Keycloak携带Bearer Token触发CORS及临时头问题
问题背景
- 依赖版本:keycloak-angular@10.0.1、keycloak-js@18.0.0、Angular@13.0.3
- 本地Keycloak配置了2个客户端:Angular前端应用、Java Spring REST API后端服务
- 已验证能力:Angular端通过Keycloak登录可正常生成token,手动将token填入Postman调用Java后端接口可正常返回资源
- 故障现象:Angular应用直接发起请求调用Java后端接口异常,将
enableBearerInterceptor设为true后,浏览器网络面板显示请求为临时头(Provisional headers),面板看似携带了token,但后端实际未收到token,仅收到带CORS预检字段的请求头 - 调用接口信息:GET方法,地址为
localhost:7770/teams
相关代码
Keycloak初始化逻辑(app.module.ts):
function initializeKeycloak(keycloak: KeycloakService): () => Promise<boolean> { console.log('keycloak init'); return () => keycloak.init({ config: { url: 'http://localhost:8080/', realm: 'dev', clientId: 'frontend' }, initOptions: { onLoad: 'check-sso', checkLoginIframe: true, silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html' }, enableBearerInterceptor: false, bearerPrefix: 'Bearer', }).finally(() => { console.log("finally!"); scriptsService.load('env').then(() => { console.log("Loaded env variables:", window["env"]) }).catch(error => console.warn("Unable to load script: ", error) ).finally(() => { }); }); } { provide: APP_INITIALIZER, useFactory: initializeKeycloak, deps: [KeycloakService], multi: true },
登录落地页请求逻辑:
this.teamsService.fetchTeams().subscribe(value => { this.isLoading = false; if (value.length != 0) { } this.teams = value; this.selectedTeam = this.activeTeamService.getActiveTeam(); });
fetchTeams(filter?: any): Observable<Team[]> { return this.getList(this.TEAMS, filter, this.teamAdapter); };
protected getList(path, params, adapter): Observable<any> { let options = { params: params } return this.http.get(this.urlBase + path, options) .pipe(map((data: any[]) => data ? data.map(item => adapter.adapt(item)) : data)) .pipe(catchError(err => this.error(err))); };
后端实际收到的请求头:
host = localhost:7770 connection = keep-alive pragma = no-cache cache-control = no-cache accept = */* access-control-request-method = GET access-control-request-headers = access-control-allow-origin,authorization origin = http://localhost:4200 user-agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36 sec-fetch-mode = cors sec-fetch-site = same-site sec-fetch-dest = empty referer = http://localhost:4200/ accept-encoding = gzip, deflate, br accept-language = en-US,en;q=0.9
问题根因
你看到的带access-control-request-*字段的请求不是实际的业务GET请求,是浏览器自动发起的CORS预检OPTIONS请求。开启bearer拦截器后,请求会自动附加Authorization自定义头,浏览器判定该请求不属于简单请求,必须先发送OPTIONS预检,等后端返回符合CORS规范、允许携带Authorization头的响应后,才会发送真正带token的GET请求。
故障核心是Spring后端没有正确处理这个OPTIONS预检请求,预检直接被鉴权逻辑拦截或者返回了不符合要求的响应,浏览器直接拦截了后续的真实GET请求,所以网络面板只会显示临时头,后端自然也收不到带token的实际请求。
Postman本身不会触发CORS预检逻辑,所以手动塞token调接口能通,和浏览器场景的行为逻辑完全不一样,不能作为同场景排查依据。
解决方案
按以下步骤排查修复:
- 修正Spring后端CORS配置,必须满足以下要求:
- 允许OPTIONS方法直接放行,不要被鉴权过滤器(Spring Security、Keycloak安全过滤器等)拦截
- 允许的请求头必须包含
Authorization,不要只配置跨域源相关字段 - 允许的源明确写前端地址
http://localhost:4200,不要用*通配符(带身份凭证的跨域请求不允许使用通配符源) - 开启允许携带凭证配置
参考全局CORS配置:
如果使用了Spring Security,还要在安全配置里放行所有OPTIONS预检请求:@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:4200") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors().and() .csrf().disable() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 其余接口鉴权规则按业务需求配置 .anyRequest().authenticated(); return http.build(); } - 检查keycloak-angular拦截器配置,确认后端接口地址
localhost:7770没有被加到bearerExcludedUrls排除列表中,不需要额外配置拦截规则的话保持默认即可。 - 修复后打开浏览器网络面板验证:先看OPTIONS请求的响应状态是否为200,响应头是否包含
Access-Control-Allow-Origin: http://localhost:4200、Access-Control-Allow-Headers: Authorization两个字段,预检通过后会自动发起真正的GET请求,此时GET请求头会正常携带Authorization: Bearer xxx字段,后端可正常解析token返回数据。
内容的提问来源于stack exchange,提问作者schylake
相关产品推荐
相关产品推荐

