跨域请求过滤掉了我的大部分 API 响应标头
Posted
技术标签:
【中文标题】跨域请求过滤掉了我的大部分 API 响应标头【英文标题】:Cross-origin request filters out most of my API response headers 【发布时间】:2017-11-01 17:22:54 【问题描述】:我有一个带有 Angular 4 Web 前端的 Spring Boot REST API。我通常对这两个框架都非常满意。一个不断出现的问题与 CORS 请求有关。感觉就像打地鼠游戏。每次我解决一个问题时,另一个问题很快就会出现并毁了我的周末。
我现在可以毫无问题地向我的 spring boot rest api 发出请求。但是......当我想从我的 Angular 网站的响应中检索我的标头时,只有 5 个标头可用,其中大部分都丢失了,包括我目前最受关注的 ETag 标头。
我读了一些 SO 帖子,声称我只需要在我的 angular http 调用中添加一个请求标头来公开我需要的标头(顺便说一下……在调试控制台中,我可以看到我期望的所有标头)。
Angular2 Http Response missing header key/values 的建议是添加 headers.append('Access-Control-Expose-Headers', 'etag');
我试过这个,但我得到以下错误:“在预检响应中,Access-Control-Allow-Headers 不允许请求标头字段 Access-Control-Expose-Headers。”
说实话,我对这条消息感到困惑。我在 Spring Boot 中调整了一些 CORS 设置,但无济于事。
我不知道该去哪里。我几乎正在考虑从 java + spring boot 切换回 php(畏缩),因为我从来没有遇到过这样的噩梦,我无法用 PHP 解决。
如果您有任何建议,请帮助我。
我的角度前端的相关代码如下:
import Injectable from '@angular/core';
import Http, RequestOptions, Response from '@angular/http';
import Post from '../class/post';
import Observable from 'rxjs/Rx';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/map';
@Injectable()
export class PostDaoService
private jwt: String;
private commentsUrl = 'http://myapidomain/posts';
private etag: string;
constructor(private http: Http, private opt: RequestOptions)
// tslint:disable-next-line:max-line-length
this.jwt = 'eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJQYXNjYWwiLCJ1c2VySWQiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.4D9TUDQAgIWAooyiMN1lV8Y5w56C3PKGzFzelSE9diqHMik9WE9x4EsNnEcxQXYATjxAZovpp-m72LpFADA';
getPosts(trigger: Observable<any>): Observable<Array<Post>>
this.opt.headers.set('Authorization', 'Bearer ' + this.jwt);
this.opt.headers.set('Content-Type', 'application/json');
this.opt.headers.set('Access-Control-Expose-Headers', 'etag');
if (this.etag !== null)
this.opt.headers.set('If-None-Match', this.etag);
return trigger.mergeMap(() =>
this.http.get(this.commentsUrl)
.map((response) =>
if (response.status === 304)
alert('NO CHANGE TO REPOURCE COLLECTION');
else if (response.status === 200)
console.log(response.headers);
console.log(response.text());
return response.json()._embedded.posts as Post[];
));
submitPost(): Promise<Object>
this.opt.headers.set('Authorization', 'Bearer ' + this.jwt);
this.opt.headers.set('Content-Type', 'application/json');
return this.http.post(this.commentsUrl, JSON.stringify(text: 'some new text'))
.toPromise()
.then(response => response.json())
.catch();
Spring Boot 应用中的 Application 类(带有 cors 配置)如下:
@SpringBootApplication
@EnableJpaRepositories("rest.api.repository")
@EnableMongoRepositories("rest.api.repository")
@EnableTransactionManagement
@EnableConfigurationProperties
@EnableCaching
public class Application extends SpringBootServletInitializer
public static final long LOGGED_IN_USER = 1L;
public static void main(String[] args)
SpringApplication.run(Application.class, args);
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
return application.sources(Application.class);
@Bean
public FilterRegistrationBean corsFilter()
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("Access-Control-Expose-Headers");
config.addAllowedHeader("X-Requested-With");
config.addAllowedHeader("Authorization");
config.addAllowedHeader("Content-Type");
config.addAllowedHeader("If-None-Match");
config.addAllowedHeader("Access-Control-Allow-Headers");
config.addExposedHeader("Access-Control-Allow-Origin");
config.addExposedHeader("Access-Control-Allow-Headers");
config.addExposedHeader("ETag");
config.addAllowedMethod("GET");
config.addAllowedMethod("POST");
config.addAllowedMethod("PUT");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
还有我的控制器:
@RepositoryRestController
@CrossOrigin(methods = RequestMethod.GET,
RequestMethod.POST,
RequestMethod.PUT,
RequestMethod.DELETE,
RequestMethod.OPTIONS,
RequestMethod.HEAD)
public class PostController
private PostRepository postRepository;
private UserRepository userRepository;
private LikeRepository likeRepository;
private DislikeRepository dislikeRepository;
@Autowired
PagedResourcesAssembler pagedResourcesAssembler;
protected PostController()
@Autowired
public PostController(PostRepository postRepository, UserRepository userRepository, LikeRepository likeRepository, DislikeRepository dislikeRepository)
this.postRepository = postRepository;
this.userRepository = userRepository;
this.likeRepository = likeRepository;
this.dislikeRepository = dislikeRepository;
@ResponseBody
@RequestMapping(value = "/posts", method = RequestMethod.GET)
public ResponseEntity<PagedResources<PersistentEntityResource>> getAll(HttpRequest request,
Pageable pageable,
PersistentEntityResourceAssembler resourceAssembler)
Page<Post> page = postRepository.findAll(pageable);
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.SECONDS))
.eTag(String.valueOf(page.hashCode()))
.body(pagedResourcesAssembler.toResource(page, resourceAssembler));
@ResponseBody
@RequestMapping(value = "/posts", method = RequestMethod.POST)
public ResponseEntity<PersistentEntityResource> sendPost(@RequestBody Post post,
PersistentEntityResourceAssembler resourceAssembler,
UriComponentsBuilder b)
User sender = userRepository.findOne(1L);
URI loc = null;
post.setSender(sender);
post = postRepository.save(post);
UriComponents uriComponents =
b.path("/posts/id").buildAndExpand(post.getIdentify());
HttpHeaders headers = new HttpHeaders();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.SECONDS))
.location(uriComponents.toUri())
.eTag(String.valueOf(post.getVersion()))
.body(resourceAssembler.toFullResource(post));
@ResponseBody
@RequestMapping(value = "/posts/id", method = RequestMethod.PUT)
public PersistentEntityResource edit(@PathVariable(value = "id") long id, @RequestBody Post post, PersistentEntityResourceAssembler resourceAssembler)
Post editedPost = postRepository.findOne(id);
editedPost.setCreated(post.getCreated());
editedPost.setText(post.getText());
postRepository.save(editedPost);
return resourceAssembler.toFullResource(editedPost);
@ResponseBody
@RequestMapping(value = "/posts/id/likes", method = RequestMethod.POST)
public PersistentEntityResource likePost(@PathVariable(value = "id") long id, PersistentEntityResourceAssembler resourceAssembler)
final boolean isAlreadyLiked = false;
User userWhoLikesIt = userRepository.findOne(1L);
Post post = postRepository.findOne(id);
post.setLiked(post.getLiked() + 1);
Likey like = new Likey(userWhoLikesIt);
likeRepository.save(like);
return resourceAssembler.toFullResource(like);
@ResponseBody
@RequestMapping(value = "/posts/id/dislikes", method = RequestMethod.POST)
public PersistentEntityResource dislikePost(@PathVariable(value = "id") long id, PersistentEntityResourceAssembler resourceAssembler)
User userWhoDislikesIt = userRepository.findOne(1L);
DisLike dislike = new DisLike(userWhoDislikesIt);
dislikeRepository.save(dislike);
return resourceAssembler.toFullResource(dislike);
@ResponseBody
@RequestMapping(value = "/posts/id/likes", method = RequestMethod.GET)
public ResponseEntity<PagedResources<PersistentEntityResource>> getLikes(HttpRequest request,
Pageable pageable,
PersistentEntityResourceAssembler resourceAssembler)
Page<Likey> page = likeRepository.findAll(pageable);
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.SECONDS))
.eTag(String.valueOf(page.hashCode()))
.body(pagedResourcesAssembler.toResource(page, resourceAssembler));
@ResponseBody
@RequestMapping(value = "/posts/id/dislikes", method = RequestMethod.GET)
public ResponseEntity<PagedResources<PersistentEntityResource>> getDislikes(HttpRequest request,
Pageable pageable,
PersistentEntityResourceAssembler resourceAssembler)
Page<DisLike> page = dislikeRepository.findAll(pageable);
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.SECONDS))
.eTag(String.valueOf(page.hashCode()))
.body(pagedResourcesAssembler.toResource(page, resourceAssembler));
有人知道我在这里做错了什么吗?
编辑:我还想知道我的 WebSecurityConfig.java 是否与此处相关,因为我必须在此处专门验证 OPTIONS 请求以避免先前的预检问题:
@Configuration
@EnableWebSecurity
@EnableAutoConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Autowired
private JwtAuthenticationProvider authenticationProvider;
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception
return new ProviderManager(Arrays.asList(authenticationProvider));
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception
JwtAuthenticationTokenFilter authenticationTokenFilter = new JwtAuthenticationTokenFilter();
authenticationTokenFilter.setAuthenticationManager(authenticationManager());
authenticationTokenFilter.setAuthenticationSuccessHandler(new JwtAuthenticationSuccessHandler());
return authenticationTokenFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
// All urls must be authenticated (filter for token always fires (/**)
.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").authenticated()
.and()
// Call our errorHandler if authentication/authorisation fails
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
.and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //.and()
// Custom JWT based security filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
【问题讨论】:
【参考方案1】:你必须让你的 Spring 代码发送 Access-Control-Expose-Headers
作为响应头——据我所知,这就是你已经拥有的 config.addExposedHeader(…)
代码应该为你做的事情。但是,如果您在响应中没有看到 Access-Control-Expose-Headers
标头,那么我猜想配置代码没有按预期工作,您需要对其进行调试。
Angular2 Http Response missing header key/values 的建议是添加
headers.append('Access-Control-Expose-Headers', 'etag');
该建议是错误的,因为它只是导致将 Access-Control-Expose-Headers
request 标头添加到从客户端前端代码发送的请求中。
但Access-Control-Expose-Headers
是一个响应标头,您向其发出请求的服务器必须在其响应中发送该标头。
我试过这个,但我得到以下错误:“在预检响应中,
Access-Control-Allow-Headers
不允许请求标头字段Access-Control-Expose-Headers
。”
对,那是因为您的客户端前端代码不应该发送该标头。
【讨论】:
我目前还没有机会调试这个问题,但是你刚刚证实了我对我发现的关于其中一些标头的目的以及我的 CORS 配置的建议的许多怀疑在春天设置。我在 CORS 周围遇到了很多问题,这些问题似乎主要是由于 spring-jwt-security 与跨源支持相结合。谢谢,今晚我会调查此事。以上是关于跨域请求过滤掉了我的大部分 API 响应标头的主要内容,如果未能解决你的问题,请参考以下文章
从跨域 http.get 请求的 ASP.NET Web API 2 响应中获取 Angular 中的特定响应标头(例如 Content-Disposition)