session一致性问题及token实现

目录

cookie出现之前

session

概述

工作原理

集群session丢失

session一致性问题解决方法

基于IP-hash处在均衡

服务器session复制

session统一缓冲

三种session一致性问题解决方法的适用情况

session的不足

token

token的实现原理

token实现思路

实践


cookie出现之前

cookie出现之前,又有http请求是无状态的,用户第一次和服务器连接后并且登录成功,第二次请求服务器依然不知道当前请求是哪个用户。初始时,这种模式很happy的,因为web就是文档的浏览,每一次请求就是一个新的请求,不需要记录谁刚刚发了请求,谁浏览了什么文档。

随着网络的发展,网站不再是谁都可以使用,而需要登录。那这样就需要管理回话,必须记住谁登录了系统,谁往自己的购物车放了商品。所以上面的模式已经不再适合,而cookie的出现,就是为了解决回话需要管理的情况。

第一次登录后服务器返回一些数据(cookie)给浏览器,然后浏览器保存在本地,当该用户发送第二次请求的时候,就会自动的把上次请求存储的cookie数据自动的携带给服务器,服务器通过浏览器携带的数据就能判断当前用户是哪个了。

但是cookie仅仅是浏览器实现的一种数据存储功能。而且cookie存储的数据量有限,不同的浏览器有不同的存储大小,但一般不超过4KB。因此使用cookie只能存储一些小量的数据。如果Cookie很多,这无形地增加了客户端与服务端的数据传输量,而Session的出现正是为了解决这个问题。

session

概述

同一个客户端每次和服务端交互时,不需要每次都传回所有的Cookie值,而是只要传回一个会话标识(SessionId),这个ID是客户端第一次访问服务器的时候生成的,而且每个客户端是唯一的。这样每个客户端就有一个唯一的ID,客户端只要传回这个ID就行了,这个ID通常是NAME为JSESIONID的一个Cookie。在Web服务器上,各个会话独立存储保存不同会话的信息。如果遇到禁用Cookie的情况,一般的做法就是把这个会话标识放到URL的参数中。

session和cookie的作用有点类似,都是为了存储用户相关的信息。不同的是,cookie是存储在本地浏览器,而session存储在服务器。存储在服务器的数据会更加的安全,不容易被窃取。

工作原理

session是由tomecat产生的,保存在tomcat本地的ConcurrentHashMap中,以sessionID为key。

用户产生会话时,会向浏览器发送sessionId,浏览器会保存在cookie,再次请求时,会携带sessionId,这样tomcat就可以sessionId的验证,追踪到请求是属于哪个session的哪个用户的。

另外session不是在用户登录时候产生的,session表示会话,会话是用来跟踪多个请求的,在调用request.getsession时才会产生session,登录只是明确会话的主人是谁。

集群session丢失

这样就解决了cookie的不足,但是这样也有弊端,就是服务器需要保存所有的session id,占用服务器的资源,如果服务器多了,还会出现数据不一致的问题。

比如现在有两台服务器,用户A初次登陆负载均衡到了服务器A,session id就保存在服务器A上,如果用户A下一次请求被负载均衡到了服务器B,服务器B可是没有用户A的session id啊,那咋办?

session一致性问题解决方法

基于IP-hash处在均衡

如果负载均衡策略是ip-hash,那么就可以由服务器ip和节点取余,这样用户就会一直访问初始访问的那台服务器。

优点

  1. 配置简单,对引用无入侵性。

缺点

  1. 如果挂了一个节点,那么这个节点重启时保存的用户session-id就会全部丢失;
  2. 而且水平扩展过程中也会造成部分session丢失;
  3. 存在单点负载高德风险;如果公司访问外网的ip只有一个,那么公司有多少人,就会有多少人请求ip一样,那么最后就会怼到一台tomcat服务器

服务器session复制

通过服务器之间同步数据,实现每天服务器上都是全部数据,这样无论用户被负载到哪台服务器,都可以找到自己的session id

优点

  1. 对应用无侵入性,不需要修改代码
  2. 能适应各种负载均衡策略
  3. 服务器重启或宕机不会造成session丢失
  4. 安全性较高

缺点

  1. session同步会有一定的延时
  2. 占用内网宽带
  3. 受制于内存资源,水平扩展能力差:session存储于内存,也就是每个节点都存储了所有的会话信息,高并发会产生很多会话,最后内存都不够存会话信息,那么就会限制水平扩展能力
  4. 序列化反序列化消耗cpu性能:同步是要走网络的,走网络过来是要变成java对象的,那么就会有序列化和反序列化

所以这种方式适合小型集群规模,例如3-5台服务器。

session统一缓冲

session统一缓冲就是把session id 集中存储到一个地方,比如redis或memcached, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了, 所有人都得重新登录一遍,所以redis也得搞一个集群出来增加可靠性,但是,这样的话就因为一个小小的session搞出来一个集群,好像不太划算吧。

优点

  1. 能适应各种负载均衡策略
  2. 能适应各种负载均衡策略
  3. 服务器重启或宕机不会造成session丢失
  4. 安全性较高
  5. 扩展能力强
  6. 适合集群数据量大时使用

缺点

  1. 对应用有入侵性,需要增加相关配置:比如引入jar包,增加配置
  2. 增加一次网络开销,用户体验降低:因为用户需要访问缓存
  3. 序列化反序列化消耗cpu性能:需要经过网络传输

三种session一致性问题解决方法的适用情况

session的不足

session虽然解决了请求辨别的问题,session复制和基于ip_hash的方式都会但用户越来越多时,造成内存开销,而且session是存储于服务器内存的,伴随而来的就是扩展性差。而session统一缓冲的方式,会增加维护压力。

那么有没有一种方式,服务端不需要保存session,还可以实现用户验证呢?token的实现解放了服务端需要保存session的压力。

token

token的实现原理

用户登录,发一个令牌token,客户端保存,里边包含用户的userId,所以这个token是经过加密的,下一次用户通过http访问的时候,只需要把这个token携带过来,服务端解密,获取token里边的userId验证,就知道这个用户是否已经登录过了。

这样一来,服务端就不需要保存session id了,也就解决了由于session保存,而引起的一系列不足。

token实现思路

  1. 用户登录校验,校验成功后就返回Token给客户端。

  2. 客户端收到数据后保存在客户端

  3. 客户端每次访问API时携带Token到服务器端。

  4. 服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码

实践

在用户登录校验成功后,生成token,返回给客户端

........
校验成功的代码省略
........

//生成token
String jwtToken = JwtTokenUtil.generate(userModel.getId(), userModel.getUserCode(), userModel.getSchoolNo(), jwtTtl);
            //向redis中存入Token信息queryProjectRoleUser/{companyId}/{projectId}

 redisTemplate.opsForValue().set(redisTokenKey.concat(userCode), jwtToken, redisTokenTtl,SECONDS);

 LoginTokenModel tokenModel = new LoginTokenModel();
 tokenModel.setId(userModel.getId());
 tokenModel.setUserCode(userModel.getUserCode());
 tokenModel.setUserName(userModel.getUserName());
 tokenModel.setCompanyId(userModel.getCompanyId());
 tokenModel.setSchoolNo(userModel.getSchoolNo());
 //将token放入model,返回给前端
 tokenModel.setToken(jwtToken);
          
 itooResult = new ItooResult("0000", "用户登录成功!", tokenModel);
 return itooResult;

客户端再次访问时,携带token,通过拦截器拦截进行验证。

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        boolean boolAccess = false;
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String userCode = "";

        try {
            //从request中获取token
            String jwtToken = httpServletRequest.getHeader("Authorization");
            if (!StringUtils.isBlank(jwtToken)) {
                Claims claims = JwtTokenUtil.getClaims(jwtToken);
                //获取token中的userId
                userCode = claims.getSubject();
                String redisToken = (String)this.redisTemplate.opsForValue().get(this.redisTokenKey + userCode);
                //判断redis中存储的token和request中的token是否一致,如果一致验证通过
                if (!StringUtils.isBlank(redisToken) && redisToken.equals(jwtToken)) {
                    boolAccess = true;
                    TenancyContext.setContextValue(TenancyContext.CONTEXT_TYPE_TENANCY_ID, claims.getAudience());
                    TenancyContext.setContextValue(TenancyContext.CONTEXT_TYPE_USER_ID, claims.getId());
                    long compareTime = JwtTokenUtil.compareTime(claims.getIssuedAt());
                    if (compareTime > (long)(this.redisTokenTtl - this.diffTokenTtl)) {
                        this.redisTemplate.expire(this.redisTokenKey + userCode, (long)this.redisTokenTtl, TimeUnit.MILLISECONDS);
                    }
                }
            }

            //如果不一致,返回401,权限验证失败
            if (!boolAccess) {
                this.authenticate(response, userCode);
            }
        } catch (Exception var11) {
            logger.error(userCode + "权限认证出错!", var11);
            this.authenticate(response, userCode);
        }

        return boolAccess;
    }

    private HttpServletResponse authenticate(ServletResponse response, String userCode) {
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setStatus(401);
        this.redisTemplate.delete(this.redisTokenKey + userCode);
        return httpServletResponse;
    }

这样就实现了使用token辨别用户,而且token是现在的主流趋势。

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子松的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值