技术栈: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 注册流程
- 用户提交 手机号 + 密码到 auth 服务。
- 查重:auth 通过 OpenFeign 调 user-service,确认手机号没被注册。
- 加密:auth 用
passwordEncoder.encode()把明文密码做 BCrypt 哈希,得到密文。 - 存储:auth 把密文通过 OpenFeign 传给 user-service,由它存进 tb_user 表。
设计要点:密码加密在 auth 做,user-service 拿到的已是密文,只负责存、不负责加密——加密是认证逻辑,存储是数据逻辑。
🎤 密码绝不存明文。BCrypt 是单向哈希,像把肉打成肉馅——能加密、不能还原。即使数据库泄露,攻击者也拿不到原始密码。而且 BCrypt 自带随机盐,同一个密码每次加密结果都不同,能有效防彩虹表。
4.2 登录流程
- 用户提交 手机号 + 密码。
- 查用户:auth 通过 OpenFeign 按手机号调 user-service,拿到含密文密码的用户信息。
- 验密码:用
passwordEncoder.matches(明文, 密文)比对。 - 签发 JWT:验证通过后,用 jjwt 把用户 id 装进令牌载荷,用密钥签名,返回 token。
验密码的核心认知:不是「解密数据库密文再比对」(哈希不可逆),而是把用户这次输入的明文再哈希一次,比对两个密文是否一致。这套逻辑被 matches() 封装好了。
🎤 JWT 分三段:头部(算法)、载荷(用户 id、过期时间)、签名(防伪)。载荷是 Base64 编码、明文可见的,所以只放用户 id 这类「被看到也无害」的信息,绝不放密码。防伪靠第三段签名——用只有服务端知道的密钥生成,别人改了载荷也算不出对的签名。
4.3 带令牌访问业务流程
- 前端带令牌:登录后保存 token,之后每次请求放在
Authorization请求头里。 - 网关校验:请求先到 gateway,
GlobalFilter拦截,做三件事:判断白名单 → 取令牌 → 验签 + 查过期。 - 塞入身份:验证通过后,网关从令牌解析出用户 id,写进自定义请求头
X-User-Id。 - 路由转发:按路由规则(
lb://服务名)经 Nacos 找到目标服务,转发过去。 - 业务取身份:业务服务用拦截器从
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?
提示:讲项目时,先讲「解决什么问题」和「整体架构」,再按三条流程串,深挖问题随机应变。切忌一上来就贴代码。