文章目录
前后端分离基本认识
- 前后端分离,一般是不会用cookie的,基本上都是提交用户凭证,后端返回一个token,前端每次请求,都应当将这个token的请求头发送给后台,让后台确认是哪个用户在操作。
前后端分离请求跨域问题
- 前后端分离,一般都涉及到跨域的问题,即当前浏览器发起该次请求的url所在的域与当前页面所在的域(协议、ip、端口任一不一致,视为跨域)。前端vue项目一般有两种解决方式。
- 一个是在开发的时候,配置一个devServer代理,这个代理其实就是开发时vue项目启动时监听指定端口的服务,这个服务会像网关一样,比如当遇到以/api开头的,就会将原本发给该服务的请求转发到指定的服务器,就相当于后台请求后台,这个时候是没有浏览器的同源策略的限制的,所以不涉及到跨域,但这也只仅限开发的时候,这样去用。
- 另一个可以直接在axios中,配置请求的地址,这个时候,可以使用cors处理跨域问题。浏览器在发起一个请求前,发现此次请求涉及到跨域,那么就要按照跨域的处理来玩,比如说,如果是个简单请求,那么直接把请求发给后台,后台需要返回cors所规定的一些与跨域相关的响应头,以告知浏览器如何正确的处理这次跨域请求(也就是说,从这里就可以看出来,浏览器是会把跨域请求给发出去的,请求也确确实实到了后台服务器,至于后台有没有处理这个请求,或者说有没有进controller里面的方法,那就要看springmvc它是如何决策的,或者springmvc是否能支持修改能不能到controller方法)。如果后台,没有设置跨域相关的响应头,那么浏览器会认为,这次的跨域请求没有获得后台服务的允许,就会把错误打印在控制台上。如果后台返回了跨域相关的相应头,那么浏览器就会把服务器响应的数据,给到js处理。这也就是说,跨域问题仅仅是浏览器的问题,浏览器是为了保证安全问题。当然如果是复杂请求,在发送真实请求前,会发送一个预检请求,只有预检请求通过了,浏览器才会发送真实的请求,那么springmvc肯定也有处理预检请求的逻辑。
- 在项目打包上线时,可以使用nginx,因为请求都是浏览器发给nginx,一个路径专门到vue项目,一个路径专门转发到后台,对于浏览器来说,都是直接发给nginx这一个服务,所以自然就不存在跨域问题了。
cookie的基本认识
那么现在就说到这个cookie的问题,它的本质就是一个请求头(Cookie)和响应头(Set-Cookie),只不过这个头会被默认携带给后台。
- 具体来说:只要你的浏览器没有禁用cookie,它会“自动”携带本域名下的所有cookie到后台,不区分端口(这是浏览器的默认行为,浏览器的规范),cookie最开始是后台返回给前端的,然后浏览器会保存下来,然后每次发送请求前,浏览器都会检查当前要发送请求的域名有没有保存下来的cookie,有的话就把它带过去,没有的话,那就不带。cookie默认情况下,关了浏览器就删了。后台也可以设置删除cookie的响应给前端,设置某个cookie的maxAge=0,那浏览器收到这个响应,就把这个cookie给删了,当然,你也可以手动用js删除指定的某个cookie
跨域 + cookie
那么,现在再来看下,一个新的问题,刚刚说到cookie在前后端不分离的情况下,cookie默认就是刚刚说的这样玩的,但是如果和跨域问题一起出现的话,又会怎样呢?浏览器它还会携带cookie吗?能携带,但是需要正确的配置它!
WebMvcConfigurer
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
// 处理的请求匹配路径
.addMapping("/**")
// 预检请求能够被客户端缓存多久
.maxAge(3600)
// 是否允许客户端携带凭证,会设置到Access-Control-Allow-Credentials跨域响应头中
.allowCredentials(true)
// 允许的域,会设置到Access-Control-Allow-Origin跨域响应头中
.allowedOrigins("*")
// 允许的请求方式,可以参考DefaultCorsProcessor和CorsConfiguration
.allowedMethods("*")
// 允许浏览器请求携带的请求头
.allowedHeaders("x-token")
// 允许浏览器获取的响应头
.exposedHeaders("token","Authorization")
;
}
}
CorsRegistration
跨域的配置可以看下这个类,里面的解释很清楚,处理逻辑,可以参考DefaultCorsProcessor这个类
public class CorsRegistration {
private final String pathPattern;
private final CorsConfiguration config;
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
// Same implicit default values as the @CrossOrigin annotation + allows simple methods
this.config = new CorsConfiguration().applyPermitDefaultValues();
}
/**
* The list of allowed origins that be specific origins, e.g.
* {@code "https://domain1.com"}, or {@code "*"} for all origins.
* <p>A matched origin is listed in the {@code Access-Control-Allow-Origin}
* response header of preflight actual CORS requests.
* <p>By default, all origins are allowed.
* <p><strong>Note:</strong> CORS checks use values from "Forwarded"
* (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>),
* "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" headers,
* if present, in order to reflect the client-originated address.
* Consider using the {@code ForwardedHeaderFilter} in order to choose from a
* central place whether to extract and use, or to discard such headers.
* See the Spring Framework reference for more on this filter.
*/
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
/**
* Set the HTTP methods to allow, e.g. {@code "GET"}, {@code "POST"}, etc.
* The special value {@code "*"} allows all methods.
* <p>By default "simple" methods, i.e. {@code GET}, {@code HEAD}, and
* {@code POST} are allowed.
*/
public CorsRegistration allowedMethods(String... methods) {
this.config.setAllowedMethods(Arrays.asList(methods));
return this;
}
/**
* Set the list of headers that a preflight request can list as allowed
* for use during an actual request. The special value {@code "*"} may be
* used to allow all headers.
* <p>A header name is not required to be listed if it is one of:
* {@code Cache-Control}, {@code Content-Language}, {@code Expires},
* {@code Last-Modified}, or {@code Pragma} as per the CORS spec.
* <p>By default all headers are allowed.
*/
public CorsRegistration allowedHeaders(String... headers) {
this.config.setAllowedHeaders(Arrays.asList(headers));
return this;
}
/**
* Set the list of response headers other than "simple" headers, i.e.
* {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
* {@code Expires}, {@code Last-Modified}, or {@code Pragma}, that an
* actual response might have and can be exposed.
* <p>Note that {@code "*"} is not supported on this property.
* <p>By default this is not set.
*/
public CorsRegistration exposedHeaders(String... headers) {
this.config.setExposedHeaders(Arrays.asList(headers));
return this;
}
/**
* Whether the browser should send credentials, such as cookies along with
* cross domain requests, to the annotated endpoint. The configured value is
* set on the {@code Access-Control-Allow-Credentials} response header of
* preflight requests.
* <p><strong>NOTE:</strong> Be aware that this option establishes a high
* level of trust with the configured domains and also increases the surface
* attack of the web application by exposing sensitive user-specific
* information such as cookies and CSRF tokens.
* <p>By default this is not set in which case the
* {@code Access-Control-Allow-Credentials} header is also not set and
* credentials are therefore not allowed.
*/
public CorsRegistration allowCredentials(boolean allowCredentials) {
this.config.setAllowCredentials(allowCredentials);
return this;
}
/**
* Configure how long in seconds the response from a pre-flight request
* can be cached by clients.
* <p>By default this is set to 1800 seconds (30 minutes).
*/
public CorsRegistration maxAge(long maxAge) {
this.config.setMaxAge(maxAge);
return this;
}
protected String getPathPattern() {
return this.pathPattern;
}
protected CorsConfiguration getCorsConfiguration() {
return this.config;
}
}
AdminController
@RestController
@RequestMapping("admin")
public class AdminController {
@PostMapping("login")
public Result<UserEntity> login(@RequestBody LoginDto loginDto, HttpSession session, HttpServletResponse response) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(loginDto.getUsername());
// 写出session,其实就是写出了一个名为Set-Cookie的响应头JSESSIONID=xxx给了前端
session.setAttribute("user", userEntity);
response.setHeader("token","123456");
response.setHeader("Authorization", "halo");
// 写出这个响应头给前端,但是浏览器不会把这个响应头给到js
response.setHeader("auth", "auth123");
return Result.ok(userEntity);
}
@GetMapping("getUserInfo")
public Result<UserEntity> getUserInfo(HttpServletRequest request) {
// 如果之前已经存在session(浏览器需要携带上次设置的cookie过了),那就获取(如果浏览器都没带,那就没得商量了),如果不存在,也不创建新的session
HttpSession session = request.getSession(false);
if (session != null) {
Object user = session.getAttribute("user");
return Result.ok(user);
} else {
System.out.println("啥也没有");
return Result.ok(null);
}
}
}
request.js
import axios from 'axios'
const instance = axios.create({
baseURL: 'http://localhost:8083',
timeout: 60000,
withCredentials: true /* 需要设置这个选项,axios发送请求时,才会携带cookie, 否则不会携带 */
})
// Add a request interceptor
instance.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
// Add a response interceptor
instance.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
console.log('收到响应',response);
return response.data.data;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
export default instance
loginApi.js
import request from '@/utils/request'
export function login(data) {
return request({
url:'/admin/login',
method: 'post',
data
})
}
export function getUserInfo() {
return request({
url:'/admin/getUserInfo',
method: 'get'
})
}
App.vue
<template>
<div class="container">
<el-button @click="doLogin">登录</el-button> {{ userInfo }}
<el-button @click="doGetUserInfo">获取用户信息</el-button>
</div>
</template>
<script>
import {login,getUserInfo } from '@/api/loginApi'
export default {
components: {
TalkItem
},
data() {
return {
userInfo: {}
}
},
methods: {
async doLogin() {
let result = await login({username:'zzhua',password:'123456'})
this.userInfo = result
console.log('登录成功',result);
},
async doGetUserInfo() {
let userInfo = await getUserInfo()
console.log('获取用户信息',userInfo);
}
}
}
</script>