微服務下的登錄實現及相關問題解決


  最近由於工作需要,需要開發一個登錄的微服務;由於前期在網上找session共享的實現方案遇到各種問題,所以現在回過頭來記錄下整個功能的實現和其中遇到的問題;總結一下主要有以下幾點:

  1、登錄實現(整合redis以及用戶信息的共享問題)

  2、登錄攔截器的實現及攔截后成功跳轉(這里踩了一個大坑)

  3、登錄過期時間隨用戶的操作而跟新(即當用戶操作時間大於設置的登錄時間時不要讓用戶推出登錄)

  4、Springboot的自定義異常捕獲(初衷是為了解決2的跳轉問題,最后兜了一大圈)

  下面對上面提到的幾點進行詳細記錄;

一、登錄實現(整合redis以及用戶信息的共享問題)

  這是整個功能的核心所在,由於我們有多個服務所以首要解決的就是session共享的問題,解決這個問題主要是通過redis來實現的,我把登錄成功后對session的操作全部換為對redis的操作,以userId為key,然后將userId返回給前台,在前台需要寫一個common.js來重寫ajax請求,使得每次訪問后台的請求都自動帶上userId,這樣再寫一個攔截器登錄整個登錄功能差不多就實現了;

二、登錄攔截器的實現及攔截后成功跳轉

  首先需要寫一個攔截器的配置類,主要就是將我們自定義的攔截器注冊到項目中以及白名單的添加;下面是注冊攔截器的代碼:

@Configuration
public class LoginInterceptorConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
/**
* LoginInterceptor是自定義的攔截器
* addPathPatterns參數/**是通配符表示攔截所有的請求
* excludePathPatterns方法的參數是可變參數,可以輸n個字符串類型的參數,用來添加白名單
*/
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**").excludePathPatterns("/login/doLogin");
}
}

  然后貼上我登錄攔截器(LoginInterceptor)里的代碼:

package com.huayun.base.interceptors;

import com.huayun.base.entity.UserBean;
import com.huayun.base.exception.BaseErrorCode;
import com.huayun.base.exception.BaseException;
import com.huayun.base.util.RedisUtil;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import redis.clients.jedis.Jedis;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * 登陸攔截器
 *  主要判斷請求中有沒有token以及token有沒有過期
 */
@Component
public class LoginInterceptor extends HandlerInterceptorAdapter {

//    @Autowired
//    private RedisClusterUtil redisUtil;


    public static LoginInterceptor interceptor;

    @PostConstruct
    public void init(){
        interceptor = this;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object token = request.getParameter("token");
        String userId = request.getParameter("userId");
        String url = request.getRequestURI();
        if(isWhiteMenu(url)){   //   /login/logout
            return true;
        }
        if(token == null || userId == null) {
            JSONObject json = new JSONObject();
            json.put("code","10005");
            json.put("msg","登陸驗證失敗,請重新登陸!");
            response.setHeader("Access-Control-Allow-Origin","*");
            response.setContentType("application/json;charset=utf-8");
            response.setStatus(200);
            response.getWriter().write(json.toString());
            return false;
//            throw new BaseException("登陸驗證失敗,請重新登陸",
//                    BaseErrorCode.VALIDATOR_ERROR);
        }
//        UserBean user = null;
        try {
            // 獲取key過期時間
            Long expireTime = RedisUtil.getExpire(userId);
            if(expireTime < 0) { // key不存在則登錄超時
                JSONObject json = new JSONObject();
                json.put("code","10005");
                json.put("msg","登陸超時,請重新登陸!");
                response.setHeader("Access-Control-Allow-Origin","*");
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(200);
                response.getWriter().write(json.toString());
                return false;
            }
            if(expireTime < 60*5) {// 如果過期時間小於5分鍾秒則重置過期時間
                RedisUtil.expire(userId,30*60);
            }
//            user = (UserBean)RedisUtil.get(userId);// redis單機
//            user = (UserBean) RedisClusterUtil.getObject(userId.toString());//redis哨兵
        }catch (Exception ex) {
//            return true;//ruturn true 是為了當redis連接出問題時程序能正常運行,但沒有進行登陸過期的判斷
            ex.printStackTrace();
            throw new BaseException("Redis連接異常",
                    BaseErrorCode.VALIDATOR_ERROR);
        }
//        if(user == null) {
//            JSONObject json = new JSONObject();
//            json.put("code","10005");
//            json.put("msg","登陸超時,請重新登陸!");
//            response.setHeader("Access-Control-Allow-Origin","*");
//            response.setContentType("application/json;charset=utf-8");
//            response.setStatus(200);
//            response.getWriter().write(json.toString());
//            return false;
////            throw new BaseException("登陸超時,請重新登陸",
////                    BaseErrorCode.VALIDATOR_ERROR);
//        }
        return true;
    }

    /**
     * 判斷是否白名單
     * @param url
     * @return
     */
    private boolean isWhiteMenu(String url) {
        if(url.contains("removeLoginParam") || url.contains("setLoginParam") ||
                url.contains("getYwzAndBdz") || url.contains("getYwzAndBdzInfo")
                || url.contains("searchStation") || url.contains("swagger-resources")
                || url.contains("configuration/ui") || url.contains("v2/api-docs")){
            return true;
        }else{
            return false;
        }

    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

 

  這其中攔截成功的跳轉登錄頁我踩了一個大坑,起初很天真的想直接用跳轉和轉發,稍微想下也知道是不可能實現的,因為這完全是跨域的,百度說在response里添加Access-Control-Allow-Origin就可以了,但僅僅添加這個響應頭也是不行的,因為我們是前后端分離的;這其中一個大神給的意見是讓我自定義一個異常,攔截成功后直接拋異常然后進行捕獲統一處理,他自己曾經就這樣試過,然后我就屁顛屁顛的把他的代碼拿來改了,最后的確是成功跳轉了,但后來回看代碼其實成功的關鍵並不在於異常的捕獲然后捕獲成功后的處理;也就是說我不捕獲異常直接在攔截器里寫也是可以成功跳轉的;

  關鍵是跳轉的這段邏輯,由於跨域、前后端分離等原因從后台直接跳轉實現不了,所以換個思路,不直接跳轉,而是給攔截到的請求進行自定義響應,讓ajax的success回調能捕獲到我們返回的登錄被攔截的信息最后從前端跳登錄頁;關鍵代碼如下:

          JSONObject json = new JSONObject();
                json.put("code","10005");
                json.put("msg","登陸超時,請重新登陸!");
                response.setHeader("Access-Control-Allow-Origin","*"); response.setContentType("application/json;charset=utf-8"); response.setStatus(200);
                response.getWriter().write(json.toString());

   標紅的缺一不可,其實起初我也想到了這個實現方法,但是不論怎么寫ajax的回調捕獲到的響應信息都是空,最后發現我們可以自定義請求的http響應碼response.setStatus(200);

   至此,登錄攔截器也基本實現了,接下來說說一些小細節

三、登錄過期時間隨用戶的操作而跟新

  這個bug是給領導演示的時候發現的,我默認redis的key過期時間為30分鍾,那天演示的時候領導一直在操作頁面結果三十分鍾到了,果斷跳到登錄頁了,這下領導不滿意了,你這個怎么這樣呀,不能實時獲取用戶的操作狀態嗎?這樣用戶體驗太差了;我表面點頭稱是其實心里在想哪個用戶像你這樣一點點半個小時的呀,哈哈;話雖這么說bug還是要改掉的;想到的第一個解決方案就是寫監聽器監聽session的狀態,只要監聽到用戶在操作就跟新過期時間,先不說這樣平白無故添加了n次redis的操作,就連監聽器我都實現不了;因為我們是多個服務session沒有實現真正意義上的共享.無法有效監聽;所以后來想到了一個超級簡單的方法,就是在攔截器里獲取過期時間的同時添加一個判斷,當有效時間小於五分鍾時重新更新有效時間,這樣就不會新增無畏的redis操作了。關鍵代碼如下:

       // 獲取key過期時間
            Long expireTime = RedisUtil.getExpire(userId);
            if(expireTime < 0) { // key不存在則登錄超時
                JSONObject json = new JSONObject();
                json.put("code","10005");
                json.put("msg","登陸超時,請重新登陸!");
                response.setHeader("Access-Control-Allow-Origin","*");
                response.setContentType("application/json;charset=utf-8");
                response.setStatus(200);
                response.getWriter().write(json.toString());
                return false;
            }
            if(expireTime < 60*5) {// 如果過期時間小於5分鍾秒則重置過期時間
                RedisUtil.expire(userId,30*60);
            }

 四、Springboot的自定義異常捕獲並返回自定義異常信息

  本來單單是開發一個登錄微服務是不大需要來進行全局異常的捕獲的,但之前由於在登錄攔截成功后始終無法正常跳轉,為了解決這一問題可謂是想盡了辦法,在一位大神的建議下嘗試捕獲全局異常,雖然這樣最后也解決了,但成功解決的根本原因並不在全局異常捕獲而在於給ajax請求的response中sethttp的狀態碼,但功能都實現了所以我也就記錄下springboot環境下如何進行全局異常捕獲

  首頁定義一個異常處理器,使用@ControllerAdvice、@ExceptionHandler注解攔截異常,具體代碼如下:

import com.huayun.base.exception.BaseException;
import com.google.common.collect.Maps;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class BaseExpectionAdvice {

    protected final static String code_ok = "1";
    protected final static String code_fail = "0";

    /**
     * 異常頁面控制
     * BaseException是我們自定義的異常,此方法意為捕獲我們拋出的Base異常並進行統一跳轉處理
   * 如果想捕獲全局異常將BaseException.class改成Exception.class就可以了 *
@param * @return */ @ExceptionHandler(BaseException.class) public String ExceptionHandler(Exception ex, HttpServletResponse resp) { // ex.printStackTrace(); if (ex.getMessage() != null) { System.out.println(ex.getMessage()); // logger.error(ex.toString() + "-" + ex.getMessage()); } if (ex instanceof BaseException) { BaseException be = (BaseException) ex; render(be.getCode(), be.getMessage(), null, resp, HttpURLConnection.HTTP_OK); } else { render(code_fail, "系統錯誤!" + (ex.getMessage() != null ? ex.getMessage() : ""), null, resp, HttpURLConnection.HTTP_BAD_REQUEST); } return null; } public void render(String code, String message, Map<String, Object> dataMap, HttpServletResponse resp, Integer httpStatus) { Map<String, Object> jsonmap = Maps.newHashMap(); jsonmap.put("code", code); jsonmap.put("msg", message); if (dataMap != null) { jsonmap.put("data", dataMap); } String jsonStr = JSON.toJSONString(jsonmap, SerializerFeature.WriteMapNullValue); jsonStr = jsonStr.replaceAll("null", "\"\""); PrintWriter writer = null; try { resp.setHeader("Access-Control-Allow-Origin","*"); resp.setContentType("application/json;charset=utf-8"); // resp.setContentType("text/html;charset=utf-8"); resp.setCharacterEncoding("UTF-8"); resp.setStatus(httpStatus == null ? HttpURLConnection.HTTP_OK: httpStatus); writer = resp.getWriter(); writer.write(jsonStr); } catch (IOException e) { e.printStackTrace(); // logger.error(e.getMessage()); } finally { writer.flush(); writer.close(); } } public void render(String code, String message, Map<String, Object> dataMap, HttpServletResponse resp) { render(code, message, dataMap, resp, null); } }

 BaseException的代碼

  

public class BaseException extends RuntimeException {
    private String code;
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public BaseException() {
        super();
    }

    public BaseException(String message, Throwable cause,
            boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }

    public BaseException(String message, Throwable cause) {
        super(message, cause);
    }

    public BaseException(String message) {
        super(message);
    }

    public BaseException(Throwable cause) {
        super(cause);
    }

    public BaseException(String message, String code) {
        super(message);
        this.message = message;
        this.code = code;
    }

}

 

異常捕獲后拋異常的代碼:
try{
   ... 
}catch(Exception ex){
   throw new BaseException("自定義異常信息","自定義異常狀態碼")  

}

 


免責聲明!

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



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