开发中遇到的关于跨域请求的问题

8 minute

SpringBoot 跨域配置

这是一段跨域配置的代码,有一些问题需要注意。

 1@Configuration
 2public class CorsConfig implements WebMvcConfigurer {
 3    @Override
 4    public void addCorsMappings(CorsRegistry registry) {
 5        registry.addMapping("/**")
 6                .allowedOriginPatterns("http://127.0.0.1:*", "https://127.0.0.1:*", "http://localhost:*", "https://localhost:*")
 7                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
 8                .allowCredentials(true)
 9                .allowedHeaders("*")
10                .maxAge(3600);
11    }
12}

第一,127.0.0.1localhost 需要同时指定!通过测试,如果只指定其中一个,无法进行另一个的请求。

第二,当项目里使用的 springboot 的版本是 2.1.2.RELEASE 时,使用 allowedOrigins("*") 没有问题,但是当版本改为 2.4.4 后会提示报错。

当 allowCredentials 为 true 时,allowedOrigins 不能包含特殊值 “*",因为它不能在 “Access-Control-Allow-Origin” 响应头中设置。

要允许一组起源的凭证,明确地列出它们,或者考虑使用 allowedOriginPatterns 代替。

Options 请求跨域问题

在浏览器内请求跨域时,浏览器会先进行一次 preflight request(预检请求),以确认目标域是否允许跨域,如果允许,再进行实际操作中的请求。

在接口请求中我们总会自定义请求头做 token,但是浏览器预检请求中的 OPTIONS 只会携带自定义的 token 字段但不会带相应的值,导致跨域以及报错。

所以要处理跨域以及在 SpringSecurity 的认证过滤器链中需要过滤掉 OPTIONS。

如果使用的是 Interceptor,则可以在定义的拦截器中将 OPTIONS 方法的请求放行即可。

 1@Component
 2public class EnterInterceptor implements HandlerInterceptor {
 3    @Override
 4    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 5        if (request.getMethod().equals("OPTIONS")) {
 6            return true;
 7        }
 8        if (AuthUtil.checkToken(request)) {
 9            return true;
10        } else {
11            response.sendRedirect("/api/error/401/" + URLEncoder.encode("token验证失败", "UTF-8"));
12            return false;
13        }
14    }
15}

除此之外,为节省资源,可以通过配置缓存时间以防止浏览器频繁进行预检请求。

在 CORS 中,当浏览器发送跨域请求时,服务器可以返回一个 Access-Control-Max-Age 响应头,它指定了在这个响应被缓存多久。这意味着,在接下来的一段时间内,浏览器不需要向服务器再发送预检请求,而可以直接发送实际请求。

还是如下这段跨域配置代码:

1@Override
2public void addCorsMappings(CorsRegistry registry) {
3    registry.addMapping("/**")
4            .allowedOriginPatterns("http://127.0.0.1:*", "https://127.0.0.1:*", "http://localhost:*", "https://localhost:*")
5            .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
6            .allowCredentials(true)
7            .allowedHeaders("*")
8            .maxAge(3600);
9}

在上面的代码片段中,maxAge(3600) 指定了响应在缓存中的最长时间为 3600 秒(1 小时),在这段时间内,浏览器将不再发送预检请求,从而提高了跨域请求的性能和效率。然而,需要注意的是,设置 maxAge 过长可能会导致安全风险,因为在这段时间内,任何网站都可以使用这个响应进行访问。因此,应该根据实际需求设置一个适当的值。

请求头字段 Authorization

请求头字段 Authorization 是用于在客户端向服务器发送请求时进行身份验证的一种方式。它通常用于发送包含访问令牌(Access Token)的请求,以便服务器可以验证客户端是否有权限执行该请求。

当使用 Authorization 头字段进行身份验证时,其值通常是以 Bearer 或 Basic 为前缀的字符串,例如 Bearer xxxxxxxxBasic username:password(其中 username 和 password 是 base64 编码后的用户名和密码字符串)。服务器可以从该头字段中提取身份验证信息并进行验证,以确保请求者有权访问所请求的资源或执行所请求的操作。

但是为什么要添加 Bearer / Basic 呢?

Bearer 和 Basic 是 Authorization 头字段中常用的两种身份验证方式:

Bearer 身份验证方式通常用于 OAuth 2.0 协议中,其特点是使用一个访问令牌(Access Token)作为凭据进行身份验证,客户端在请求时需要将该令牌放在 Authorization 头字段中的 Bearer 后面,例如:Authorization: Bearer xxxxxxxx。服务器在接收到请求后,可以根据该令牌验证请求者的身份和权限,并对其进行授权。

Basic 身份验证方式则比较简单,在请求时需要将用户名和密码用冒号连接后进行 base64 编码,并将编码后的字符串放在 Authorization 头字段中的 Basic 后面,例如:Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=。服务器在接收到请求后,可以将 base64 解码后的用户名和密码与其保存的凭据进行比较,以验证请求者的身份和权限。

在 HTTP 协议中,Authorization 头字段的格式是固定的,它由一个身份验证方式和一个凭据组成,两者之间用空格分隔。因此,在使用 Authorization 头字段进行身份验证时,需要在身份验证方式和凭据之间加上相应的前缀,以便服务器可以正确解析和验证请求。

所以,如果后端采用了 OAuth 之类的框架,则需要正确配置 Authorization 头字段。如果后端没有采用类似框架,比如是用了 JWT 加密的 token,那可以不用 Authorization 头字段,而采用自定义的字段,比如 auth,之后在代码中自行获取 auth 字段值即可:

 1public static Boolean checkToken(HttpServletRequest request) {
 2    String token = request.getHeader("auth");
 3    if (token == null) { // token不存在
 4        log.error("token 不存在");
 5        return false;
 6    }
 7    Claims claims = JwtUtil.getClaimsByToken(token);
 8    if (claims == null || JwtUtil.isTokenExpired(claims.getExpiration())) { // token 错误或过期
 9        log.error("token 错误或过期");
10        return false;
11    }
12    log.info("token 验证通过");
13    return true;
14}