AJAX 跨域访问
明确跨域问题产生的原因
首先了解一个概念:浏览器的同源策略
AJAX同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
同源的定义
如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
当我们使用前端通过浏览器对后台接口发起请求的时候,浏览器判断我们的请求的目标接口所在主机和本机的协议、域名(ip)、端口号是否相同,当三者有一个不同时,发生请求跨域错误这就是我们所说的请求跨域问题。
参照如下:
我们以http://origin.test/home
为源,源向下列URL发起请求的结果进行对比:
注:源的协议为http,域名为origin.test
,端口号为80。
目标URL | 结果 | 原因 |
---|---|---|
http://target.test/home | 失败 | 协议、端口号相同,域名不同,不满足三要素同时成立的条件。域名不同导致浏览器判断其来自不同主机。 |
http://origin.test:8081/home | 失败 | 协议、域名相同,端口号不同,不满足三要素同时成立的条件。源的端口号为默认端口80。 |
https://origin.test/home | 失败 | 域名、端口号相同,协议不同,不满足三要素同时成立的条件。源的协议为http,目标URL的协议为https。(在自测接口的时候需要注意此点,有时浏览器会默认为我们加上https) |
https://origin.test/index/page.html | 成功 | 满足域名、端口号、协议同时相同。 |
https://origin.test/home/user.html | 成功 | 满足域名、端口号、协议同时相同。 |
跨域问题的解决
在JavaEE的开发中,我们通过在Sping的配置类写入如下代码得以避免跨域问题:
/**
* 跨域配置
* @return
*/
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
//重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//放行哪些原始域
.allowedOriginPatterns("*")
//是否发送Cookie信息
.allowCredentials(true)
//放行哪些原始域(请求方式)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
//放行哪些原始域(头部信息)
.allowedHeaders("*")
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Header1", "Header2");
}
};
}
主要是在干一件事:为API所在的服务器设置允许哪些Origin服务器访问自己——.allowedOriginPatterns("*")
项目中遇见的问题
在前后端分离项目中,设置拦截器(Interceptor)实现登录拦截,LoginInterceptor内容如下:
注:拦截策略配置中,拦截的范围是/**
。
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod targetMethod = (HandlerMethod) handler;
if (!targetMethod.hasMethodAnnotation(CheckToken.class)){
return true;
}
String token = TokenUtil.getToken(request);
if (token == null) {
throw new LoginException("未登录,请登录后再操作。");
}
UserInfo userInfo = redisService.getUserByToken(token);
if (userInfo == null) {
redisService.removeToken(token);
throw new LoginException("登录超时,请重新登录。");
}
return true;
}
}
不用看拦截器preHandle方法中代码具体的内容,我们的问题是,在配置了拦截器后,前端对后端的接口发起请求,出现了跨域错误,且相同的请求浏览器发起了2次,两次请求的类型都不同,如下:
因为前端发起的是Ajax请求,类型是xhr
,但浏览器发起的相同的请求是preflight
类型,且点入Request发现其请求方式是OPTIONS
,所以猜测其访问的并不是Controller的方法。
解决方案
解决此问题的方法是:在拦截器中加上判断handler类型是否不属是HanlderMethod的代码,如果不属于HanlderMethod,说明其不是要访问我们的控制器方法,直接将其放行。
if (!(handler instanceof HandlerMethod)) {
return true;
}
修改后的LoginInterceptor拦截器如下:
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisService redisService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果是预请求,则放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod targetMethod = (HandlerMethod) handler;
if (!targetMethod.hasMethodAnnotation(CheckToken.class)){
return true;
}
String token = TokenUtil.getToken(request);
if (token == null) {
throw new LoginException("未登录,请登录后再操作。");
}
UserInfo userInfo = redisService.getUserByToken(token);
if (userInfo == null) {
redisService.removeToken(token);
throw new LoginException("登录超时,请重新登录。");
}
return true;
}
}
浏览器判断跨域的具体步骤
君子不器。既然解决了问题,我们就要明白其中的原理,不应拘泥于手段而不思考其背后的目的。
浏览器发起请求会用按照如下顺序顺序进行操作:
- 发起请求前,将目标URL的协议、域名、端口号和源依据同源对比策略进行对比(同源对比策略的演示上面已经阐述的很清楚)。
- 若同源,则直接发送数据请求,获取响应内容,操作完成。
- 若不同源,判断此AJAX请求为一个跨域请求:
- 此时浏览器会再发起一个OPTIONS请求,类型是preflight,其他的内容目标请求一样,确认目标URL所在服务器是否允许源所在服务器进行跨域访问,这个OPTIONS请求仅仅是为了确认是否允许跨域,不会真正访问Controller中的方法。
- 如果目标URL所在服务器不进行任何响应,则判断目标服务器不允许跨域,出现跨域错误。
- 如果目标URL所在服务器响应的内容是不允许当前服务器进行跨域,则判断目标服务器不允许跨域,出现跨域错误。
这就是我们解决方案的为什么要判断handler类型是否不属是HanlderMethod的原因。