1.背景
随着互联网的飞速发展和移动终端的广泛普及,互联网用户的体量越来越大,日活几亿的app层出不穷,无论是互联网服务还是app服务,对于服务器的高并发要求越来越高,服务器高性能、高可用越来越重要,所以分布式微服务架构开发的项目越来越多。我们往往会用多台服务器去部署不同的业务模块,比如电商项目中,可能购物车系统、订单系统、视频种草模块、秒杀系统等等都会在不同的服务器单独部署,微服务项目的优势主要体现在:
1.高并发:很多人同时访问这个服务器,由于不同模块分布在不同服务器,单个服务器只需要运行单一功能模块。对服务器的CPU以及内存的压力自然就减小了很多。
2.高可用:全年365天每天24小时随时可以访问,不能因为个别服务器的异常,导致整个项目瘫痪。比如某一个服务器宕机,但是其他功能还能正常运行,这也方便了我们日常的维护和升级优化,服务之间低耦合,互不影响。
3.高性能:当有用户访问时响应的速度要尽量的快,即使并发高,也要快速响应,单独模块的处理响应速度肯定要优于所有功能都缠绕在一台服务器的速度。
2.单系统登录时代的“两张票”
因为http协议时无状态的协议,所以在早期单一服务器单系统架构时,我们使用Cookie和Session两种机制来存储状态。
Cookie,有时也用其复数形式Cookies。类型为“小型文本文件”,是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息。我们可以把cookie想象成一张可以打孔的简单票据,比如你定的矿泉水票,每次送来一桶水就打一个孔,这个票据是在客户那里存放的。它的优势就是体积很小,可以存在客户端不用占用服务器资源,但是也有致命的缺点,保存信息非常有限只有几Kb,保存在客户端每次发请求时携带,不能保证其安全性。
Session,俗称会话,在计算机中,尤其是在网络应用中,称为“会话控制”。Session对象存储特定用户会话所需的属性及配置信息。这样,当用户在应用程序的Web页之间跳转时,存储在Session对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。当服务端需要保存客户的某种状态时,例如服务器需要知道发起请求的是谁,加入购物车的是什么东西,是谁付的款。这就是session。在服务器上,我们可以把这种状态数据(Session)存在缓存、或持久化到数据库中。而在客户端,和这Session对应的,只有是cookie中的身份标识。我们可以把Session想象成你去理发店办的会员卡,这个会员卡一般在理发店放着,你每次过去的时候报一下ID,它们用电脑帮你打下卡,证明你来过一次。
写了一个简单的登录Demo 把Service层的逻辑贴出来看一下传统的Session保存。
demo代码和图示如下:
@Service
public class Demo1Impl implements Demo1 {
@Autowired
private DemoMapper demoMapper;
@Override
public JsonResult login(UserLoginDTO userLoginDTO, HttpSession httpSession) {
UserLoginVO userLoginVO = demoMapper.selctByUsername(userLoginDTO.getUsername());
if (userLoginVO != null) {
if (userLoginVO.getPassword().equals(userLoginVO.getPassword())) {
//得到当前客户端对应的会话对象
httpSession.setAttribute(userLoginVO.getUsername(), userLoginVO);
return JsonResult.ok();
}
return JsonResult.fail(ServiceCode.ERR_FORBIDDEN,"密码错误");
}return JsonResult.fail(ServiceCode.ERR_NOT_FOUND,"用户名不存在");
}
public JsonResult logOut(HttpSession session,UserLoginDTO userLoginDTO){
//删除会话对象中的user
session.removeAttribute(userLoginDTO.getUsername());
return JsonResult.ok();
}
public UserLoginVO currentUser(HttpSession session,UserLoginDTO userLoginDTO){
UserLoginVO userLoginVO = (UserLoginVO) session.getAttribute(userLoginDTO.getUsername());
//如果没有登录返回的是null
return userLoginVO;
}
}
3.问题
这也给我们带来了一系列的问题,其中一个就是用户的登录状态问题,为了避免出现用户在一个功能模块登录后,访问其他模块时还需要重新登录的问题,我们就不得不引入单点登录(single sign on SSO)的模式来实现记录用户登录状态的功能,这样用户只需要在一个模块登录后,访问其他模块就无需重复登录了。
4.实现
目前常用的实现用户登录状态记录的有三种方式:
4.1 Web_Cookie(共同父域下的单点登录)
对内的统一登录:如果我们在一个主网站登录成功了,我们在此网站的不同功能模块中自然就处于登录状态了,比如你登录了淘宝网,那么无论你是打开某个商品详情,或是打开购物车,或者是打开自己的订单页面查看物流等信息,这时的我们都是登录状态的。使用相同父域名的服务群,可以利用cookie的特性之--允许一个子域可以设置或获取其父域的 Cookie。cookie中保存的数据,不论是表明了用户信息的JWT,还是SessionId,我们可以让所有服务都得到这一标志。记得给cookie设HTTPOnly 属性来防止跨站脚本攻击。
4.2 Redis_Token
@Service
public class Demo1Impl implements Demo1 {
@Autowired
private RedisTemplate<String,Serializable> redisTemplate;
@Autowired
private DemoMapper demoMapper;
@Override
public JsonResult login(UserLoginDTO userLoginDTO) {
UserLoginVO userLoginVO = demoMapper.selctByUsername(userLoginDTO.getUsername());
if (userLoginVO != null) {
if (userLoginVO.getPassword().equals(userLoginVO.getPassword())) {
String token= UUID.randomUUID().toString();
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
ops.rightPush(token, userLoginDTO);
redisTemplate.expire(token,10,TimeUnit.MINUTES);//设置10分钟有效期
return JsonResult.ok(token);
}
return JsonResult.fail(ServiceCode.ERR_FORBIDDEN,"密码错误");
}return JsonResult.fail(ServiceCode.ERR_NOT_FOUND,"用户名不存在");
}
public JsonResult logOut(UserLoginDTO userLoginDTO){
//删除会话对象中的user
redisTemplate.delete(userLoginDTO.getToken());
return JsonResult.ok();
}
public Serializable currentUser(UserLoginDTO userLoginDTO) {
if(userLoginDTO.getToken()==null) return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED,"请先登录");
ListOperations<String, Serializable> ops = redisTemplate.opsForList();
Serializable serializable = ops.index(userLoginDTO.getToken(), 1);
UserLoginDTO u = (UserLoginDTO) serializable;
if (serializable==null)return JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED_DISABLED,"登录超时,请重新登陆");
//4.返回要访问的资源.
return u;
}
}