美文网首页
前后端分离项目——登录Token校验思路

前后端分离项目——登录Token校验思路

作者: moutory | 来源:发表于2020-12-17 09:50 被阅读0次

    前言

    根据token校验当前用户登录状态是Web项目的常见手段,我给自己的项目做token校验功能时,发现网上很多文章代码高度相似,实现的思路也差不多(基本都是前端校验后从router入手去做页面拦截)。所以想自己写一篇文章记录一下自己实现的思路,实现功能的前提在于需求,希望能够给相关开发人员一个参考。

    需求思考

    对token的校验分为前端和后端

    • 对后台来说,并不是所有的请求都需要用户登录后才可以执行,所以需要后台去鉴别需要拦截的需求,用户是否满足已登录的状态。
    • 对前端来说,也并不是用户没有登录,就一定不允许访问某个页面,可能只是说不允许访问某个页面的某些功能而已。所以使用router进行登录校验的时候,不要一棒子打死,最好是先确定好想要实现什么样的功能,再去设计代码

    代码实现思路

    • 用户前端登录成功,后台将用户的唯一token存入redis(有效期30min)中,并返回给前端
    • 前端接收到token后,将其存放到session缓存,每次发起请求时将token封装到请求头head中
    • 后台根据token是否有效,无效则拒绝请求
    • 前端返回结果

    一、环境介绍

    前端: Vue-Cli 2.x + axios
    后端:SpringBoot 2.3.4

    二、前端代码

    1、成功登录后回调函数封装用户tokenuserId(为什么要传userId后面会说)
    image.png
    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 {
    
        @Autowired
        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();
                }
            }
        }
    
    
    }
    
    

    解释一下思路:

    1. 使用userId作为用户登录的唯一key值,UUID作为value。存放在redis中,30min后过期
    2. 由于请求还未到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

    相关文章

      网友评论

          本文标题:前后端分离项目——登录Token校验思路

          本文链接:https://www.haomeiwen.com/subject/nbtggktx.html