目录
cookie出现之前
cookie出现之前,又有http请求是无状态的,用户第一次和服务器连接后并且登录成功,第二次请求服务器依然不知道当前请求是哪个用户。初始时,这种模式很happy的,因为web就是文档的浏览,每一次请求就是一个新的请求,不需要记录谁刚刚发了请求,谁浏览了什么文档。
cookie
随着网络的发展,网站不再是谁都可以使用,而需要登录。那这样就需要管理回话,必须记住谁登录了系统,谁往自己的购物车放了商品。所以上面的模式已经不再适合,而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和节点取余,这样用户就会一直访问初始访问的那台服务器。
优点
- 配置简单,对引用无入侵性。
缺点
- 如果挂了一个节点,那么这个节点重启时保存的用户session-id就会全部丢失;
- 而且水平扩展过程中也会造成部分session丢失;
- 存在单点负载高德风险;如果公司访问外网的ip只有一个,那么公司有多少人,就会有多少人请求ip一样,那么最后就会怼到一台tomcat服务器
服务器session复制
通过服务器之间同步数据,实现每天服务器上都是全部数据,这样无论用户被负载到哪台服务器,都可以找到自己的session id
优点
- 对应用无侵入性,不需要修改代码
- 能适应各种负载均衡策略
- 服务器重启或宕机不会造成session丢失
- 安全性较高
缺点
- session同步会有一定的延时
- 占用内网宽带
- 受制于内存资源,水平扩展能力差:session存储于内存,也就是每个节点都存储了所有的会话信息,高并发会产生很多会话,最后内存都不够存会话信息,那么就会限制水平扩展能力
- 序列化反序列化消耗cpu性能:同步是要走网络的,走网络过来是要变成java对象的,那么就会有序列化和反序列化
所以这种方式适合小型集群规模,例如3-5台服务器。
session统一缓冲
session统一缓冲就是把session id 集中存储到一个地方,比如redis或memcached, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是增加了单点失败的可能性, 要是那个负责session 的机器挂了, 所有人都得重新登录一遍,所以redis也得搞一个集群出来增加可靠性,但是,这样的话就因为一个小小的session搞出来一个集群,好像不太划算吧。
优点
- 能适应各种负载均衡策略
- 能适应各种负载均衡策略
- 服务器重启或宕机不会造成session丢失
- 安全性较高
- 扩展能力强
- 适合集群数据量大时使用
缺点
- 对应用有入侵性,需要增加相关配置:比如引入jar包,增加配置
- 增加一次网络开销,用户体验降低:因为用户需要访问缓存
- 序列化反序列化消耗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实现思路
-
用户登录校验,校验成功后就返回Token给客户端。
-
客户端收到数据后保存在客户端
-
客户端每次访问API时携带Token到服务器端。
-
服务器端采用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是现在的主流趋势。