在單機系統中,用戶登錄以後,服務端會保存用戶的會話信息,只要用戶不退出從新登錄,在一段時間內用戶能夠一直訪問該網站,無需重複登錄。用戶的信息存在服務端的 session 中,session中能夠存放服務端須要的一些用戶信息,例如用戶ID,所屬公司companyId,所屬部門deptId等等。java
可是隨着業務的發展,技術架構須要調整,原來的單機系統逐漸被更換,架構由單機擴展到分佈式,甚至當下流行的微服務。雖然在用戶端看來系統仍然是一個總體,但在技術端來講業務則被拆分紅多個模塊,各個模塊之間相互獨立,甚至不在同一臺物理機器上,模塊之間經過 RPC 進行通訊。web
那麼原來單機只需一份的 session, 如何知足在多系統的運行下保證會話一致性呢?單獨保存在任何一個系統中都不合適,並且每一個單獨模塊系統也多是分佈式形式的,是由集羣組成。那麼session的分配就更復雜了。redis
針對以上問題,咱們可能會從如下幾個方面想到解決的方法,每一個服務端存儲一份,經過同步的方式保證一致性,可是這種方式有個很明顯的缺點:session的同步須要數據傳輸,佔內網帶寬,有時延,網絡不穩定的時候會形成部分系統同步延遲,那麼就不能保證 session 一致性。並且全部服務端都包含全部session數據,數據量受內存限制,沒法水平擴展。spring
那麼咱們是否能夠單獨將 session 信息存儲在某一個獨立的介質中,介質能夠是DB也能夠是緩存。數據庫
考慮到以下業務:登錄的時候咱們常常會給用戶一個過時時間(通常移動端常設置爲7天或者一個月甚至更久),到期後用戶須要輸入登錄信息從新登錄,即會話過時。這種到期的設置咱們天然想到了Redis的 key expire功能,因此最終咱們能夠將Redis引入進來實現咱們的這種需求。系統以下圖所示:緩存
咱們只需在用戶首次登錄的時候將用戶信息放到 Token並緩存到 Redis 中,同時設置一個過時時間,僞代碼以下:網絡
@Override
public Map login(UserDto dto) {
Map<String, Object> restMap = new HashMap<>();
// 校驗登錄信息
User user = checkLoginInfo(dto);
//刪除舊的token
String token = (String) redisUtils.get(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName());
if (!ObjectUtils.isEmpty(token)) {
redisUtils.delete(CacheConstants.USER_TOKEN_KEY_WEB + token);
}
// 惟一簽名信息
String signStr = user.getCompanyId() + user.getUserName() + dto.getPassword() + DateUtils.now().getTime();
token = MD5Utils.md5(signStr);
// 設置用戶 token
redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_WEB + token, user.getId(), LOGIN_EXPIRED_TIME);
//緩存新的token
redisUtils.setExpiredAt(CacheConstants.USER_TOKEN_KEY_COPY + user.getUserName(), token, LOGIN_EXPIRED_TIME);
dto.setCompanyId(user.getCompanyId());
dto.setId(user.getId());
restMap.put("token", token);
restMap.put("userName", user.getUserName());
return restMap;
}
複製代碼
那麼在系統中如何使用呢,咱們能夠定義一個攔截器 SessionInterceptor
,當訪問 web 接口的時候檢驗用戶的 token 信息,判斷用戶是否登錄,未登陸的狀況下一些業務接口是沒法訪問的,以及在登錄的狀況下拿到咱們須要的用戶信息,如 userId。session
public class SessionInterceptor {
@Autowired
private RedisUtils redisUtils;
@Autowired
private UserService userService;
@Pointcut("execution(* com.jajian.demo.web.*.controller.*.*(..)) && @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void controllerMethodPointcut() {
}
@Around("controllerMethodPointcut()")
public Object Interceptor(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Signature signature = proceedingJoinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
if (targetMethod.getDeclaringClass().isAnnotationPresent(NoLogin.class) || targetMethod.isAnnotationPresent(NoLogin.class)) {
return proceedingJoinPoint.proceed();
}
// 從獲取RequestAttributes中獲取HttpServletRequest的信息
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
Log.debug("驗證token", "token驗證失敗,{}", "token不存在");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
Integer userId= (Integer)redisUtils.get(CacheConstants.USER_TOKEN_KEY_WEB + token);
if (null == userId) {
Log.debug("驗證token", "token驗證失敗,{}", "token超時");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
User user = userService.getById(userId.longValue());
if (ObjectUtils.isEmpty(user)){
Log.debug("驗證token", "token驗證失敗,{}", "用戶信息不存在");
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
if (user.getStatus() == UserStatusEnum.NO.getCode() || user.getDeleteFlag() == DeleteFlagEnum.YES.getCode()){
Log.debug("驗證token", "token驗證失敗,用戶信息異常 userName : {}, status : {},deleteFlag : {}", user.getUserName(),user.getStatus(), user.getDeleteFlag());
throw new FieldException(Constants.LOGIN_ERROR_CODE, "login.session.timeout");
}
return proceedingJoinPoint.proceed();
}
}
複製代碼
以上實現方式簡單易用,並且Redis 在分佈式系統中的使用率也很高,因此無需額外的技術引入。能夠支持水平擴展,數據庫或緩存水平切分便可,服務端重啓或者擴容都不會有session丟失的狀況發生。架構
我的公衆號:JaJianapp
歡迎長按下圖關注公衆號:JaJian!
按期爲你奉上分佈式,微服務等一線互聯網公司相關技術的講解和分析。