前后端分离项目——登录Token校验思路
前言
根据token校验当前用户登录状态是Web项目的常见手段,我给自己的项目做token校验功能时,发现网上很多文章代码高度相似,实现的思路也差不多(基本都是前端校验后从router入手去做页面拦截)。所以想自己写一篇文章记录一下自己实现的思路,实现功能的前提在于需求,希望能够给相关开发人员一个参考。
需求思考
对token的校验分为前端和后端
- 对后台来说,并不是所有的请求都需要用户登录后才可以执行,所以需要后台去鉴别需要拦截的需求,用户是否满足已登录的状态。
-
对前端来说,也并不是用户没有登录,就一定不允许访问某个页面,可能只是说不允许访问某个页面的某些功能而已。所以
使用router进行登录校验的时候,不要一棒子打死,最好是先确定好想要实现什么样的功能,再去设计代码
。
代码实现思路
- 用户前端登录成功,后台将用户的唯一token存入redis(有效期30min)中,并返回给前端
- 前端接收到token后,将其存放到session缓存,每次发起请求时将token封装到请求头head中
- 后台根据token是否有效,无效则拒绝请求
- 前端返回结果
一、环境介绍
前端: Vue-Cli 2.x + axios
后端:SpringBoot 2.3.4
二、前端代码
1、成功登录后回调函数封装用户token
和userId
(为什么要传userId后面会说)
2、token和userId放在全局参数store中
const user = {
state: {
userId: '',
userToken: '', // 用户token,用户确认当前用户是否登录
},
getters: {
userId: state => {
let userId = state.userId;
if(!userId){
userId = JSON.parse(window.sessionStorage.getItem('userId'));
}
return userId;
},
userToken: state => {
let userToken = state.userToken;
if(!userToken){
userToken = JSON.parse(window.sessionStorage.getItem('userToken'));
}
return userToken;
},
},
mutations: {
setUserId: (state,userId) => {
state.userId = userId;
window.sessionStorage.setItem('userId',JSON.stringify(userId));
},
setUserToken: (state,userToken) => {
state.userToken = userToken;
window.sessionStorage.setItem('userToken',JSON.stringify(userToken));
},
}
}
export default user;
这里的话,userToken和userId放到sessionStorage是关键步骤
3、使用 axios.interceptors.request.use
对axios的请求进行统一拦截,封装token和userId
import axios from 'axios';
import router from '../router';
// 设置请求拦截器
axios.interceptors.request.use(function (config) {
// Do something before request is sent
//window.localStorage.getItem("accessToken") 获取token的value
let token = JSON.parse(window.sessionStorage.getItem('userToken'));
let userId = JSON.parse(window.sessionStorage.getItem('userId'));
if (token && userId) {
//将token放到请求头发送给服务器,将tokenkey放在请求头中
console.log(token);
console.log(userId);
config.headers.userId = userId;
config.headers.userToken = token;
//也可以这种写法
// config.headers['accessToken'] = token;
}
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
三、后端
后端主要是使用拦截器来进行请求的拦截和校验
1、定义拦截器
package com.qiqv.music.controller.interceptor;
import com.qiqv.music.utils.JSONUtils;
import com.qiqv.music.utils.QiqvJSONResult;
import com.qiqv.music.utils.RedisOperator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
/**
* 自定义拦截器类
*/
public class MiniInterceptor implements HandlerInterceptor {
private RedisOperator redisOperator;
// token规则为 user-reids-token:userId : UUID
private static String USER_REDIS_TOKEN = "user-redis-token";
/**
* 判断用户是否登录
* 若用户userId不存在,则为未登录
* 若用户userId存在,则判断token是否存在
* 若存在,则用户状态为已登录
* 若不存在,则用户状态为登录超时
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果是 嗅探请求,则直接放行
if("OPTIONS".equals(request.getMethod())){
return true;
}
String userId = request.getHeader("userId");
String userOldToken = request.getHeader("userToken");
if(StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userOldToken)){
String userTokenKey = USER_REDIS_TOKEN + ":" + userId;
String userToken = redisOperator.getValue(userTokenKey);
// 用户有token,但最新token为空,说明登录状态过期
if(StringUtils.isBlank(userToken)){
returnErrorResponse(response,QiqvJSONResult.noAuth("登录过期,请重新登录"));
return false;
}
// 两个token不一致,可能是恶意用户乱填token
if(!userOldToken.equals(userToken)){
returnErrorResponse(response,QiqvJSONResult.noAuth("无效token,请重新登录"));
return false;
}
}else{
System.out.println("该用户没有登录");
returnErrorResponse(response,QiqvJSONResult.noAuth("请登录后再操作"));
return false;
}
return true;
}
public void returnErrorResponse(HttpServletResponse response, QiqvJSONResult qiqvJSONResult) throws IOException {
OutputStream outputStream = null ;
try {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
outputStream = response.getOutputStream();
outputStream.write(JSONUtils.objectToJson(qiqvJSONResult).getBytes("UTF-8"));
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(outputStream != null){
outputStream.close();
}
}
}
}
解释一下思路:
- 使用userId作为用户登录的唯一key值,UUID作为value。存放在redis中,30min后过期
- 由于请求还未到controller,所以转换结果的时候需要手动转一下json
2、注册拦截器
package com.qiqv.music.config;
import com.qiqv.music.controller.interceptor.MiniInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Bean
public MiniInterceptor miniInterceptor(){
return new MiniInterceptor();
}
/**
* 设置拦截的url路径
* 暂时只针对前端用户评论、收藏、评分功能进行拦截
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry){
List listOfVerify = Arrays.asList("/consumer/**","/rank/rateSongList","/collect/**","/comment/**");
List listOfExc = Arrays.asList("/consumer/login","/consumer/queryUserById","/consumer/getAllConsumer","/collect/getUserCollect","/comment/query**","/comment/allComment");
registry.addInterceptor(miniInterceptor()).addPathPatterns(listOfVerify)
.excludePathPatterns(listOfExc);
super.addInterceptors(registry);
}
}
这里的话,针对需要拦截的路径和需要放行的路径进行配置就行
关于redisTemple的引入这里就不再赘述。
到这里为止,前后端的token就都做完了,后面就再讲讲前端的一些其他思路吧
对于登录状态的判断,前端可以在router.foreach上对路由进行状态判定,从而实现页面程度的拦截(具体可以参考最后的参考文章2)
隐藏的小坑:
跨域问题
在使用拦截器后,会发现前端部分请求会无法正常到达后端,百度后发现是因为axios发送正式请求前会先发送一个嗅探请求
,而嗅探请求是不携带我们封装的header的,所以会导致部分请求会无法成功,解决的方式有很多种,这里的话是选择了在后端去直接处理
参考文章
1、SpringBoot加了拦截器后出现的跨域问题解析
https://blog.csdn.net/mrkorbin/article/details/104066979
2、Vue项目中实现用户登录及token验证
https://www.cnblogs.com/web-record/p/9876916.html