本文中主要使用springboot项目做后端,Vue做前端,前后端使用axios实现数据交互,前后端分别部署情况:
项目 | 部署域名 |
---|---|
springboot后台项目 | http://localhost:8081 |
vue前端项目 | http://localhost:8080 |
由于前后端部署在两个不同端口下就涉及到了跨域概念。
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制,这里引用了一篇文章详细的讲解了CROS的内部机制,非常详细,请参阅:
跨域资源共享 CORS 详解
qs 是一个增加了一些安全性的查询字符串解析和序列化字符串的库。
QS官方文档
安装方式
npm i qs -save
在使用的模块中引入
import qs from "qs";
主要方法示例:
var obj = qs.parse('a=c');
//输出 { a: 'c' }
qs.parse('foo[bar]=baz');
//输出
//foo: {
// bar: 'baz'
//}
qs.parse('foo[bar][baz]=foobarbaz');
//输出
//foo: {
// bar: {
// baz: 'foobarbaz'
// }
//}
qs.parse('a=b;c=d', { delimiter: ';' });
//输出 { a: 'b', c: 'd' }
//等等还有好多使用方法请参照官方文档
qs.stringify({ a: 'b' })
//输出 a=b
qs.stringify({ a: { b: 'c' } })
//输出 a%5Bb%5D=c
qs.stringify({ a: { b: 'c' } }, { encode: false })
//输出 a[b]=c
qs.stringify({ a: ['b', 'c', 'd'] });
//输出 'a[0]=b&a[1]=c&a[2]=d'
qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
//输出 'a=b&a=c&a=d'
如果axios不会安装,请参阅上一篇文章在新建项目中引用axios章节
import axios from "axios";
// 覆写库的正式环境的路径,开发环境及测试环境设置请求到/api
//process.env.NODE_ENV === 'production' 生产环境
//process.env.NODE_ENV === 'development' 开发环境
//process.env.NODE_ENV === 'test' 测试环境
axios.defaults.baseURL = process.env.NODE_ENV === 'production' ? "http://localhost:8081" :"/api";
// 现在,在超时前,所有请求都会等待 30 秒
axios.defaults.timeout = 30000;
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
//在此处设置请求发送前的各种配置信息,具体congfig内容参见axios文档http://www.axios-js.com/zh-cn/docs/
// 在发送请求前配置token请求头
//config.headers['token'] = "tokenaxcs234";
//开启跨域携带cookie
config.withCredentials=true;
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
//var token = response.headers.token;
//console.log(token)
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
这里特别强调一下process.env.NODE_ENV变量
在vue-cli3.0中的process.env.NODE_ENV变量说明
使用vue-cli3构建的项目就简单多了,因为vue-cli3使用DefinePlugin方式帮把process.env.NODE_ENV配置好了,我们不需要再自己去配置。
它自带了三种模式:
development:在vue-cli-service serve下,即开发环境使用
production:在vue-cli-service build和vue-cli-service test:e2e下,即正式环境使用
test: 在vue-cli-service test:unit下使用
如果低版本vue-cli需要自己在电脑中配置一下环境变量,程序中才能读取到process.env.NODE_ENV的值。可以参考下面文章:
process.env.NODE_ENV详解
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import "./plugins/element.js";
import axios from "axios";
import VueAxios from "vue-axios";
//此处引入了上步建立的httpconfig模块,其他部分代码可以忽略
import "@/util/httpconfig.js"
Vue.use(VueAxios, axios);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
module.exports = {
//生产环境配置在/production-sub-path/子路径下,开发环境则部署在根路径下,请按照正式路径修改生产环境
//publicPath: process.env.NODE_ENV === 'production'
// ? '/production-sub-path/'
//: '/',
devServer: //设置开发环境代理访问路径
proxy: {
'/api': {
// 此处的写法,目的是为了 将 /api 替换成 http://localhost:8081 测试环境的路径
target: 'http://localhost:8081',
secure: false, // 如果是https接口,需要配置这个参数
// 允许跨域
changeOrigin: true,
ws: true,//是否代理websockets
pathRewrite: {
'^/api': ''
}
}
}
}
}
关于vue.config.js文件配置详情请参阅官方文档。
通过对第二章节的学习,针对跨域的内部机制已经有了了解,springboot后端实现跨域就是在HttpServletResponse中配置响应头:
- Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。如果要接受前端传入的cookie必须返回与前端传入的Origin字段一致的域名,不能返回星号。
- Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
另外java web的会话(session)保持是使用的cookie中JSESSIONID字段,前端cookie中向后端发送cookie:JSESSIONID={value},其中value为session的id,后端会自动将此次请求与指定会话id的会话进行关联保持账户登录。
所以如果前端要与后端java web保持会话可以使用cookie的方式,如果前端禁用cookie时也可以使用url重写方式保持会话。关于会话保持可以通过点击下面链接进行了解。
javaweb会话保持
上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。
Access-Control-Allow-Credentials: true
另一方面,开发者必须在AJAX请求中打开withCredentials属性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
xhr.withCredentials = false;
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
- Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
看一个案例来理解Access-Control-Expose-Headers的作用:
基于token的认证(基于token的会话保持机制)
基于token的认证机制将认证信息返回给客户端并存储。下次访问其他页面,需要从客户端传递认证信息回服务端。简单的流程如下:
客户端使用用户名跟密码请求登录;
服务端收到请求,去验证用户名与密码;
验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端;
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token;
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据;
基于token的验证机制,有以下的优点:
支持跨域访问,将token置于请求头中,而cookie是不支持跨域访问的;
无状态化,服务端无需存储token,只需要验证token信息是否正确即可,而session需要在服务端存储,一般是通过cookie中的sessionID在服务端查找对应的session;
无需绑定到一个特殊的身份验证方案(传统的用户名密码登陆),只需要生成的token是符合我们预期设定的即可;
更适用于移动端(Android,iOS,小程序等等),像这种原生平台不支持cookie,比如说微信小程序,每一次请求都是一次会话,避免CSRF跨站伪造攻击,还是因为不依赖cookie;
那么vue+springboot项目中的实现方式就是这样的,
vue前端第一次请求后端项目在请求头中不带token信息自动跳转到登录页面,登录成功后后端要在Response头Access-Control-Expose-Headers指定token字段。
Access-Control-Expose-Headers=token
然后向响应头中添加token信息
response.addHeader("token","token123456abc");
vue客户端中axios的响应操作中可以使用获得响应头token信息
var token=response.headers.token
然后将token存储到vuex或者本地localstore中,下次请求时将token信息添加到请求头中。
对于springmvc中使用过滤器或者拦截器配置响应头的几个坑需要注意
如果直接在controller中使用HttpServletResponse.addHeader方法配置响应头可以向前端正确输出,没有问题。
如果使用Filter过滤器实现响应头的添加,HttpServletResponse.addHeader方法必须写在filterChain.doFilter(servletRequest,servletResponse)方法之前,如果写在filterChain.doFilter之后将不能正确输出,因为filterChain.doFilter执行就已经向前端输出响应了。
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//Filter中添加响应头的正确位置,写在filterChain.doFilter之前
((HttpServletResponse)servletResponse).addHeader("token","token123456abc");
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//spring中Interceptor中添加响应头的位置,需要写在preHandle方法中,且返回true
response.addHeader("token","token123456abc");
return true;//返回true放行 返回falser 拦截
}
Access-Control-Allow-Methods
非简单请求时正式请求前会有一个预检请求,会包含本字段。
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
直接配置响应头:
Access-Control-Allow-Methods: GET, POST, PUT
或配置CorsFilter:(其实都是在影响头中进行配置)
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
可以一次性将Allow-Methods允许的方法全部配置,避免多次预检。
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
在不确定前端会传入何种字段是可以设置为星号,允许前端传入任何字段。Access-Control-Allow-Headers字段只有配置了前端Access-Control-Request-Headers中传入的字段,才允许正式跨域请求。
小说明:
Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例中针对基于token的验证机制中后端响应头中输出的token字段,就可以在前段请求头中使用Access-Control-Request-Headers字段进行配置
该字段与简单请求时的含义相同,允许发送cookie。
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
好了通过以上的回顾,正式进入springboot的几种跨域实现方式
springboot跨域实现可以从控制的粗细力度上区分有三种模式,一种控制到具体某一api请求接口的方式(@CrossOrigin),另一类型是SpringBoot全局配置方式、最后一种是Nginx中配置响应头的方式。
Spring Framework 4.2 GA为CORS提供了第一类支持,使您比通常的基于过滤器的解决方案更容易和更强大地配置它。所以springMVC的版本要在4.2或以上版本才支持@CrossOrigin。
@CrossOrigin注解可以加载controller类上也可以加载具体某一 @RequestMapping、@GetMapping、@PostMapping标注的方法上,可以细粒度的去控制某一请求API是否支持跨域。
@CrossOrigin(origins = "http://localhost:63342", methods = {GET, POST, PUT, DELETE}, maxAge = 60L)
@RequestMapping(value = "/test/cros", method = {OPTIONS, GET})
public Object testCros() {
return "hello cros";
}
@CrossOrigin注解中相关字段值可以进行对应设置。@CrossOrigin注解既可以加在方法上也可以加载类上。
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
/*
* 表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中
* 需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。
* 同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,
* 且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
*/
config.setAllowCredentials(true);
//允许跨域访问的域名设置
config.addAllowedOrigin("http://localhost:8080");
config.addAllowedHeader("*");
//该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
config.addExposedHeader("token");
//该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒)
config.setMaxAge(1728000l);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
将上段代码加载任何一个@Configuration标注的配置类中即可。
自定义Filter:
// 自定义一个Filter来处理CORS跨域请求
@Component
public class CORSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
// TODO:这里应该是只需要处理OPTIONS请求即可~~~
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "content-type,Authorization");
response.setHeader("Access-Control-Allow-Credentials", "true");
//允许加入的自定义响应头
response.setHeader("Access-Control-Expose-Headers", "token");
response.addHeader("token","token123456abc");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
上面代码是自定义的一个CORSFilter,它实现了接口Filter。在doFilter方法中针对跨域响应头的设置必须写在filterChain.doFilter之前,写在之后由于filterChain.doFilter方法执行后就向前端输出响应,将不起任何作用。
另外需要强调一下,自定义的Filter有三种方式可以加入容器。
@Bean
public FilterRegistrationBean registerFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
//拦截URL模式
bean.addUrlPatterns("/*");
//注册自定义的Filter
bean.setFilter(new CookieFilter());
return bean;
}
如果存在多个自定义Filter需要在@Configuration标注的配置类中注册多个FilterRegistrationBean,也就是上段代码需要重复写多次,每一个将方法张拦截模式和注册的Filter进行修改。
自定义的Interceptor:
@Component
public class CrosInterceptor implements HandlerInterceptor {
//在controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "content-type,Authorization");
response.setHeader("Access-Control-Allow-Credentials", "true");
//允许加入的自定义响应头
response.setHeader("Access-Control-Expose-Headers", "token");
response.addHeader("token","token123456abc");
return true;//返回true放行 返回falser 拦截
}
//在controller执行之后,跳转页面之前执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
//所有操作完毕之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
这里再次强调一下,spring拦截器中注册响应头必须写在preHandle方法中,如果写在postHandle方法中将无效,因为到postHandle方法执行时,响应输出给客户端了。
自定义拦截器还需要注册到WebMvcConfigurer配置文件中:
@Configuration
public class configation implements WebMvcConfigurer {
@Autowired
CrosInterceptor crosInterceptor;
//注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加自定义拦截器
registry.addInterceptor(crosInterceptor)
//拦截器拦截那些路径
.addPathPatterns("/apis/home");
//不拦截的路径
// .excludePathPatterns("");
}
}
#
# Wide-open CORS config for nginx
#
location / {
#### 对OPTIONS请求,会设置很多的请求头,并返回204
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
}
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/apis/home")
// -------addMapping后还可以继续配置-------
.allowedOrigins("http://localhost:8080")
.maxAge(1728000l);
registry.addMapping("/**").allowedOrigins("*");
}
}
等价的xml的方式表达:
<mvc:cors>
<mvc:mapping path="/test/cros" ... />
<mvc:mapping path="/**" ... />
mvc:cors>
对于spring实现跨域几种方式的内部机理需要细致研究的可以参考下面文章的最后一部分。CORS跨域资源共享(三)