微服務下的登錄實現及相關問題解決
最近由於工作需要,需要開發一個登錄的微服務;由於前期在網上找session共享的實現方案遇到各種問題,所以現在回過頭來記錄下整個功能的實現和其中遇到的問題;總結一下主要有以下幾點:
1、登錄實現(整合redis以及用戶信息的共享問題)
2、登錄攔截器的實現及攔截後成功跳轉(這裏踩了一個大坑)
3、登錄過期時間隨用戶的操作而跟新(即當用戶操作時間大於設置的登錄時間時不要讓用戶推出登錄)
4、非掃描類如何使用@autowired成功註入
5、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); }
微服務下的登錄實現及相關問題解決