业务场景

在单机系统中,用户登陆之后,服务端会保存用户的会话信息,只要用户不退出重新登陆,在一段时间内用户可以一直访问该网站,无需重复登陆。用户的信息存在服务端的
session 中,session中可以存放服务端需要的一些用户信息,例如用户ID,所属公司companyId,所属部门deptId等等。




但是随着业务的发展,技术架构需要调整,原来的单机系统逐渐被更换,架构由单机扩展到分布式,甚至当下流行的微服务。虽然在用户端看来系统仍然是一个整体,但在技术端来说业务则被拆分成多个模块,各个模块之间相互独立,甚至不在同一台物理机器上,模块之间通过
RPC 进行通信。



那么原来单机只需一份的 session,
如何满足在多系统的运行下保证会话一致性呢?单独保存在任何一个系统中都不合适,而且每个单独模块系统也可能是分布式形式的,是由集群组成。那么session的分配就更复杂了。

Redis 实现


针对以上问题,我们可能会从以下几个方面想到解决的方法,每个服务端存储一份,通过同步的方式保证一致性,但是这种方式有个很明显的缺点:session的同步需要数据传输,占内网带宽,有时延,网络不稳定的时候会造成部分系统同步延迟,那么就不能保证
session 一致性。而且所有服务端都包含所有session数据,数据量受内存限制,无法水平扩展。

那么我们是否可以单独将 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。
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丢失的情况发生。

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信