源码:https://github.com/langyastudio/langya-tech/tree/springboot/cors
说到跨域访问,必须先解释一个名词:同源策略。所谓同源策略就是在浏览器端出于安全考量,向服务端发起请求必须满足:协议相同、Host相同、端口相同的条件,否则访问将被禁止,该访问也就被称为跨域访问。
虽然跨域访问被禁止之后,可以在一定程度上提高了应用的安全性,但也为开发带来了一定的麻烦。比如:我们开发一个前后端分离的易用,页面及 js 部署在一个主机的 nginx 服务中,后端接口部署在一个 tomcat 应用容器中,当前端向后端发起请求的时候一定是不符合同源策略的,也就无法访问。
来自网络,找不到原文了
目前比较常用的是:
cors
、jsonp
虽然浏览器对于不符合同源策略的访问是禁止的,但是仍然存在例外的情况,如以下资源引用的标签不受同源策略的限制:
- html 的 script 标签
- html 的 link 标签
- html 的 img 标签
- html 的 iframe 标签
除了基于 HTML 本身的特性实现跨域访问,我们还可以使用如下方式实现跨域访问:
jsonp
- postMessage
- flash request
CORS
— 跨域资源共享,通过修改 Http 协议 header 的方式实现跨域。说的简单点就是通过设置 HTTP 的响应头信息,告知浏览器哪些情况在不符合同源策略的条件下也可以跨域访问,浏览器通过解析 Http 协议中的 Header 执行具体判断。具体的 Header 如下:
-
Access-Control-Allow-Origin
允许哪些 ip 或 域名可以跨域访问
-
Access-Control-Max-Age
表示在多少秒之内不需要重复校验该请求的跨域访问权限
-
Access-Control-Allow-Methods
表示允许跨域请求的 HTTP 方法,如:GET,POST,PUT,DELETE
-
Access-Control-Allow-Headers
表示访问请求中允许携带哪些 Header 信息,如:
Accept
、Accept-Language
、Content-Language
、Content-Type
实际上对跨域访问的支持在服务端实现起来更加容易,最常用的方法就是通过代理的方式,如:nginx 或 haproxy 代理跨域。
其实实现代理跨域的逻辑非常简单:就是在不同的资源服务:js 资源、html 资源、css 资源、接口数据资源服务的前端搭建一个中间层,所有的浏览器及客户端访问都通过代理转发。所以在浏览器、客户端看来,它们访问的都是同一个 ip、同一个端口的资源,从而符合同源策略实现跨域访问。
cors 跨域对于 get
、post
等都支持。
可以基于 Filter 实现,全局有效。下图的虚线框就是 Filter2 的拦截范围,这里关于Filter不做详细介绍:
│ ▲
▼ │
┌───────┐
│Filter1│
└───────┘
│ ▲
▼ │
┌───────┐
┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐
└───────┘
│ │ ▲ │
▼ │
│ ┌─────────────────┐ │
│DispatcherServlet│<───┐
│ └─────────────────┘ │ │
│ ┌────────────┐
│ │ │ModelAndView││
│ └────────────┘
│ │ ▲ │
│ ┌───────────┐ │
│ ├───>│Controller1│────┤ │
│ └───────────┘ │
│ │ │ │
│ ┌───────────┐ │
│ └───>│Controller2│────┘ │
└───────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
直接继承 FilterRegistrationBean<Filter>
可以快速的定义 filter 并自动生效,如下:
@Order(1)
@Component
public class CorsFilterBean extends FilterRegistrationBean<Filter>
{
@PostConstruct
public void init()
{
setFilter(new CorsFilter());
//设置复合条件的URL地址
setUrlPatterns(List.of("/admin/*", "/portal/*"));
}
class CorsFilter implements Filter
{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 跨域
String uiDomain = null;
String origin = req.getHeader("origin");
if (StrUtil.isNotBlank(origin))
{
uiDomain = origin;
}
if (StrUtil.isNotBlank(uiDomain))
{
String method = req.getMethod();
if ("OPTIONS".equals(method) || "POST".equals(method))
{
resp.addHeader("Access-Control-Allow-Origin", uiDomain);
resp.addHeader("Access-Control-Allow-Headers", "access-control-allow-origin," +
"Authorization," + "school_id," +
"token,Content-Type,Cookie,Origin, Referer,If-Match, If-Modified-Since, If-None-Match," +
"If-Unmodified-Since, X-Requested-With,X_Requested_With");
resp.addHeader("Access-Control-Allow-Method", "GET, POST");
resp.addHeader("Access-Control-Allow-Credentials", "true");
// 在这个时间范围内,所有同类型的请求都将不再发送预检请求而是直接使用此次返回的头作为判断依据
resp.addHeader("Access-Control-Max-Age", "1440");
}
//P3P
//跨域向IE写入cookie
resp.addHeader("P3P", "CP=\"CURa ADMa DEVa PSAo PSDo OUR BUS UNI PUR INT DEM STA PRE COM NAV OTC NOI " +
"DSP COR\"");
// OPTIONS请求
if ("OPTIONS".equals(method))
{
//204 empty
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
}
resp.addHeader("Keep-Alive", "timeout=5, max=60");
chain.doFilter(request, response);
}
}
}
还可以使用如下方式实现 cors 跨域:
重写 WebMvcConfigurer 的 addCorsMappings 方法
使用 CrossOrigin 注解
jsonp
是一个非官方的协议,它允许在服务器端集成 Script tags 返回至客户端,通过 Javascript callback 的形式实现跨域访问 — jsonp
只适合 GET 类型的请求。
使用 @ControllerAdvice
注解 + 继承 ResponseBodyAdvice
,即可统一处理返回值的方式完成 jsonp
的实现,如下:
@ControllerAdvice
public class JsonpResponseAdvice extends FastJsonHttpMessageConverter implements ResponseBodyAdvice
{
private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*");
public static final Charset UTF8 = StandardCharsets.UTF_8;
private final Charset charset;
private final SerializerFeature[] features;
@Override
public Object beforeBodyWrite(Object obj,
@NotNull MethodParameter methodParameter,
@NotNull MediaType mediaType,
@NotNull Class aClass,
@NotNull ServerHttpRequest serverHttpRequest,
@NotNull ServerHttpResponse serverHttpResponse)
{
HttpServletRequest servletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
HttpServletResponse response = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse();
String functionName = servletRequest.getParameter("callback");
if (functionName != null)
{
if (this.isValidJsonpQueryParam(functionName))
{
String text = JSON.toJSONString(obj, this.features);
String jsonpText = functionName + "(" + text + ")";
byte[] bytes = jsonpText.getBytes(this.charset);
OutputStream out = null;
try
{
//jsonp content
response.setContentType("application/javascript; charset=utf-8");
out = response.getOutputStream();
out.write(bytes);
out.flush();
out.close();
}
catch (IOException e)
{
}
}
}
return obj;
}
@Override
public boolean supports(@NotNull MethodParameter methodParameter,
@NotNull Class aClass)
{
return true;
}
protected boolean isValidJsonpQueryParam(String value)
{
return CALLBACK_PARAM_PATTERN.matcher(value).matches();
}
public JsonpResponseAdvice()
{
super();
this.charset = UTF8;
this.features = new SerializerFeature[0];
}
}
JSONP works by creating a
<script>
tag that executes Javascript from a different domain;it is not possible to send a POST request using a<script>
tag.