跨域请求过滤掉了我的大部分 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)

该请求被重定向到“https://..com/site/login?”,这对于需要预检的跨域请求是不允许的

无法从前端 JavaScript 访问跨域响应标头

django+vue无法设置跨域cookies

jQuery ajax 请求因为跨域而被阻止

允许跨域 ajax 请求中的标头