小Cの已经记不起来的博客

微服务架构下 鉴权体系 的设计与权衡

最近把手头一个跑了三四年的单体应用往微服务上拆,业务逻辑迁移都还算顺利,结果在鉴权这块卡了好几天。单体的时候一个 Session 走天下,登录之后用户信息往 ThreadLocal 一塞就完事了,拆完之后发现事情完全不对:用户在服务 A 登录了,服务 B 压根不认识你是谁。网上搜了一圈,大部分文章要么无脑吹 JWT 无状态多好多好,要么就是画一堆架构图讲“集中式认证 vs 去中心化认证”,看完还是不知道自己项目里该用哪个。这里记录一下我踩的坑和最后落地的方案,不一定是最优解,但至少是实测跑通的。

单体时代的老办法为什么不灵了

原来单体的鉴权非常朴素:用户登录,服务端生成 Session(后来量大了挪到 Redis),浏览器带个 Cookie,每次请求过来查一下就完事。

拆成微服务之后,这套东西的问题就全暴露了:

  1. Session 在单个服务的内存里,服务 B 根本读不到服务 A 存的东西
  2. 就算把 Session 全放 Redis 让大家共享,每个服务都得写一遍“读 Cookie → 查 Redis → 反序列化用户 → 塞上下文”的逻辑,重复代码写到吐
  3. 前端还要接小程序和 App 的话,Cookie 这套用起来更别扭

所以鉴权逻辑必须收拢到一处,这个大方向没什么争议,争议在于收到哪儿、收到什么程度。

方案一:网关统一鉴权,用户信息透传

最直觉的做法:所有请求先过网关,网关负责认证,通过之后把用户 ID、角色这些信息塞到请求头里转发给下游,下游服务直接从头里读。

我一开始觉得挺完美,直到有一次本地联调,同事直接绕过网关用 curl 调了我一个服务,随手编了个 X-User-Id: 1 的头,就把我登录态模拟出来了。当时才意识到:下游服务是在无条件信任网关的。正常流量走网关没问题,但只要内网有任何一台机器能直连服务端口,伪造个 header 就进去了。内部服务裸奔这事,平时没事,出事就是大事。

网关侧大致是这样(Spring Cloud Gateway):

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          filters:
            - RemoveRequestHeader: X-User-Id   # 先删掉外部伪造的头
            - name: AuthFilter                  # 再由过滤器验 token、塞入可信的用户头

注意那个 RemoveRequestHeader,这一行很关键,先把外部传进来的同名头删掉,再塞可信的值,不然等于没防。

方案二:JWT,以及它被吹过头的部分

JWT 的卖点是“无状态”:签发之后服务端不用存,任何持有公钥的服务本地验个签就完事,看起来完美。

但真用起来,最经典的问题马上就来了:没法主动失效。

网上常见的补救是搞个黑名单:token 进来先查一遍 Redis。听起来有道理,但你仔细想想——这不就是把状态又存回服务端了吗?每个 token 都要查一次 Redis,和 Session 方案的开销有什么本质区别?还额外搭上签名的 CPU 和更肥的请求头。当然也不是说完全没价值,黑名单里只存“异常注销”的那一小部分,正常过期的不用管,量是可控的。但这时候它就不是“无状态”方案了,别再拿无状态当卖点。

另外提醒一句,JWT 的 payload 只是 Base64 编码,不是加密,谁拿到都能解开看。别把手机号、身份证这种东西塞进去,我真见过往里塞的...

我最后落地的方案

权衡了一圈,最后是这么一套混着用的:

这样 JWT 就只承担“短期的、失效风险可承担的凭证”这一个角色,不再指望它包打天下。

写在最后

折腾完这一圈,最大的感受是:鉴权这事没有最佳实践,只有你愿意接受哪种代价。

网上的文章总喜欢把某个方案捧成银弹,实际项目里基本都是混着用的。我的建议是先想清楚业务对“注销时效”的真实要求——普通信息系统,短 token 过期就过期了,问题不大;支付、后台管理这种敏感场景,别犹豫,上黑名单,或者干脆老老实实 Session + Redis。先有安全要求,再选技术方案,顺序别反了。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放