zoukankan      html  css  js  c++  java
  • IDEA项目搭建十二——站点用户登录会话实现

    一、简介

    前两天写了一篇用户登录会话设计的脑图,这次就把这个引入到项目中实现,总体来说需要几步先罗列一下:

    1、需要一个Cookie工具类用于读写cookie

    2、需要一个Cache工具类用于在服务端保存用户会话

    3、需要一个UserSession管理类用于操作用户会话的登入与登出等

    4、需要一个BaseController基类来为子类中的controller提供用户会话的使用,比如当前登录的loginUser.getUserId()

    5、需要一个Interceptor拦截器来截获每一次请求来验证是否登录

    6、需要一个再次登入的小逻辑来实现登录后自动跳回访问页的功能,此处分为两类请求:

    (1)是非Ajax请求,这种直接重定向跳回登录,登录成功后再跳回原先的访问页;

    (2)是Ajax请求,这种无法重定向,所以会返回这次请求的HttpStstusCode,配合封装好的ajax函数使用

    上面的6步基本就可以完成这套用户结构了,不多说直接上代码,想看设计方案的看另一个博文吧

    二、代码

    1、Cookie工具类,不粘贴了,基本都哪些东西,很简单

    2、Cache工具类我用的是Redis,不粘贴了,基本都哪些东西,很简单

    3、UserSession管理类,这里我们有两个用户平台一个Admin一个Agent用户会话逻辑都一样,只是参数配置不一样所以建立了一个抽象类来实现主逻辑方法,各自去实现里面的参数配置方法

    (1) UserSessionManage 抽象基类,这里简化为只保存UserId,真正应该还保存一些其他的用户信息只不过我用UserId就代表整体了

    import com.google.common.base.Strings;
    import com.ysl.encryptHelper.RandomOptions;
    import com.ysl.encryptHelper.YSLEncryptTools;
    import com.ysl.ts.common.CookieUtil;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 用户会话管理类
     * @author TaiYongHai
     * @version 20180724
     */
    public abstract class UserSessionManage {
    
        /**
         * 平台Cookie名-User
         */
        protected abstract String cookieNameUser();
    
        /**
         * 平台Cookie名-Token
         */
        protected abstract String cookieNameToken();
    
        /**
         * 平台Cookie名-Series
         */
        protected abstract String cookieNameSeries();
    
        /**
         * 平台加密Key
         */
        protected abstract String encryptKey();
    
        /**
         * 平台加密向量
         */
        protected abstract String encryptVector();
    
        /**
         * 普通登录有效时间(秒)(非记住密码)
         */
        protected abstract int generalValidTime();
    
        /**
         * 特殊登录有效时间(秒)(记住密码)
         */
        protected abstract int specialValidTime();
    
    
        /*
        1、登录成功向Response写入Cookie(非记住密码写User和Token)(记住密码写User、Token和Series)
        2、获取登录Cookie返回用户信息或跳转登录页
        3、退出向Response写入立即过期使Cookie失效
         */
    
        /**
         * 设置用户登录信息到Cookie
         *
         * @param response 响应对象
         * @param userId   用户Id
         * @return
         */
        public boolean setUserToCookies(HttpServletResponse response, int userId) {
            return setUserToCookies(response, userId, null, generalValidTime());
        }
    
        /**
         * 设置用户登录信息到Cookie
         *
         * @param response   响应对象
         * @param userId     用户Id
         * @param userSeries 用户序列
         * @return
         */
        public boolean setUserToCookies(HttpServletResponse response, int userId, String userSeries) {
            return setUserToCookies(response, userId, userSeries, specialValidTime());
        }
    
        /**
         * 设置用户登录信息到Cookie
         *
         * @param response   响应对象
         * @param userId     用户Id
         * @param userSeries 用户序列
         * @param validTime  有效时间(秒)
         * @return
         */
        private boolean setUserToCookies(HttpServletResponse response, int userId, String userSeries, int validTime) {
            boolean result = false;
            try {
                //用户标识Cookie双向加密
                String user = YSLEncryptTools.aesEncrypt(String.valueOf(userId), encryptKey(), encryptVector());//参数1:加密内容,参数2:加密Key,参数3:加密向量;返回加密结果
                CookieUtil.addCookie(response, cookieNameUser(), user, validTime);
                //用户TokenCookie单向加密
                String random = YSLEncryptTools.createRandom(20, RandomOptions.Number);//参数1:随机数长度,参数2:随机结果类型;返回随机数
                String token = YSLEncryptTools.md5Encrypt(random);//参数1:加密内容;返回32位、UTF-8编码的MD5值
                CookieUtil.addCookie(response, cookieNameToken(), token, validTime);
                //用户序列Cookie单向加密
                if (!Strings.isNullOrEmpty(userSeries)) {
                    String series = YSLEncryptTools.md5Encrypt(encryptKey() + userSeries);//返回32位、UTF-8编码的MD5值;返回加密结果
                    CookieUtil.addCookie(response, cookieNameSeries(), series, validTime);
                }
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 从Cookies中获取用户登录信息
         *
         * @param request 当前请求对象
         * @return 0:代表用户未登录,-1:代表用户已在另一处登录,-2:代表用户更新密码需重新登录
         */
        public int getUserOfCookies(HttpServletRequest request) {
            int result = 0; //代表用户未登录
            try {
                //用户标识Cookie双向加密
                String user = CookieUtil.getCookieValue(request, cookieNameUser());
                if (!Strings.isNullOrEmpty(user)) {
                    user = YSLEncryptTools.aesDecrypt(user, encryptKey(), encryptVector());//参数1:解密内容,参数2:加密Key,参数3:加密向量;返回解密结果
                    result = Integer.valueOf(user);
                }
                //用户TokenCookie单向加密
                String token = CookieUtil.getCookieValue(request, cookieNameToken());
                if (!Strings.isNullOrEmpty(token)) {
                    //从SessionCache中获取token进行对比
              //result = -1;//代表用户已在另一处登录
                }
                //用户序列Cookie单向加密
                String series = CookieUtil.getCookieValue(request, cookieNameSeries());
                if (!Strings.isNullOrEmpty(series)) {
                    //从SessionCache中获取token进行对比
              //result = -2;//代表用户更新密码需重新登录
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    
        /**
         * 从Cookies删除用户登录信息
         *
         * @param request 当前请求对象
         * @param response 当前响应对象
         * @return
         */
        public boolean deleteUserOfCookies(HttpServletRequest request, HttpServletResponse response) {
            boolean result = false;
            try {
                CookieUtil.removeCookie(request, response, cookieNameUser());
                CookieUtil.removeCookie(request, response, cookieNameToken());
                CookieUtil.removeCookie(request, response, cookieNameSeries());
                result = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return result;
        }
    }

     (2)AdminUserSessionManage Admin平台的实现类,实现各自的Cookie名称、加密Key、会话持续时间,这里我只提供一个平台的UserSessionManage另一个平台的就不提供了,大部分代码都一样

    /**
     * Admin用户会话管理类
     * @author TaiYongHai
     * @version 20180724
     */
    public class AdminUserSessionManage extends UserSessionManage {
    
        /**
         * Admin平台Cookie名-User
         */
        @Override
        protected String cookieNameUser() {
            return "_admin_u";
        }
    
        /**
         * Admin平台Cookie名-Token
         */
        @Override
        protected String cookieNameToken() {
            return "_admin_t";
        }
    
        /**
         * Admin平台Cookie名-Series
         */
        @Override
        protected String cookieNameSeries() {
            return "_admin_s";
        }
    
        /**
         * Admin平台加密Key
         */
        @Override
        protected String encryptKey() {
            return "admin00000";
        }
    
        /**
         * Admin平台加密向量
         */
        @Override
        protected String encryptVector() {
            return "admin11111";
        }
    
        /**
         * 普通登录有效时间(秒)(非记住密码)
         */
        @Override
        protected int generalValidTime() {
            return 24 * 60 * 60;
        }
    
        /**
         * 特殊登录有效时间(秒)(记住密码)
         */
        @Override
        protected int specialValidTime() {
            return 168 * 60 * 60;
        }
    }
    View Code

      4、BaseController用于给各自平台的Controller当作基类提供当前登录用户信息,这里我只提供一个平台的BaseController另一个平台的就不提供了,大部分代码都一样

    import org.springframework.beans.factory.annotation.Autowired;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * Controller基类Admin
     */
    public class BaseController {
    
        @Autowired
        protected HttpServletRequest request;
    
        @Autowired
        private SysUserService service;
        /**
         * 当前登录用户
         */
        private AdminLoginUserModel loginUser;
    
        /**
         * 获取当前登录用户
         * @return
         */
        public AdminLoginUserModel getLoginUser() {
            AdminUserSessionManage userSession = new AdminUserSessionManage();
            //解析用户cookie
            int userId = userSession.getUserOfCookies(request);
            if (userId > 0) {
    
            }
            //检查Cache中是否存在该信息,不存在去DB中获取
            //do something...代码后续实现
            //获取响应
            ServiceResponse<SysUserModel> res = service.get(new ServiceRequest<>(userId));
            //解析响应数据
            SysUserModel model = ServiceResponse.getResponseData(res);
            //填充登录信息
            model.setId(userId);
            //
            loginUser = new AdminLoginUserModel();
            loginUser.setSysUser(model);
    
            return loginUser;
        }
    } 

    其中Model我就不说了就是用户信息表实体,AdminLoginUserModel就是当前登录用户信息的复合实体(可能登录信息需要不止一张表实体就够了,所以用复合实体可以多张表信息进行填充),ServiceResponse<>和ServiceRequest<>这两个是我们项目约定好使用Eureka与生产者通信用的统一请求响应实体可以忽略不重要。

    5、使用HandlerInterceptor拦截器验证每次请求的用户会话

    import com.ysl.ts.common.userSessionManage.AdminUserSessionManage;
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.net.URLEncoder;
    
    /**
     * 请求拦截器
     */
    public class AdminRequestInterceptor implements HandlerInterceptor {
    
        /**
         * Action执行前调用
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
            try {
                System.out.println("Admin调用拦截器");
                AdminUserSessionManage userSession = new AdminUserSessionManage();
                int userId = userSession.getUserOfCookies(request);
                if (userId > 0) {
                    System.out.println("Admin拦截验证-已登录");
                    return true;//通过验证则返回true允许请求继续执行
                } else {
              // *****此处判断userId可能为0、-1、-2 如果是0代表用户未登录,如果是-1代表用户在另一处登录,如果是-2代表用户更新密码需重新登录,
              //分别判断一下返回不同的页面或返回不同的状态码,给前台的用户进行展示即可,此处直接简写了***** System.out.println(
    "Admin拦截验证-未登录"); //根据header标识判断当前请求是否为Ajax请求 if (request.getHeader("x-requested-with") == null) { //如果不是Ajax请求则重定向 //整理回跳url地址 String backUrl; String url = request.getRequestURL().toString(); String params = request.getQueryString(); backUrl = url + (params == null ? "" : "?" + params); //重定向回登录页 response.sendRedirect("/admin?backUrl=" + URLEncoder.encode(backUrl, "utf-8"));//使用urlencode编码之后传递 } else { //如果是Ajax则更改状态码通知前端 response.setStatus(401);//登录异常(401) //response.setStatus(402);// 已在另一处登录(402)             //response.setStatus(403);// 权限验证不通过(403) } return false;//未通过则返回false结束请求 } } catch (Exception e) { e.printStackTrace(); return false; } } /** * Action执行后,View渲染前调用 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { } /** * View渲染后调用,整个流程执行结束调用 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { } }

     将拦截器注入WebConfig中

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class BaseWebConfig implements WebMvcConfigurer {
    
        /**
         * 注入拦截器
         *
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            //可以注入多个拦截器
            /*
            使用addPathPatterns增加拦截规则,使用excludePathPatterns排除拦截规则
            /admin/**:代表http://域名/admin/** 拦截该目录下的所有目录及子目录
            /admin:代表http://域名/admin 仅拦截此形式访问(无法拦截/admin/ 形式)
            /admin/*:代表http://域名/admin/* 拦截该目录的所有下级目录不包含子目录(可以拦截/admin/ 形式)
             */
            registry.addInterceptor(new AdminRequestInterceptor()).addPathPatterns("/admin/**").excludePathPatterns("/admin").excludePathPatterns("/admin/*").excludePathPatterns("/admin/content/**");
        }
    }
    View Code

     6、后端判断出用户登录后前端需要有连续使用的小逻辑

    (1)非Ajax请求这里拦截器已经获取到backUrl并跳回登录页了,只需要在登录页保存一下这个值,登录成功后跳转到这个url上即可

    (2)Ajax请求则需要统一提供一个js ajax请求函数以便统一做用户判断

    <script type="text/javascript">
        //声明类对象
        var yslCommon = {};
        //对该类的对象声明Ajax函数(保留用户使用JQuery.ajax的习惯)
        //传入一个参数对象,参数与JQuery.ajax一致
        yslCommon.ajax = function (settings) {
            //初始化默认参数设置
            var defaultSettings = {
                type: "post"
            };
            //初始化默认函数(此处增加对整体响应的控制便于判断此次ajax执行是否有效)
            var defaultFunctions = {
                //success请求成功回调方法(参数为返回数据)
                success: function (data) {
                    console.log("defaultSuccess");
                    //直接将回调结果不作任何处理回调给用户传入的参数对象的success()中
                    settings.success(data);
                },
                //error请求错误回调方法(参数为响应对象)
                error: function (res) {
                    console.log("defaultError");
                    //配合后端拦截器做用户会话或权限判断后错误返回不同的HTTP Status Code
                    console.log(res.status);
                    //如果是401代表用户会话失效,如果是402代表用户已在另一处登录,如果是403代表用户无权访问,其他错误直接回调用户传入的参数对象的error()中
                    if (res.status === 401) {
                        alert("用户登录失效请重新登录");//此处后续更改为弹出登录模态框
                    } else if (res.status === 402) {
                        alert("您已在另一处登录,如不是自己请更新密码");
                    } else if (res.status === 403) {
                        alert("用户没有该访问权限");
                    } else {
                        settings.error(res);
                    }
                }
            };
            //先将用户设置参数合并到默认设置中
            $.extend(defaultSettings, settings);
            //再将我们需要控制重写的success()和error()合并到默认设置中
            $.extend(defaultSettings, defaultFunctions);
            //将合并好的设置参数传递到原生JQuery.ajax函数中执行
            $.ajax(defaultSettings);
        }
    </script>

      

     OK,至此这套用户会话解决方案就完成了,如有不足还望指出,谢谢

  • 相关阅读:
    jquery之滚楼
    jquery之仿京东菜单
    jquery之鼠标移动[沸腾京东]
    jquery之飘雪
    jquery之手风琴
    jquery 开始与结束方法 loading窗
    JS对象与数组
    Selenium+Python浏览器调用:Firefox
    Python脚本检查网页是否可以打开
    ubuntu安装pycharm教程
  • 原文地址:https://www.cnblogs.com/taiyonghai/p/9366410.html
Copyright © 2011-2022 走看看