[!tip] 本文没有具体实现过程,只讨论各个登录方式的优缺点。
1、Cookie 和 Session
1.1、Cookie + Session 实现流程:
用户首次登录时:
- 用户访问登录接口
- 服务器验证密码无误后,会创建 SessionId,并将它保存起来。
- 服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。
- 服务器端的 SessionId 可能存放在很多地方,例如:内存、文件、数据库等。
- 第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:
用户访问非登录接口的页面时,会自动带上第一次登录时写入的 Cookie。
服务器端比对 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。
如果一致,则身份验证成功。
1.2、Cookie + Session 存在的问题
虽然我们使用 Cookie + Session 的方式完成了登录验证,但仍然存在一些问题:
- 由于服务器端需要对接大量的客户端,也就需要存放大量的 SessionId,这样会导致服务器压力过大。
- 如果服务器端是一个集群,为了同步登录态,需要将 SessionId 同步到每一台机器上,无形中增加了服务器端维护成本。
- 由于 SessionId 存放在 Cookie 中,所以无法避免 CSRF 攻击。
2、Redis+UUID
上述 cookie+session 最大的问题还是无法满足于分布式系统登录的需求
因为 session 存储在服务端是存储在单点的机器上面,如果需要实现多台集群之间的 session 同步又要涉及很多的一致性的问题得不偿失。
此时的解决方案就是将用户登陆的标识存储在一台速度快,性能好的单独的存储区域,所以 Redis+UUID 这种方案就被广泛使用。
- [4] 原理:使用 UUID 生成全局唯一字符串作为 key,将用户的信息作为 value 存储在 Redis 中
流程如下:
- 访问登录接口时:
校验账号密码,对比通过后生成 UUID 作为 key,将用户信息作为 value 存储在 Redis 中,随后将 UUID 作为token返回给客户端(浏览器) - 校验登录
[!note]+
- 浏览器拿到 UUID 之后,后续的请求在请求头上带上这个 token。
- 请求到后端时触发拦截器校验,校验请求头中是否带有 token,如果没有拦截。
- 拿到 token 之后去 Redis 中取出用户信息,如果 Redis 中没有该 Key 或者已经过期,就进行拦截。
- 取出用户信息之后存入 ThreadLocal 中,作为当前登录用户信息,方便后续使用。
如果是微服务系统,由于网关和业务分离,此时第 2,4 步需要跟改为如下:
[!note]+
2.请求到达网关时触发拦截器拦截校验,校验请求头中是否带有 token,如果没有拦截。
4.校验存在之后,将 Redis 的 key 存放到 head 中(因为网关和业务不在一个微服务,不是一个请求,不是一个线程,不能使用 ThreadLocal 共享信息),
5. 请求到业务微服务触发业务微服务的拦截器,再取出 Redis 中取出信息存储到 ThreadLocal 中
缺点:
[!warning] 不能防止盗刷
Redis+UUID 的最大的缺点之一就是不能防止盗刷。如果我们没有再单独设置限流的情况下,仅使用这种方式实现登录,那么假如说有一个恶意请求多次重复的登录该接口,就会导致 Redis 中存储大量的重复的该用户的信息,导致内存爆满。
[!tip]+ 解决方案
将 Redis 中的存储改为 Hash 类型,将 username 作为 key 将 UUID 作为 value 中的 hashkey 即可解决。
3、JWT
JWT 是现在比较火的一种登录校验方式,
[!note] 登录流程
- 客户端发送登录请求,校验成功之后将用户信息基于私钥和 JWT 生成的 token 字符串返回给前端。
- 后续的请求携带上这个 token 字符串,到达后端拦截器之后会将这个 token 解析出用户信息,如果解析失败或者没有携带,则拦截请求,转接登录界面。
- 如果登录成功则将用户信息存入到 Threadlocal 中。
[!success] 优点:
- 单点登录友好:相较于传统的 Cookie 和 Session 方案,JWT 方式的用户信息被加密在了 token 字符串中,存储在客户端,我们在设计分布式系统的时候不用在意用户登录信息同步问题。
- 由于后端不用存储用户信息,所以盗刷不会给系统造成很大的负担。
[!warning] 缺点:
无状态:后端无法实现 logout 功能,由于登录状态在于前端是否携带 token 字符串,所以后端无法手动登出。在以下几种需要手动登出的情况下,用户依然可以登录。
- 退出登录;
- 修改密码;
- 服务端修改了某个用户具有的权限或者角色;
- 用户的帐户被删除/暂停。
- 用户由管理员注销;
token 的续签问题:
token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?
用户登录返回两个 token :第一个是 acessToken ,它的过期时间 token 本身的过期时间比如半个小时,另外一个是 refreshToken 它的过期时间更长一点比如为 1 天。客户端登录后,将 accessToken 和 refreshToken 保存在本地,每次访问将 accessToken 传给服务端。服务端校验 accessToken 的有效性,如果过期的话,就将 refreshToken 传给服务端。如果有效,服务端就生成新的accessToken 给客户端。否则,客户端就重新登录即可。该方案的不足是:
- 需要客户端来配合;
- 用户注销的时候需要同时保证两个 token 都无效;
- 重新请求获取 token 的过程中会有短暂 token 不可用的情况(可以通过在客户端设置定时器,当 accessToken 快过期的时候,提前去通过 refreshToken 获取新的 accessToken)。
4、Redis+JWT
如果将上述的方案合二为一
[!note] 登录流程
- 登录校验成功之后,将用户信息存储到 Redis 中以 userId 作为 key。
- 然后基于用户信息用 JWT 生成 token 字符串返回给前端
- 拦截器在拦截请求的时候需要判断 token 是否可以解析,然后再判断 Redis 中是否有值。
此时,如果用户盗刷登录接口重复登录的话,由于 Redis 中不能存储相同的 key 所以会抛出异常,而不会将相同用户信息存储多份,不会导致内存内存爆满。
然后因为 Redis 中也存储了用户登录的状态,所以此时我们可以手动将用户的登录信息移除,解决了单独使用 JWT 导致的无法退出登录的问题。