无状态token和前端拦截器介绍
一:什么是有状态
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。传统项目中,在用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
当登录数量过大,就需要我们新增服务器了。但是利用tomcat提供的session共享存在着不小的问题,缺点是什么?
-
每台服务端都保存大量数据,增加服务端压力
-
服务端保存用户状态,无法进行水平扩展
-
客户端请求依赖服务端,多次请求必须访问同一台服务器
二:无状态token
由于前后端分离项目无法使用Session作为前后端数据传递,所以无状态token就是为了取代Session的作用而出现【非前后端分离项目也可以使用无状态token】,无状态token就是完全抛弃Session,使用Redis来替代,相当于自己实现Session方案,前后端传递token需要手动写代码传递,不能像Session一样自动传递,我们在做登录的时候,后端接收到前端传过来的数据在数据库中进行对比,对比成功后通过UUID生成一个随机的token值,将这个token作为key,将查询出来的对象作为value存入Redis,并设置过期时间为30分钟,然后将这个token和对象通过map或者对象的形式返回给前端,前端拿到对象进行数据回显,拿到token通过axios的请求拦截器加在请求头中,每次发送请求都携带token,后端通过这个token到Redis中取值,判断当前状态,从而实现了Session一样的功能,这就是无状态token
三:axios请求拦截器【前置拦截器】
请求拦截器在每次发送axios请求之前执行,它会拦截一切axios请求,可以将token通过请求拦截器加在请求头中再发送请求到后端
//===============================axios请求拦截器【发请求前将token加到请求头】===============================
axios.interceptors.request.use(res=>{
let token = localStorage.getItem("token");
if(token){
res.headers["token"] = token;
}
return res;
},error => {
Promise.reject(error)
})
四:axios响应拦截器【后置拦截器】
响应拦截器在每次后端响应后执行,它主要负责对后端响应的数据作出对应的处理,比如:未登录状态下请求后端接口被后端mvc拦截器拦截后响应给前端json数据,前端的响应拦截器获取到后跳转页面至登录界面
//===============================axios响应拦截器===============================
axios.interceptors.response.use(res => {
//没登陆或者过期
if (false === res.data.success || "noLogin" === res.data.msg) {
localStorage.removeItem("token");
localStorage.removeItem("logininfo");
router.push({path: '/login'});
}
return res;
},error => {
Promise.reject(error)
})
五:路由拦截器
路由拦截器在每次发送前端请求后执行,一般作为权限校验,比如未登录的状态下访问前端其他界面,路由拦截器就会进行拦截,作为访问受限资源使用
//===============================路由拦截器【静态资源拦截器】===============================
router.beforeEach((to, from, next) => {
//跳转登录注册页面清空localStorage
if (to.path == '/login' || to.path == "/register") {
localStorage.removeItem("token");
localStorage.removeItem("logininfo");
next();//放行
}else{
//判断是否登录
let logininfo = localStorage.getItem('logininfo');
if (logininfo) {
next();
} else {
next({path: '/login'});//跳转到login
}
}
})
六:Springboot后端SpringMVC拦截器
SpringMVC拦截器与SSM框架使用的拦截器是一样的,区别在于由于Springboot框架抛弃了xml的配置方式,需要使用类的形式进行配置拦截内容
拦截器类【负责拦截逻辑处理】:
package cn.ybl.basic.interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;
/**
* @Author Mr.Yang
* @createTime 2022/7/30 14:49
* @Describe 登录拦截器
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的数据
String token = request.getHeader("token");
if(token!=null){
//根据请求头key在Redis中取值
Object logininfo = redisTemplate.opsForValue().get(token);
if(logininfo!=null){ //说明没过期
//重新设置过期时间
redisTemplate.opsForValue().set(token,logininfo,30, TimeUnit.MINUTES);
//放行
return true;
}
}
//请求头中没有数据或者登录信息过期了==============进行拦截
//手动拼接一个json数据返回给前端
response.setContentType("application/json;charset=utf8");
response.getWriter().write("{\"success\":false,\"msg\":\"nologin\"}");
return false;
}
}
拦截器配置类【负责配置拦截内容】:
package cn.ybl.basic.interceptor.config;
import cn.ybl.basic.interceptor.LoginInterceptor;
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;
/**
* @Author Mr.Yang
* @createTime 2022/7/30 15:38
* @Describe 拦截器配置类【配置拦截的请求】
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
//拦截所有请求
.addPathPatterns("/**")
//放行登录请求
.excludePathPatterns("/logins/**")
//放行注册请求
.excludePathPatterns("/register/**")
//放行fastdfs请求
.excludePathPatterns("/fastDfs/**")
//放行图形验证码请求
.excludePathPatterns("/verifyCode/**")
//放行店铺入驻
.excludePathPatterns("/shop/settlement")
//放行swagger
.excludePathPatterns("//swagger-resources/**\", \"/webjars/**\", \n" +
" \"/v2/**\", \"/swagger-ui.html/**");
}
}
八:静态资源拦截器
用在没有使用路由的界面,拦截不经过后端的请求,限制访问前端受限资源
//====前端拦截器:拦截不经过后端代码的请求【前端的有些页面是需要登录之后才能访问的】====//
//获取当前请求的url
let url = location.href;
//访问受限资源————如果不是登录页面并且也不是注册页面【为了测试:假如index.html是需要登录之后才能访问的】
if(url.indexOf("login.html")==-1 && url.indexOf("register.html")==-1){
let logininfo = localStorage.getItem("logininfo");
if(!logininfo){//如果访问的不是登录页面和注册页面,并且loginInfo没有值【没有登录或过期】
location.href = "../login.html";
}
}