Fixing 401s with CORS Preflights and Spring Security – 用CORS预热和Spring安全修复401的问题

最后修改: 2019年 2月 14日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this short tutorial, we’re going to learn how to solve the error “Response for preflight has invalid HTTP status code 401”, which can occur in applications that support cross-origin communication and use Spring Security.

在这个简短的教程中,我们将学习如何解决 “预检的响应有无效的HTTP状态代码401 “的错误,这可能发生在支持跨源通信和使用Spring Security的应用程序中。

First, we’ll see what cross-origin requests are and then we’ll fix a problematic example.

首先,我们将看到什么是跨源请求,然后我们将修复一个有问题的例子。

2. Cross-Origin Requests

2.跨源请求

Cross-origin requests, in short, are HTTP requests where the origin and the target of the request are different. This is the case, for instance, when a web application is served from one domain and the browser sends an AJAX request to a server in another domain.

简而言之,跨源请求是指请求的来源和目标不同的HTTP请求。例如,当一个网络应用程序从一个域提供服务,而浏览器向另一个域的服务器发送AJAX请求时,就属于这种情况。

To manage cross-origin requests, the server needs to enable a particular mechanism known as CORS, or Cross-Origin Resource Sharing.

为了管理跨源请求,服务器需要启用一种被称为CORS的特殊机制,即跨源资源共享。

The first step in CORS is an OPTIONS request to determine whether the target of the request supports it. This is called a pre-flight request.

CORS的第一步是一个OPTIONS请求,以确定请求的目标是否支持它。这被称为飞行前请求。

The server can then respond to the pre-flight request with a collection of headers:

然后,服务器可以用一组标头来响应飞行前的请求。

  • Access-Control-Allow-Origin: Defines which origins may have access to the resource. A ‘*’ represents any origin
  • Access-Control-Allow-Methods: Indicates the allowed HTTP methods for cross-origin requests
  • Access-Control-Allow-Headers: Indicates the allowed request headers for cross-origin requests
  • Access-Control-Max-Age: Defines the expiration time of the result of the cached preflight request

So, if the pre-flight request doesn’t meet the conditions determined from these response headers, the actual follow-up request will throw errors related to the cross-origin request.

所以,如果飞行前的请求不符合从这些响应头确定的条件,实际的后续请求将抛出与跨源请求有关的错误。

It’s easy to add CORS support to our Spring-powered service, but if configured incorrectly, this pre-flight request will always fail with a 401.

我们由Spring驱动的服务添加CORS支持很容易,但如果配置不当,这个飞行前请求将总是以401失败。

3. Creating a CORS-enabled REST API

3.创建一个支持CORS的REST API

To simulate the problem, let’s first create a simple REST API that supports cross-origin requests:

为了模拟这个问题,让我们首先创建一个简单的REST API,支持跨源请求。

@RestController
@CrossOrigin("http://localhost:4200")
public class ResourceController {

    @GetMapping("/user")
    public String user(Principal principal) {
        return principal.getName();
    }
}

The @CrossOrigin annotation makes sure that our APIs are accessible only from the origin mentioned in its argument.

@CrossOrigin注解确保我们的API只能从其参数中提到的起源处访问。

4. Securing Our REST API

4.确保我们的REST API安全

Let’s now secure our REST API with Spring Security:

现在让我们用Spring Security来保护我们的REST API。

@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .anyRequest()
            .authenticated()
            .and()
            .httpBasic();
        return http.build();
    }
}

In this configuration class, we’ve enforced authorization to all incoming requests. As a result, it will reject all requests without a valid authorization token.

在这个配置类中,我们已经对所有传入的请求进行了强制授权。因此,它将拒绝所有没有有效授权令牌的请求。

5. Making a Pre-flight Request

5.提出飞行前请求

Now that we’ve created our REST API, let’s try a pre-flight request using curl:

现在我们已经创建了我们的REST API,让我们用curl尝试一下飞行前请求。

curl -v -H "Access-Control-Request-Method: GET" -H "Origin: http://localhost:4200" 
  -X OPTIONS http://localhost:8080/user
...
< HTTP/1.1 401
...
< WWW-Authenticate: Basic realm="Realm"
...
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Access-Control-Allow-Origin: http://localhost:4200
< Access-Control-Allow-Methods: POST
< Access-Control-Allow-Credentials: true
< Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
...

From the output of this command, we can see that the request was denied with a 401.

从这个命令的输出中,我们可以看到请求被拒绝了,结果是401.

Since this is a curl command, we won’t see the error “Response for preflight has invalid HTTP status code 401” in the output.

由于这是一个curl命令,我们不会在输出中看到错误 “Response for preflight has invalid HTTP status code 401″。

But we can reproduce this exact error by creating a front end application that consumes our REST API from a different domain and running it in a browser.

但我们可以通过创建一个前端应用程序,从不同的领域消费我们的REST API,并在浏览器中运行它,来重现这个确切的错误。

6. The Solution

6.解决办法

We haven’t explicitly excluded the preflight requests from authorization in our Spring Security configuration. Remember that Spring Security secures all endpoints by default.

在我们的Spring Security配置中,我们还没有明确地将预检请求排除在授权之外。请记住,Spring Security默认保护所有端点的安全。

As a result, our API expects an authorization token in the OPTIONS request as well.

因此,我们的API希望在OPTIONS请求中也有一个授权令牌。

Spring provides an out of the box solution to exclude OPTIONS requests from authorization checks:

Spring提供了一个开箱即用的解决方案,将OPTIONS请求排除在授权检查之外。

@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // ...
        http.cors(); 
        return http.build();
    }
}

The cors() method will add the Spring-provided CorsFilter to the application context, bypassing the authorization checks for OPTIONS requests.

cors()方法将把Spring提供的CorsFilter添加到应用程序上下文中,绕过OPTIONS请求的授权检查。

Now we can test our application again and see that it’s working.

现在我们可以再次测试我们的应用程序,看看它是否在工作。

7. Conclusion

7.结语

In this short article, we’ve learned how to fix the error “Response for preflight has an invalid HTTP status code 401”, which is linked with Spring Security and cross-origin requests.

在这篇短文中,我们学习了如何修复 “预检的响应有一个无效的HTTP状态代码401 “的错误,这与Spring Security和跨源请求有关。

Note that, with the example, the client and the API should run on different domains or ports to recreate the problem. For instance, we can map the default hostname to the client and the machine IP address to our REST API when running on a local machine.

注意,在这个例子中,客户端和API应该在不同的域或端口上运行,以重现这个问题。例如,当在本地机器上运行时,我们可以将默认主机名映射到客户端,将机器IP地址映射到我们的REST API。

As always, the example shown in this tutorial can be found over on Github.

一如既往,本教程中显示的例子可以在Github上找到over