文章目录
跨源资源共享策略问题
新起的项目需要前后端对接,联调时前端在浏览器窗口请求后端接口出现异常,浏览器控制台报出以下信息。由于当前主流都是前后端分离架构,前端项目和后端项目通常不在同一个站点,所以在浏览器上很容易出现这个问题
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
1. Spring 解决方式:设置 CORS
面向前端的后端项目大部分接口都需要向前端开放,所以比较简单通用的方式是在后端 Spring 项目中添加 Filter拦截器
,通过为 HTTP 响应添加指定 header 的方式来设置 CORS。一个开箱即用的设置如下:
以下代码通过
@Value
动态配置允许跨源共享的地址列表,配合 Apollo 等配置中心框架可以实现比较灵活的跨源共享
@Order(value = 1)
@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({WebMvcConfigurer.class, HandlerInterceptor.class})
public class CorsFilter implements Filter {
@Value("#{'${web.allow.origins:https://www.baidu.com}'.split(',')}")
private List<String> allowedOrigins;
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String origin = httpServletRequest.getHeader(HttpHeaders.ORIGIN);
if (allowedOrigins.contains(origin)) {
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, HttpMethodEnum.allowedMethods());
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "x-requested-with,Content-Type,X-CAF-Authorization-Token,sessionToken,X-TOKEN");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); //是否支持cookie跨域
}
if (((HttpServletRequest) request).getMethod().equals(HttpMethodEnum.OPTIONS.getMsg())) {
response.getWriter().println("ok");
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
@Getter
@AllArgsConstructor
public enum HttpMethodEnum {
UNDEFINED(-1, "undefined"),
POST(0, "POST"),
GET(1, "GET"),
PUT(2, "PUT"),
OPTIONS(3, "OPTIONS"),
DELETE(4, "DELETE"),
;
@Getter
private final Integer code;
@Getter
private final String msg;
public static String allowedMethods() {
return Stream.of(POST, GET, PUT, OPTIONS, DELETE)
.map(HttpMethodEnum::getMsg)
.collect(Collectors.joining(","));
}
}
}
2. CORS 的前世今生
实际上 跨源资源共享 (Cross-origin Resource Sharing,CORS)
的产生是多种因素共同作用的结果:
- 浏览器基于安全需要(主要是防范 CSRF)实现了
同源策略
,限制在一个站点内访问另一个站点资源的行为- 但客观上一个站点不可能包含用户需要用到的所有资源,所以确实存在跨站访问资源的需求
- 为了满足这种正常需求,浏览器实现 CORS 策略以实现正常的跨站点资源访问
2.1 CSRF 跨站请求伪造漏洞
跨站请求伪造(Cross-site request forgery,CSRF)
是指利用受害者尚未失效的身份认证信息,在受害人不知情的情况下使用其身份向服务器发送请求,达到完成非法操作的目的。一个 CSRF 攻击的流程如下所示:
- 用户输入账号信息请求登录站点A
- 站点A 验证用户信息,验证通过后返回一个cookie给用户,浏览器将其写在本地
- 用户站点A 的cookie未失效,在同一浏览器中点击了恶意链接,访问恶意站点B
- 站点B 收到用户请求后返回攻击性代码,构造访问站点A 的语句
- 浏览器收到恶意代码后,在用户不知情的情况下携带cookie信息请求了站点A,完成非法操作
前提条件:
- 用户访问站点A 并产生了cookie
- 用户在站点A 的cookie依然有效的情况下访问了恶意站点B
2.2 浏览器同源策略的产生
上一节大致介绍了 CSRF 的攻击原理,正是为了防范 CSRF 之类的漏洞,浏览器实现了最基本也最核心的安全功能同源策略
,阻止非同源的内容进行交互
同源策略会限制客户端 js 代码的部分行为:
- 无法读取非同源站点的 Cookie、LocalStorage 和 IndexedDB
- 无法接触非同源站点的 DOM
- 无法向非同源站点发送 AJAX 请求
- 限制跨域请求,一般有两种限制方式:
- 浏览器直接限制发起跨域请求
- 跨域请求可以正常发起,但是返回的结果被浏览器拦截
同源的定义
如果两个 URL 的协议、域名、端口三者有任意一个不同,则这两个 URL 是非同源的,直观的比较如下表所示:
源URL | 目标URL | 是否同源 |
---|---|---|
http://nathan.com/user/page.html | http://nathan.com/user/other.html | 同源,只是资源路径不同 |
http://nathan.com/user/page.html | https://nathan.com/user/other.html | 不同源,协议不同 |
http://nathan.com/user/page.html | http://nathan.com:88/user/other.html | 不同源,端口不同 ( HTTP 协议默认端口为 80) |
http://nathan.com/user/page.html | http://others.com/user/other.html | 不同源,主机不同 |
2.3 跨源资源共享 CORS 的实现
跨源资源共享(CORS)
是在浏览器同源策略
的前提下,实现跨站点资源访问的一种方案。它是 HTTP 协议的一部分,允许服务端通过 HTTP 协议头来指定哪些主机可以从这个服务端加载资源。读者如有兴趣可前往火狐开发者官网CORS详细解释 了解详细信息。
- 通常浏览器都采用可以正常发起请求,但是返回的结果被拦截的方式对跨域请求进行限制。这就是说如果不做其他限制,跨域请求可能已到达服务器,并对服务器端资源进行了操作,实际上根本没有被限制住
- 为了防止这种情况发生,CORS 对这种可能更改服务器数据的 HTTP 请求提出了规范要求:浏览器必须先使用
OPTIONS
方法发起一个预检请求,从而获知服务器是否允许即将发送的真正的跨域请求。如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。因为处理方式的差异,据此将跨域请求大致分为两类:
- 简单跨域请求
- 预检跨域请求
2.3.1 简单请求
通常前端发起的一个GET查询请求就是简单跨域请求
,其严格定义可参考火狐开发者官网CORS若干访问控制场景。对于简单跨域请求,处理流程如下图所示:
- 浏览器向其他域的服务器请求资源时会在 HTTP 请求头中添加Origin,将 JavaScript 脚本所在域填充进去
// Origin 字段用于说明请求来自哪个源(协议 + 域名 + 端口),服务器可以根据这个值决定是否同意这次请求 Origin: http://www.nathan.com
- 服务器端收到该跨域请求后,可以根据 HTTP 请求头的 Origin 来决定是否允许这次跨域请求。如果允许,则在 HTTP 响应中添加如下头信息通知浏览器;如果不允许则返回一个没有包含 Access-Control-Allow-Origin 头的 HTTP 响应
// 必选,值要么是 HTTP 请求头的 Origin 字段的值,要么是一个 * 值,表示接受任意域名的请求 Access-Control-Allow-Origin: http://www.nathan.com // 可选,值是一个布尔值,表示是否允许发送 Cookie。设为 true 表示服务器允许 Cookie 包含 // 在请求中发送过来,如果服务器不要浏览器发送Cookie,删除这个响应头即可 Access-Control-Allow-Credentials: true
- 浏览器收到响应后,通过响应头中的
Access-Control-Allow-Origin
字段来判断当前域是否已经得到服务端授权,如果是则将请求结果返回给 JavaScript,否则忽略此次响应
2.3.2 预检请求
一个跨域请求如果不属于简单跨域请求
,那通常就可以将其归入预检跨域请求
。使用预检跨域请求可以避免跨域请求对服务器的用户数据产生未预期的影响,其处理如下:
- 浏览器在发送真实 HTTP 请求之前先发送一个
OPTIONS
预检请求,检测服务器端是否支持进行跨域资源访问的真实请求。OPTIONS
请求头中会携带真实请求的信息,其大致如下:// 必选,用于指定请求源 Origin: http://www.nathan.com // 必选,用于描述真实请求的方法 Access-Control-Request-Method: POST // 可选,指定真实请求会额外发送的请求头 Access-Control-Request-Headers: Header1,Header2
- 服务器端接到预检请求后,检查请求头相关字段检验是否允许跨源请求。如果允许该跨域请求,则可在 HTTP 响应中添加如下头信息告知浏览器;如果不允许则返回一个没有包含 Access-Control-Allow-Origin 头的 HTTP 响应
Access-Control-Allow-Origin: http://www.nathan.com // 必选,值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法 Access-Control-Allow-Methods: GET, POST, PUT // 如果跨域请求包括 Access-Control-Request-Headers 字段, // 则Access-Control-Allow-Headers 字段是必需的。它也是一个逗号分隔的字符串, // 表明服务器支持的所有头信息字段 Access-Control-Allow-Headers: Header1,Header2,Header3 Access-Control-Allow-Credentials: true // 可选,用于指定本次预检请求的有效期,单位为秒。在有效期间,不用发出另一条预检请求 Access-Control-Max-Age: 3600
- 浏览器取得
OPTIONS
请求返回的结果,根据响应头相关字段决定是否继续发送真实的请求进行跨域资源访问