更新时间:2021-12-23 10:18:10
文章目录
一、问题描述
跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的。通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景。从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域。
二、解决办法
- 本文提供方法要求springboot 2.0版本以上。
- 这里采用CORS的方式解决跨域问题。
- 推荐使用第三种方法,将跨域拦截器添加到项目中,然后最先增加到拦截器中即可。
1:通过@CrossOrigin注解实现跨域
使用时需要在每个controller类上配置@CrossOrigin注解,注解参数解释如下:
参数 | 解释 |
---|---|
origins | 访问接口的源。用人话说就是前端调用接口,那源就是进入前端项目的URL中的(协议://域名:端口) |
allowCredentials | 是否允许附带身份凭证和cookies。推荐设置为true,需注意的是如果设置为true,那 origins就必须指定为具体的源 |
maxAge | 设置预检请求的有效时间。 |
methods | 设置实际请求允许的HTTP方法。默认与接口规定的方法一致。 |
exposedHeaders | 设置出了基本的响应头之外允许浏览器解析的响应头 |
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(
origins = "http://localhost:8080",
allowCredentials = "true",
exposedHeaders = "基本的响应头之外,允许浏览器访问的响应头,多个响应头用逗号隔开",
maxAge = 1800
)
@RestController
@RequestMapping("/frost2")
public class CrossOriginDemo {
//必须规定HTTP方法,否则跨域配置可能不生效
@GetMapping("/dailyquestion")
public String test(){
return "frost2";
}
}
2:实现WebMvcConfigurer重写addCorsMappings方法
第一种方式需要在每个Controller中增加@CrossOrigin注解,这样就很不方便。这种方式只需要增加一个配置类即可,其他地方不用做任何改变。
注意::这种方法和第三种使用拦截器不兼容。如果同时重写了addCorsMappings并且配置了其他的拦截器,例如登录拦截,那么就会导致跨域配置失败,因为无法规定两者的先后顺序。解决办法就是采用拦截器配置跨域,然后让跨域拦截器最先执行。具体见下文第三种方法。
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfigurerDemo implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedHeaders("custom-header") //允许前端携带的请求头,有自定义请求头就写进去
.allowedMethods("*") //允许前端请求的方法,"*"表示所有
.allowCredentials(true) //是否允许附带身份凭证和cookies
.maxAge(1800) //预见请求有效时间
.exposedHeaders("custom-header"); //允许浏览器访问的响应头,有自定义响应头就写进去
}
}
3:通过springboot的拦截器
与方法2相同,配置拦截器同样是实现WebMvcConfigurer接口,但实现的方法为addInterceptors。在该方法中就可以添加我们自定义的拦截器。而真自定义拦截器需要继承HandlerInterceptorAdapter,将需要在方法之前执行的逻辑写在preHandle方法中。
注意: 如果你的项目不用拦截器你那么方法2、3均可,但是如果你的项已经使用了拦截器或者将会使用拦截器,那么需要使用方法3,并且设置跨域拦截器最先执行,让跨配置最先生效这样对你的项目就不会有什么影响。
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 跨域配置类
* @author frost2
* @date 2019年12月19日15:19:20
*/
@Component
public class CORSInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8080");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type,自定义请求头");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Expose-Headers", "customResponse,自定义响应头");
//预检请求返回
if (request.getMethod().equals("OPTIONS")){
response.setStatus(200);
return false;
}
return true;
}
}
将跨域配置类添加到拦截其中,拦截器执行顺序与添加顺序相同。
import com.yjzn.customization.common.intercept.CORSInterceptor;
import com.yjzn.customization.common.intercept.PrivilegeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.text.SimpleDateFormat;
/**
*
* @author frost2
* @date 2019年12月19日15:36:58
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private CORSInterceptor corsInterceptor;
@Autowired
private PrivilegeInterceptor privilegeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*
* 先添加拦截器,优先级越高
*/
registry.addInterceptor(corsInterceptor)
.addPathPatterns("/**");
//后续添加的拦截器将在跨域拦截器之后执行,本例添加了权限拦截器
registry.addInterceptor(privilegeInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/admin/login"); //不拦截请求路径为/admin/login的请求
}
}
三、跨域相关知识
1:浏览器同源策略
(1) 什么是源?
协议://域名:端口 他们三者的组合就是一个源。
同源即为两个URL他们的协议域名端口完全相同,有一个不同即为不同源。
需要注意的是:
- http://localhost:8080与http://127.0.0.1:8080也属于不同源。
- IE浏览器没有将端口号加入到同源策略的组成部分之中,即http://localhost:8080与http://localhost:8081为同源,不受任何限制。
(2) 什么是同源策略?
同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
这是在MDN(Mozilla Developer Network)给出的定义。
对于这句话我的理解为:同源策略就是一种安全机制,他限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。
(3) 为什么需要同源策略?
浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。
这一部分我了解不深,可以参考下面两篇文章,目前我知道如果没有同源策略那么就能够实现跨站请求伪造(CSRF),他会导致你的个人隐私泄露以及财产损失。
不要再问我跨域的问题了 浅析CSRF攻击
2:CORS(跨域资源共享) – Cross-Origin Resource Sharing
(1) 什么是CROS?
CORS是一种机制,它使用额外的HTTP头来告诉浏览器让运行在一个 origin (domain)上的Web应用被准许访问来自不同源服务器上的指定的资源。
也就是说一个web应用可以通过设置额外的HTTP头来描述哪些源可以从我这里读取资源。以此来实现处理跨域请求。至于是哪些请求头会面会有总结。
(2) CROS功能
跨域资源共享标准新增了一组 HTTP首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是
GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的POST
请求),浏览器必须首先使用OPTIONS
方法发起一个预检请求(preflight request)
,从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括Cookies和 HTTP 认证相关数据)。
(3) CROS请求分类
其实CORS请求本是没有分类的。只不过其中一些请求会触发预检请求(prefilght request),另一些请求不会触发预检请求,所以人们分别将他们称为非简单请求和简单请求,也就有了分类这么一说。
故关键点就在于哪些请求会触发预检请求,哪些请求则不会。
满足以下条件不会触发预检请求,即称为简单请求。
HTTP请求方法为以下之一:
1.GET
2.HEAD
3.POST
HTTP请求头只能为以下,不允许定义其他首部字段:(记住前四个即可)
1.Accept
2.Accept-Language
3.Content-Language
4.Content-Type,并且Content-Type的值只能是
text/plain
multipart/form-data
application/x-www-form-urlencoded
5.Downlink
6.Save-Data
7.Viewpost-Width
8.width
9.DPR
满足以下条件就会触发预检请求,即称为非简单请求。
使用下面的任一HTTP方法。
1.PUT
2.DELETE
3.OPTIONS
4.TRACE
5.PATCH
6.CONNECT
请求头包含上述的9个请求头之外的请求头。
Content-Type的值不为上述三个之一。
(4) 简单请求
简单请求客户端和服务器之间使用以下CORS请求头来处理简单跨域请求:
请求头 | 作用 |
---|---|
Origin | 表明请求来源于哪个源,这里为Server-b.com。 |
Access-Control-Allow-Origin | 表明服务器允许哪些源可以访问,这里*表示所有源。 |
(5) 非简单请求
当发送一个非简单请求时,浏览器先发送一个预检请求,当判断服务器允许该实际请求时,才会发送实际的请求。如图:
(6) 预检请求(preflight request)
预检请求其实就是普通的HTTP请求,只不过其方法为OPTIONS,并且携带了两个CORS请求头。
请求头 | 作用 |
---|---|
Access-Control-Request-Method | 告诉服务器我接下来发送的实际的HTTP请求用的是什么方法。 |
Access-Control-Request-Headers | 告诉服务器我接下来发送的实际的HTTP请求发送的是哪些请求头(只会携带那9个基本请求头之外的请求头,如果Content-Type不为规定的3个值时也会一起发送)。 |
当服务当接收到预见请求需要设置CORS响应头返回服务端允许的源、方法、请求头、以及预见请求有效时间。
响应头 | 作用 |
---|---|
Access-Control-Allow-Origin | 设置服务端允许的源,如果值为*表示允许所有源 |
Access-Control-Allow-Methods | 设置服务端允许的方法 |
Access-Control-Allow-Headers | 设置服务端允许的请求头 |
Access-Control-Max-Age | 设置预检请求的有效时间,在有时间内同一个请求不用再发一遍预检请求 |
需要注意的是,预检请求是由浏览器发送的。当浏览器判断你的请求为非简单请求时就会先发送一个预见请求,用以判断服务器是否允许该实际请求。这样就可以避免跨域请求对服务器的用户数据产生未预期的影响。
(7) 携带cookie的请求
非简单请求中有一个特殊的情况,就是当需要发送cookie和HTTP认证消息时,需要前后端都设置跨域请求允许携带cookie和认证消息。
var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
function callOtherDomain(){
if(invocation) {
invocation.open('GET', url, true);
invocation.withCredentials = true;
invocation.onreadystatechange = handler;
invocation.send();
}
}
前端需要将 XMLHttpRequest
的 withCredentials
标志设置为 true,从而向服务器发送 Cookies。后端需要将响应头Access-Control-Allow-Credentials
的值设为true,如果是其他值,浏览器都不会将响应内容返回给请求的发送者。这意味着其实后端是将数据都返回了的,只是浏览器判断Access-Control-Allow-Credentials
的值不为true,故没有将响应内容返回给请求发送者
同时需要注意的是:
后端如果设置了Access-Control-Allow-Credentials的值为true,则必须设置Access-Control-Allow-Origin的值为某一个具体的源,而不能是通配符"*",因为如果允许所有原都可以携带cookie,则判断不了是否为真正的用户发送的请求。一些不良的网站就可以通过跨域请求伪造(CSRF)攻击服务器导致用户信息泄露甚至是财产损失。对于CSRF攻击可以自行百度。
本人会经常更新博客,并在文章附上更新时间! 转载请附上原文出处链接和本声明。
本文链接:https://editor.csdn.net/md/?articleId=108866404
欢迎大家关注我的公众号:frost2