基于网关的统一用户认证
下面我们结合场景讲解 JWT 在微服务架构下的认证过程。这里我将介绍两种方案:
- 服务端自主验签方案;
- API 网关统一验签方案。
服务端自主验签方案
首先咱们来看服务端验签的架构图。
服务端自主验签方案
首先梳理下执行流程:
- 第一步,认证中心微服务负责用户认证任务,在启动时从 Nacos 配置中心抽取 JWT 加密用私钥;
- 第二步,用户在登录页输入用户名密码,客户端向认证中心服务发起认证请求:
http://usercenter/login #认证中心用户认证(登录)地址
- 第三步,认证中心服务根据输入在用户数据库中进行认证校验,如果校验成功则返回认证中心将生成用户的JSON数据并创建对应的 JWT 返回给客户端,下面是认证中心返回的数据样本;
{ "code": "0", "message": "success", "data": { "user": { "userId": 1, "username": "zhangsan", }, "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxLFwidXNlcm5hbWVcIjpcInpoYW5nc2FuXCIsXCJuYW1lXCI6XCLlvKDkuIlcIixcImdyYWRlXCI6XCJub3JtYWxcIn0ifQ.1HtfszarTxLrqPktDkzArTEc4ah5VO7QaOOJqmSeXEM" } }
- 第四步,在收到上述 JSON 数据后,客户端将其中 token 数据保存在 cookie 或者本地缓存中;
- 第五步,随后客户端向具体某个微服务发起新的请求,这个 JWT 都会附加在请求头或者 cookie 中发往 API 网关,网关根据路由规则将请求与jwt数据转发至具体的微服务。中间过程网关不对 JWT 做任何处理;
- 第六步,微服务接收到请求后,发现请求附带 JWT 数据,于是将 JWT 再次转发给用户认证服务,此时用户认证服务对 JWT 进行验签,验签成功提取其中用户编号,查询用户认证与授权的详细数据,数据结构如下所示:
{ "code": "0", "message": "success", "data": { "user": { #用户详细数据 "userId": 1, "username": "zhangsan", "name": "张三", "grade": "normal" "age": 18, "idno" : 130......., ... }, "authorization":{ #权限数据 "role" : "admin", "permissions" : [{"addUser","delUser","..."}] } } }
- 第七步,具体的微服务收到上述 JSON 后,对当前执行的操作进行判断,检查是否拥有执行权限,权限检查通过执行业务代码,权限检查失败返回错误响应。
到此从登录创建 JWT 到验签后执行业务代码的完整流程已经完成。
下面咱们来聊一聊第二种方案:
API 网关统一验签方案
API 网
API 网关统一验签与服务端验签最大的区别是在 API 网关层面就发起 JWT 的验签请求,之后路由过程中附加的是从认证中心返回的用户与权限数据,其他的操作步骤与方案一是完全相同的。
在这你可能又会有疑惑,为什么要设计两种不同的方案呢?其实这对应了不同的应用场景:
服务端验签的时机是在业务代码执行前,控制的粒度更细。比如微服务 A 提供了“商品查询”与“创建订单”两个功能,前者不需要登录用户就可以使用,因此不需要向认证中心额外发起验签工作;而后者是登录后的功能,因此必须验签后才可执行。因为服务端验签是方法层面上的,所以可以精确控制方法是否验签。但也有不足,正是因为验签是在方法前执行,所以需要在所有业务方法上声明是否需要额外验签,尽管这个工作可以通过 Spring AOP+注解的方式无侵入实现,但这也无疑需要程序员额外关注,分散了开发业务的精力。
相应的,服务端验签的缺点反而成为 API 网关验签的优势。API 网关不关心后端的服务逻辑,只要请求附带 JWT,就自动向认证中心进行验签。这种简单粗暴的策略确实让模块耦合有所降低,处理起来也更简单,但也带来了性能问题,因为只要请求包含 JWT 就会产生认证中心的远程通信。如果前端工程师没有对 JWT 进行精确控制,很可能带来大量多余的认证操作,系统性能肯定会受到影响。
那在项目中到底如何选择呢?服务端验签控制力度更细,适合应用在低延迟、高并发的应用,例如导航、实时交易系统、军事应用。而 API 统一网关则更适合用在传统的企业应用,可以让程序员专心开发业务逻辑,同时程序也更容易维护。
全新的挑战
虽然 JWT 看似很美,在实施落地过程中也会遇到一些特有的问题,例如:
JWT 生成后失效期是固定的,很多业务中需要客户端在不改变 JWT 的前提下,实现 JWT 的“续签”功能,但这单靠 JWT 自身特性是无法做到的,因为 JWT 的设计本身就不允许生成完全相同的字符串。为了解决这个问题,很多项目在生成的 JWT 设为“永久生效”,架构师利用 Redis 的 Expire 过期特性在后端控制 JWT 的时效性。这么做虽然让 JWT 本身变得有状态,但这可能也是在各种权衡后的“最优解”。类似的,例如:强制 JWT 立即失效、动态 JWT 有效期都可以使用这个办法解决。
某个 JWT 在 3600 秒后过期
对于上面两种认证方案,还有优化的空间,比如在服务A第一次对某个 JWT 进行验签后获取用户与权限数据,那在 JWT 的有效期内便可将数据在本地内存或者 Redis 中进行缓存,这样下一次同样的 JWT 访问时直接从缓存中提取即可,可以节省大量服务间通信时间。但引入缓存后你也要时刻关注缓存与用户数据的一致性问题,是要性能还是要数据可靠,这又是一个架构师需要面对的抉择。
小结
今天主要涉及三方面内容,首先咱们回顾了基于 Session 的有状态用户认证解决方案,其次介绍了 JWT 与 JJWT 的使用,最后讲解了利用 JWT 实现微服务架构认证的两种方案,对产生的新问题也进行了梳理。
在多年的架构生涯中,我自己也在不断感慨,架构是一门取舍的艺术,没有完美的架构,只有适合的场景,希望未来同学们可以多学习一些前沿技术,兴许随着技术发展没准鱼和熊掌真的可以兼得呢。