技术栈:Spring Boot 3 · Spring Cloud Alibaba · OpenFeign · JWT · Gateway · Nacos


如何使用这份文档

面试讲项目,最忌讳从代码细节讲起。正确顺序是:

先讲它解决什么问题 → 再讲整体架构 → 然后按请求流程串一遍 → 最后应对深挖。

本文档按这个顺序组织。带 🎤 标记的引用框,是可以基本照着说的话术。


一、一句话概述(开场用)

🎤 我做了一套基于 Spring Cloud Alibaba 的微服务认证系统。它把「认证」这件事拆成三个角色:auth 服务负责签发令牌、网关负责统一校验令牌、各业务服务通过请求头拿到用户身份。整体用 JWT 做无状态认证,服务之间用 OpenFeign 通信,靠 Nacos 做服务发现。

关键词锚点(面试官会顺着这些追问,要准备好):无状态 JWT、网关统一鉴权、OpenFeign 远程调用、Nacos 服务发现、BCrypt 密码加密、职责分离。


二、它解决什么问题(讲动机)

背景:系统是微服务架构,有多个独立服务(auth、user-service、archive-service 等)。如果每个服务各自做一遍登录校验,会有两个问题:

  • 重复:每个服务都写一遍鉴权逻辑,改规则要改很多处。
  • 有状态难扩展:传统 session 存在单台服务器内存里,多实例之间不共享,扩容困难。

解决思路:「网关统一安检 + JWT 无状态令牌」。所有请求先经过网关校验令牌,业务服务不再重复鉴权;令牌本身自包含用户信息,验证只需用密钥本地验签,不依赖服务端存储。

🎤 类比机场:网关是安检口,严格查一次护照(验签);过了安检给你贴个写着身份的手环(请求头里的用户 id);后面登机口、免税店(各业务服务)只看手环,不再查护照。验签只做一次,业务服务零负担。


三、整体架构(讲骨架)

模块划分:项目是 Maven 多模块微服务,核心模块及职责如下。

模块 类型 职责
gateway 网关 所有请求入口;统一校验 JWT;按路由转发到后端服务
auth 认证服务 登录、注册;验密码;签发 JWT。自己不连数据库
user-service 业务服务 管理用户数据(tb_user 表的增删改查)
archive-service 业务服务 档案业务;从请求头读取当前用户身份
api 契约模块 存放各服务的 Feign 接口声明 + 配套 DTO(被各服务依赖)
common 基建底座 统一返回 Result、全局配置、ThreadLocal 用户上下文等

一个关键设计——auth 与 user-service 分离:auth 只管「认证动作」(验密码、发令牌),不碰数据库;它要用户数据时,通过 OpenFeign 远程调用 user-service 去查。user-service 只管「用户数据」,不掺和登录逻辑。

🎤 为什么不合成一个?因为职责不同:auth 管「身份核验这个动作」,user-service 管「用户数据怎么存取」。拆开后,auth 可以专注鉴权,user-service 可以专注数据,互不污染。这也是为什么 auth 里没有任何数据库依赖。


四、三条核心流程(面试主线,按这个讲)

讲项目最有效的方式是「按请求流程走一遍」。三条流程:注册、登录、带令牌访问业务。

4.1 注册流程

  1. 用户提交 手机号 + 密码到 auth 服务。
  2. 查重:auth 通过 OpenFeign 调 user-service,确认手机号没被注册。
  3. 加密:auth 用 passwordEncoder.encode() 把明文密码做 BCrypt 哈希,得到密文。
  4. 存储:auth 把密文通过 OpenFeign 传给 user-service,由它存进 tb_user 表。

设计要点:密码加密在 auth 做,user-service 拿到的已是密文,只负责存、不负责加密——加密是认证逻辑,存储是数据逻辑。

🎤 密码绝不存明文。BCrypt 是单向哈希,像把肉打成肉馅——能加密、不能还原。即使数据库泄露,攻击者也拿不到原始密码。而且 BCrypt 自带随机盐,同一个密码每次加密结果都不同,能有效防彩虹表。

4.2 登录流程

  1. 用户提交 手机号 + 密码。
  2. 查用户:auth 通过 OpenFeign 按手机号调 user-service,拿到含密文密码的用户信息。
  3. 验密码:用 passwordEncoder.matches(明文, 密文) 比对。
  4. 签发 JWT:验证通过后,用 jjwt 把用户 id 装进令牌载荷,用密钥签名,返回 token。

验密码的核心认知:不是「解密数据库密文再比对」(哈希不可逆),而是把用户这次输入的明文再哈希一次,比对两个密文是否一致。这套逻辑被 matches() 封装好了。

🎤 JWT 分三段:头部(算法)、载荷(用户 id、过期时间)、签名(防伪)。载荷是 Base64 编码、明文可见的,所以只放用户 id 这类「被看到也无害」的信息,绝不放密码。防伪靠第三段签名——用只有服务端知道的密钥生成,别人改了载荷也算不出对的签名。

4.3 带令牌访问业务流程

  1. 前端带令牌:登录后保存 token,之后每次请求放在 Authorization 请求头里。
  2. 网关校验:请求先到 gateway,GlobalFilter 拦截,做三件事:判断白名单 → 取令牌 → 验签 + 查过期。
  3. 塞入身份:验证通过后,网关从令牌解析出用户 id,写进自定义请求头 X-User-Id
  4. 路由转发:按路由规则(lb://服务名)经 Nacos 找到目标服务,转发过去。
  5. 业务取身份:业务服务用拦截器从 X-User-Id 取出用户 id,存进 ThreadLocal,业务代码任意处用 UserContext.getUserId() 获取。

🎤 白名单很关键:登录、注册接口本身不能验令牌——用户还没登录哪来的令牌,否则就死锁了。所以这两个接口在网关直接放行。


五、关键技术点(应对深挖,这些最容易被追问)

5.1 为什么 JWT 验签放在网关,而不是每个服务?

  • 性能:验签只在网关做一次,业务服务零鉴权负担。
  • 解耦:业务服务不需要懂 JWT、不需要密钥,代码极简,只读请求头。
  • JWT 自包含的优势:验签是用密钥本地计算,不查库、不远程调用,所以快;这也是 JWT 相比 session 在分布式下的核心优势——无状态、可水平扩展。

5.2 网关为什么用 WebFlux(响应式)?

网关用的是 Spring Cloud Gateway,底层是 WebFlux 非阻塞模型,和普通 Servlet 服务(auth/user-service)不同。

  • Servlet(阻塞):一个请求占一个线程,等 IO 时线程干等、浪费,高并发下线程易耗尽。
  • WebFlux(非阻塞):线程不干等,等 IO 时去服务别的请求,少量线程扛大量并发。
  • 为什么适合网关:网关流量最大、又主要在「等后端转发」,正好契合非阻塞;代价是网关里不能写阻塞代码,而 JWT 验签是纯本地计算、天然不阻塞。

5.3 用户身份为什么用请求头传,不能在网关用 ThreadLocal?

核心原因:网关和业务服务是两个独立进程,各有各的内存。ThreadLocal 只在进程内有效,网关存了业务服务跨不过去、读不到。

  • 跨进程必须靠网络可传输的载体:请求头跟着 HTTP 请求在网络上传输,能从网关「飞」到业务服务。
  • 两段 ThreadLocal 是分开的:网关解析 id → 写请求头(跨进程的桥)→ 业务服务读请求头 → 存进自己进程的 ThreadLocal 供本地取用。

🎤 这是分布式的一个根本原则:跨服务传数据只能用「网络可传输的载体」(请求头、请求体、消息队列),不能用「进程内内存」(ThreadLocal、静态变量、单例)——进程内的东西困在自己进程里。

5.4 ThreadLocal 为什么必须 remove?

线程复用会串数据:Web 服务器线程是线程池复用的。线程处理完用户 A 的请求不会销毁,会被回收去处理用户 B。如果不清 ThreadLocal,B 可能读到 A 残留的用户 id,造成「张冠李戴」的严重 bug。

做法:在拦截器的 afterCompletion(请求彻底结束、哪怕异常也会执行)里调 removeUser() 清理。

5.5 一个安全隐患(主动提,是加分项)

业务服务信任 X-User-Id 请求头。如果有人绕过网关直接访问业务服务、自己伪造这个头,就能冒充他人。

解决:保证业务服务只能通过网关访问、不直接对外暴露端口(网络隔离 + 内部鉴权),所有请求必经网关安检,X-User-Id 才可信。


六、技术栈一览

领域 技术选型 作用
微服务框架 Spring Boot 3 + Spring Cloud Alibaba 2023 基础框架
服务发现 Nacos 服务注册与发现,服务间靠服务名互找
服务调用 OpenFeign + LoadBalancer 声明式远程调用 + 负载均衡
网关 Spring Cloud Gateway (WebFlux) 统一入口、鉴权、路由
认证 JWT (jjwt) 无状态令牌的签发与校验
密码 BCrypt (Spring Security Crypto) 密码单向哈希
持久层 MyBatis-Plus + PostgreSQL 用户数据存储
配置管理 Nacos Config 集中配置(密钥等可放共享配置)

七、完整数据流(一张图讲清)

登录拿令牌

前端 → 网关(白名单放行) → auth → Feign查user-service → 验密码 → 签发JWT → 返回token

带令牌访问

前端(带token)
  → 网关 GlobalFilter(验签 + 查过期)
  → 解析 userId 塞入 X-User-Id
  → 路由(lb://) 经 Nacos 找服务
  → 业务服务拦截器读 header 存 ThreadLocal
  → 业务代码 UserContext.getUserId()

八、面试自检清单(能答上说明你真懂了)

  • 为什么密码不能明文存?BCrypt 哪两个特性让它安全?
  • JWT 三段分别是什么?载荷为什么不能放密码?防伪靠什么?
  • 登录验密码是「解密比对」还是「再哈希比对」?为什么?
  • 为什么 auth 不直接连数据库?它怎么拿用户数据?
  • 为什么验签放网关一次就够?JWT 相比 session 的优势?
  • 网关为什么用 WebFlux?它和 Servlet 模型的区别?
  • 用户身份为什么用请求头传,不能在网关存 ThreadLocal?
  • ThreadLocal 为什么必须 remove?不清会怎样?
  • X-User-Id 有什么安全隐患?怎么防?
  • api 模块和 common 模块的定位区别?Feign 接口为什么放 api?

提示:讲项目时,先讲「解决什么问题」和「整体架构」,再按三条流程串,深挖问题随机应变。切忌一上来就贴代码。