SpringCloud 微服务认证方案

在微服务开发中中我们首先会通过认证中心获取JWT,然后每次发起后端请求都会将JWT放在请求头中,这时候我们后端需要对这个JWT进行验证判断是否合法及是否有对应请求权限,这一过程主要有两种方案:

  • 服务端自主验签方案
  • API网关统一验签方案

服务端自主验签方案

首先咱们来看服务端验签的架构图。

image.png

首先梳理下执行流程:

  1. 第一步,认证中心微服务负责用户认证任务,在启动时从 Nacos 配置中心抽取 JWT 加密用私钥;
  2. 第二步,用户在登录页输入用户名密码,客户端向认证中心服务发起认证请求:
http://usercenter/login #认证中心用户认证(登录)地址
  1. 第三步,认证中心服务根据输入在用户数据库中进行认证校验,如果校验成功则返回认证中心将生成用户的JSON数据并创建对应的 JWT 返回给客户端,下面是认证中心返回的数据样本;
{

    "code": "0",
    "message": "success",
    "data": {
        "user": {
            "userId": 1,
            "username": "zhangsan",
        },
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJ1c2VySWRcIjoxLFwidXNlcm5hbWVcIjpcInpoYW5nc2FuXCIsXCJuYW1lXCI6XCLlvKDkuIlcIixcImdyYWRlXCI6XCJub3JtYWxcIn0ifQ.1HtfszarTxLrqPktDkzArTEc4ah5VO7QaOOJqmSeXEM"
    }
}
  1. 第四步,在收到上述 JSON 数据后,客户端将其中 token 数据保存在 cookie 或者本地缓存中;
  2. 第五步,随后客户端向具体某个微服务发起新的请求,这个 JWT 都会附加在请求头或者 cookie 中发往 API 网关,网关根据路由规则将请求与jwt数据转发至具体的微服务。中间过程网关不对 JWT 做任何处理;
  3. 第六步,微服务接收到请求后,发现请求附带 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","..."}]
        }
    }
}
  1. 第七步,具体的微服务收到上述 JSON 后,对当前执行的操作进行判断,检查是否拥有执行权限,权限检查通过执行业务代码,权限检查失败返回错误响应。

到此从登录创建 JWT 到验签后执行业务代码的完整流程已经完成。

下面咱们来聊一聊第二种方案:

API 网关统一验签方案

image.png

API 网关统一验签与服务端验签最大的区别是在 API 网关层面就发起 JWT 的验签请求,之后路由过程中附加的是从认证中心返回的用户与权限数据,其他的操作步骤与方案一是完全相同的。

在这你可能又会有疑惑,为什么要设计两种不同的方案呢?其实这对应了不同的应用场景:

  • 服务端验签的时机是在业务代码执行前,控制的粒度更细。
    比如微服务 A 提供了“商品查询”与“创建订单”两个功能,前者不需要登录用户就可以使用,因此不需要向认证中心额外发起验签工作;而后者是登录后的功能,因此必须验签后才可执行。
    因为服务端验签是方法层面上的,所以可以精确控制方法是否验签。 但也有不足,正是因为验签是在方法前执行,所以需要在所有业务方法上声明是否需要额外验签,尽管这个工作可以通过 Spring AOP+注解的方式无侵入实现,但这也无疑需要程序员额外关注,分散了开发业务的精力。
  • 相应的,服务端验签的缺点反而成为 API 网关验签的优势。
    API 网关不关心后端的服务逻辑,只要请求附带 JWT,就自动向认证中心进行验签。这种简单粗暴的策略确实让模块耦合有所降低,处理起来也更简单,但也带来了性能问题,因为只要请求包含 JWT 就会产生认证中心的远程通信。如果前端工程师没有对 JWT 进行精确控制,很可能带来大量多余的认证操作,系统性能肯定会受到影响。

那在项目中到底如何选择呢?

服务端验签控制力度更细,适合应用在低延迟、高并发的应用,例如导航、实时交易系统、军事应用。而 API 统一网关则更适合用在传统的企业应用,可以让程序员专心开发业务逻辑,同时程序也更容易维护。

JWT的挑战

虽然 JWT 看似很美,在实施落地过程中也会遇到一些特有的问题,例如:

  • JWT 生成后失效期是固定的,很多业务中需要客户端在不改变 JWT 的前提下,实现 JWT 的“续签”功能,但这单靠 JWT 自身特性是无法做到的,因为 JWT 的设计本身就不允许生成完全相同的字符串。
    为了解决这个问题,很多项目在生成的 JWT 设为“永久生效”,架构师利用 Redis 的 Expire 过期特性在后端控制 JWT 的时效性。这么做虽然让 JWT 本身变得有状态,但这可能也是在各种权衡后的“最优解”。类似的,例如:强制 JWT 立即失效、动态 JWT 有效期都可以使用这个办法解决。
    image.png
  • 对于上面两种认证方案,还有优化的空间。
    比如在服务A第一次对某个 JWT 进行验签后获取用户与权限数据,那在 JWT 的有效期内便可将数据在本地内存或者 Redis 中进行缓存,这样下一次同样的 JWT 访问时直接从缓存中提取即可,可以节省大量服务间通信时间。但引入缓存后你也要时刻关注缓存与用户数据的一致性问题,是要性能还是要数据可靠,这又是一个架构师需要面对的抉择。

架构是一门取舍的艺术,没有完美的架构,只有适合的场景!