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,至此這套用戶會話解決方案就完成了,如有不足還望指出,謝謝

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM