我们是如何把登录系统从“一行JWT”升级成企业级SSO的?
用户刚在 OA 登录完,打开 CRM 却又要重新输密码。小程序一启动就弹出“登录失败”。某天突然发现所有服务都拒绝 Token,原来 JWT 的签名算法改了,全系统都要改一遍……
这些事,我都经历过。
不是一次,是三次。
为了讲清楚这个过程,我虚构了一家公司——“踏浪科技”,它的认证系统,就是基于我亲身经历的痛点,一步步演进而来。
但如果你想理解:为什么需要网关?为什么不能只靠 JWT 做 SSO?为什么 OAuth2 是必经之路?
请继续往下看。
第一阶段:单体应用 (2020) #后端 #面试 #架构业务背景公司刚成立,开发了第一个内部系统:OA 办公系统。
功能很简单:
员工考勤打卡请假审批报销流程用户量:5个员工
技术架构最简单的单体应用:
HTTPS
JDBC
前端 Vue
后端 Spring Boot
MySQL
登录流程:
用户 前端 后端 数据库 后续请求带 Authorization: Bearer token 输入工号、密码 POST /login 验证密码(BCrypt) 验证通过 生成 JWT Token 返回 {token: "xxx"} 存 localStorage 用户 前端 后端 数据库
核心代码:
@PostMapping("/login")public LoginResponse login(@RequestBody LoginRequest req) { // 1. 验证密码 User user = userService.checkPassword(req.getUsername(), req.getPassword()); if (user == null) { throw new BusinessException("账号或密码错误"); } // 2. 生成JWT Token String token = Jwts.builder() .setSubject(user.getUsername()) .claim("userId", user.getId()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 15 * 60 * 1000)) // 15分钟 .signWith(SignatureAlgorithm.HS256, jwtSecret) // 从配置读取 .compact(); return new LoginResponse(token);}
这个阶段的特点:
简单直接,开发快 单体部署,运维简单 用户量小,性能够用没有问题,很稳定。
第二阶段:微服务拆分 (2021)业务变化公司拿到 A 轮融资,业务快速扩张:
OA系统功能越来越多(考勤、审批、报销、绩效、培训...)代码库膨胀到5万行团队从5人扩张到20人不同模块由不同小组开发老板的要求: "OA 系统太重了,要拆成微服务,方便各个团队独立开发部署。"
拆分后的架构前端 Vue
问题来了: 前端怎么调用
用户服务 :8001
考勤服务 :8002
审批服务 :8003
报销服务 :8004
遇到的问题问题1: 前端要记4个地址
// 登录调用户服务axios.post('')// 查考勤调考勤服务 axios.get('')// 提交审批调审批服务 axios.post('')
前端要配置多个 baseURL,维护成本高。
问题2: 每个服务都要验证 JWT
// 用户服务需要验证@Componentpublic class JwtFilter { ... }// 考勤服务也需要验证 @Component public class JwtFilter { ... }// 审批服务还需要验证 @Component public class JwtFilter { ... }
验证逻辑复制4份,改个算法要改4个地方。
问题3: CORS 跨域配置要配4次
每个服务都要配一遍允许的前端域名。
C 引入Spring Cloud Gateway解决方案: 加一个网关,统一入口。
统一调用 :8000
路由转发
路由转发
路由转发
路由转发
前端 Vue
Gateway 网关
用户服务 :8001
考勤服务 :8002
审批服务 :8003
报销服务 :8004
网关做的事:
有效
无效
请求到达
验证 JWT
提取 userId
放入 Header: X-User-Id
路由转发 给下游服务
返回 401
核心代码:
@Componentpublic class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono
下游服务简化了:
// 考勤服务@GetMapping("/my-attendance")public List getMyAttendance(@RequestHeader("X-User-Id") Long userId) { // 不需要验证JWT了,网关已经验证过 // 直接使用userId return attendanceService.getByUserId(userId);}
前端也简化了:
// 只需要配置一个baseURLaxios.defaults.baseURL = ''// 所有请求都走网关 axios.get('/user/info') // 网关路由到用户服务 axios.get('/attendance/list') // 网关路由到考勤服务 axios.post('/approval/submit') // 网关路由到审批服务
这个阶段的特点:
前端统一入口,配置简化 JWT验证只在网关做一次 下游服务只管业务逻辑 CORS配置只在网关配一次微服务架构稳定运行,问题解决。
第三阶段:多系统SSO (2022)业务变化公司业务继续扩张:
开发了CRM客户管理系统开发了财务系统收购了一家小公司,整合了他们的ERP系统现在有4个独立的系统:
OA办公系统 (oa.company.com)CRM客户管理 (crm.company.com)财务系统 (finance.company.com)ERP系统 (erp.company.com)用户的抱怨"我每天要登录4次,每个系统都要输一遍密码"
"用的是同一个账号,为什么不能登录一次就行"
"这系统是不是有bug"
老板发话了: "必须搞单点登录(SSO),用户体验太差了!"
问题分析为什么不能共享登录状态
用户在 OA 登录
Token 存在 oa.company.com 的 localStorage
用户打开 CRM
crm.company.com 的 localStorage 是空的!
问题核心:
localStorage不能跨域共享oa.company.com 的数据, crm.company.com 访问不到浏览器的安全策略,无法绕过能用 Cookie 吗
理论上可以:
如果设置: domain=.company.com那么 oa.company.com、crm.company.com 都能访问
但这有局限:
只能同一个顶级域名如果是 oa.com 和 crm.net ,Cookie完全没用我们收购的ERP系统用的是旧域名 erp-old.net需要一个通用方案,不管什么域名都能用。
引入认证中心:OAuth2解决方案: 搭建一个认证中心,用 OAuth2协议。
财务系统
CRM 系统
OA 系统
认证中心
没登录跳转
没登录跳转
没登录跳转
微服务
网关
前端
微服务
网关
前端
微服务
网关
前端
OAuth2 Server auth.company.com
用户数据库
完整流程:第一次登录OA用户 OA 前端 认证中心 OA 后端 登录成功! 1. 访问 oa.company.com 2. 检查 localStorage 没有 token 3. 跳转到认证中心 auth.company.com/login redirect=oa.company.com/callback 4. 显示登录页 5. 输入工号、密码 6. 验证通过 创建 Session(userId=123) 设置 Cookie 7. 生成授权码 code=ABC123 8. 跳转 oa.company.com/callbackcode=ABC123 9. 把 code 发给后端 10. 用 code 换 Token (带 client_id、client_secret) 11. 返回 Token=JWT-xxx 12. 返回 Token 给前端 13. 存 localStorage 用户 OA 前端 认证中心 OA 后端
关键代码:
// 认证中心:登录接口@PostMapping("/login")public void login(@RequestParam String username, @RequestParam String password, @RequestParam String redirect, HttpSession session, HttpServletResponse response) throws IOException { // 1. 验证密码 User user = userService.checkPassword(username, password); if (user == null) { throw new BusinessException("账号或密码错误"); } // 2. 创建Session (关键!) session.setAttribute("userId", user.getId()); // 3. 生成授权码 String code = UUID.randomUUID().toString(); redisTemplate.opsForValue().set( "oauth:code:" + code, user.getId(), 5, TimeUnit.MINUTES ); // 4. 跳转回OA,带上授权码 response.sendRedirect(redirect +}
此时浏览器有了认证中心的 Cookie:
Cookie: JSESSIONID=abc123; domain=auth.company.com; path=/C SSO生效:访问CRM自动登录
用户 CRM 前端 认证中心 CRM 后端 浏览器自动带上 Cookie: JSESSIONID=abc123 不需要输密码! 自动登录成功! 用户无感知! 1. 访问 crm.company.com 2. 检查 localStorage 没有 token 3. 跳转到认证中心 auth.company.com/oauth/authorize client_id=crm&redirect_uri=crm.company.com/callback 4. 从 Cookie 拿到 Session 5. Session 里有 userId=123 用户已登录! 6. 直接生成授权码 code=XYZ789 7. 立即跳转 crm.company.com/callbackcode=XYZ789 8. 把 code 发给后端 9. 用 code 换 Token 10. 返回 Token=JWT-yyy 11. 返回 Token 给前端 12. 存 localStorage 用户 CRM 前端 认证中心 CRM 后端
用户体验:
在OA登录时输入密码访问CRM,浏览器地址栏闪了一下,直接进去了全程没有看到登录页只输了一次密码!为什么能自动登录核心原理:认证中心的 Session
有
没有
第一次在 OA 登录
认证中心创建 Session userId=123
浏览器存 Cookie JSESSIONID=abc123 domain=auth.company.com
访问 CRM
CRM 跳转到认证中心
浏览器自动带 Cookie
认证中心从 Cookie 拿到 JSESSIONID
根据 sessionId 找到 Session
Session 里有 userId
直接发授权码 不要密码
显示登录页
三个关键点:
- 认证中心用Session记住"谁登录过"
session.setAttribute("userId", 123);
- 浏览器用Cookie访问Session (Cookie是钥匙)
请求 auth.company.com 时,浏览器自动带上:Cookie: JSESSIONID=abc123
- 授权码是一次性通行证 (跨域传递)
通过URL参数传递: code=XYZ789不依赖Cookie,可以跨任何域名
这个方案的优势:
支持任意域名组合 (oa.com + crm.net + erp.org) 用户只登录一次 符合OAuth2标准,生态完善浏览器兼容性提示: Safari/Chrome默认阻止第三方Cookie,可能影响跨域SSO。企业内网通常没问题,公网部署建议同域名或使用PKCE增强模式(详见第4篇)。
SSO 成功上线,用户满意度大幅提升。
第四阶段:移动端登录 (2023)业务变化产品经理提需求:
要开发微信小程序,方便手机上审批要开发钉钉小程序,和钉钉打通App端也要支持遇到的问题小程序/App 没有 Cookie,没有 localStorage!
浏览器 Web 端:
有Cookie (浏览器自动管理)有localStorage (手动存Token)访问 auth.company.com 自动带Cookie小程序/App:
没有Cookie有独立的存储 (wx.setStorageSync / SharedPreferences)每个App是独立沙盒,完全隔离传统的 OAuth2 SSO 在移动端失效!
因为 SSO 依赖:
浏览器自动带Cookie认证中心通过Cookie识别"已登录"移动端没 Cookie,认证中心无法识别。
移动端的解决方案方案1: 扫码登录
手机 App (已登录) 小程序 (未登录) 认证中心 登录成功! 1. 请求生成二维码 2. 生成临时码 QRCODE_123 3. 返回二维码 4. 显示二维码 5. 扫描二维码 6. 确认授权 (带 App 的 Token) 7. 验证 Token 绑定 QRCODE_123 8. 轮询检查 是否已授权 9. 已授权,返回 Token 10. 存本地 手机 App (已登录) 小程序 (未登录) 认证中心
适合:已经有一个已登录设备的场景。
方案2: 手机号验证码
最简单直接:
@PostMapping("/sms/login")public LoginResponse smsLogin(@RequestParam String phone, @RequestParam String code) { // 1. 验证验证码 String savedCode = redisTemplate.opsForValue().get("sms:" + phone); if (!code.equals(savedCode)) { throw new BusinessException("验证码错误"); } // 2. 查询或创建用户 User user = userService.findOrCreateByPhone(phone); // 3. 生成Token String token = jwtService.createToken(user); return new LoginResponse(token);}
方案3: 第三方登录(微信/钉钉)
// 微信小程序wx.login({ success: res => { const code = res.code // 发给后端 wx.request({ url: '', data: { code }, success: res => { wx.setStorageSync('token', res.data.token) } }) }})
// 后端@PostMapping("/wechat/login")public LoginResponse wechatLogin(@RequestParam String code) { // 1. 用code换openid WechatSession session = wechatService.code2Session(code); // 2. 查询或创建用户 User user = userService.findOrCreateByOpenId(session.getOpenid()); // 3. 生成Token String token = jwtService.createToken(user); return new LoginResponse(token);}
这个阶段的特点:
移动端不能用传统SSO 需要多种登录方式:扫码/验证码/第三方 Token存储方式不同,但验证逻辑相同抓住本质前端的职责无论是 Web、小程序还是 App,前端只做三件事:
Web 端能做的:
Cookie自动管理localStorage存Token跳转实现SSO移动端做不到的:
没有Cookie (做不了传统SSO)手动存Token手动传Token后端的职责后端只做一件事: 验证"你是谁"
验证身份
在哪验证
单体应用
微服务
多系统
本地验证 JWT
网关验证 JWT
认证中心 发授权码换 Token
JWT 验证 vs 权限验证:
// JWT验证:验证"你是谁" (本地验证,快)Claims claims = Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody();Long userId = claims.get("userId", Long.class);// 权限验证:验证"你能干什么" (调认证中心,实时) boolean hasPermission = authClient.checkPermission(userId, "user:delete");
分工明确:
JWT验证:网关/本地完成权限验证:认证中心统一管理架构演进全景图2023: 移动端
认证中心
小程序
App
扫码/验证码/第三方登录
2022: 多系统 SSO
跳转
跳转
跳转
授权码
授权码
授权码
认证中心 OAuth2
OA 前端
CRM 前端
财务前端
2021: 微服务
Gateway 网关 统一 JWT 验证
前端
用户服务
考勤服务
审批服务
2020: 单体应用
Spring Boot + JWT
前端
每一步都是解决新问题:
| 年份 | 业务场景 | 技术挑战 | 解决方案 | 核心技术 | | ---
| 2020 | 10人的 OA 系统 | 基础登录 | Spring Security +
| 2021 | OA 拆成微服务 | 重复验证 JWT | Gateway 统一鉴权 | Global Filter | | 2022 | 4个独立系统 | 重复登录 | 单点登录 SSO | OAuth2授权码 | | 2023 | 小程序/App | 没有 Cookie | 多种登录方式 | 扫码/验证码/第三方 |
常见误解澄清误解1: 网关 = SSO不是!
网关:
一个系统内部的微服务统一入口,统一验证JWT验证一次,下游不用管SSO:
多个独立系统之间每个系统有自己的前端、后端登录一次,所有系统免登录误解2: OAuth2 = 微信登录不是!
OAuth2是协议,有两种用法:
1. 企业内部 SSO:
认证中心是公司自己搭建用户用工号密码登录多个系统之间免登录2. 第三方登录:
认证中心是微信/GitHub/QQ用户用第三方账号登录拿到第三方用户信息本质相同,都是 OAuth2授权码模式。
误解3: 前端很复杂不是!
前端永远只做三件事:
- 存Token传Token跳转(没Token或需要SSO)
复杂的逻辑在后端:
网关验证认证中心权限管理安全性与生产级实现说明重要提示: 本文是概述性文章,为了让读者理解演进逻辑,简化了很多安全细节。
生产环境必须考虑:
- JWT安全:必须设置过期时间(exp、iat)密钥从配置文件读取,不能硬编码使用双Token机制(Access + Refresh Token)第2篇已详细讲解OAuth2安全:验证client_id和client_secret授权码必须绑定clientId和redirectUri使用PKCE防止授权码拦截使用state参数防CSRF攻击第4篇会完整实现浏览器兼容性:Safari/Chrome默认阻止第三方Cookie企业内网部署通常没问题公网部署建议:同域名 (*.company.com)或使用PKCE增强模式第4篇会详细讲解微服务内部调用:服务间调用需要内部Token或服务账号网关只处理前端→后端的请求第3篇会讲服务间鉴权
后续实战篇会讲完整的生产级实现:
第3篇: Gateway + JWT验证 + 内部调用第4篇: OAuth2完整安全实现 + PKCE第5篇: 第三方登录对接这篇先抓住核心思路,建立全局观。
最后说两句这个虚拟的故事,串联了我过去几年在不同公司遇到的真实场景。
从单体到微服务,从一个系统到多个系统,每次演进都是业务驱动的,不是为了炫技。
不要为了用技术而用技术,先理解为什么需要,再学怎么实现。
如果这篇文章帮你建立了全局观,欢迎关注,后续实战篇会手把手一起实现。
下一篇,我们撸起袖子写代码!
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。
