ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微服务环境下基于OAuth2与JWT的单点登录SSO落地实践

2026/9/11 0:28:31 拓冰建站 浏览量
微服务环境下基于OAuth2与JWT的单点登录SSO落地实践 做过微服务的人早晚都会碰到一个绕不开的痛点业务系统越拆越多认证登录却越来乱。用户记一堆账号密码每次换个系统都要重新登录内部平台明明是一套人马却要在五六个系统里各登录一次到了年底盘点发现每个服务都自己维护用户表、session、密码规则权限审计更是查无可查。这些问题说到底就是一个字SSO单点登录没有做好。我所理解的微服务环境下的单点登录不是简单地把所有系统接入同一个登录页而是要解决三个层面的问题身份认证怎么统一、登录状态怎么共享、权限信息怎么在各服务间安全传递。今天这篇内容就把我在实际项目中落地过的一套SSO方案完整拆开讲透包括整体设计、认证中心搭建、网关接入、前端对接、安全加固以及踩过坑才总结出来的排查方法和经验教训。内容偏实战适合正在做微服务架构改造的后端开发、架构师以及被“一堆系统登录不过来”逼疯的运维同学参考。1. 整体设计与方案选型1.1 先从核心模型说起不管用什么样的框架和技术栈SSO的核心模型其实非常固定就是三个角色的协作用户、客户端应用、认证中心。用户只和认证中心打交道提交账号密码或者扫码认证中心验证通过后给客户端应用发一个凭证客户端应用拿着凭证去换取访问令牌后续带着令牌访问微服务。这个模型不是拍脑袋定的它是几乎所有成熟SSO方案的底层骨架CAS、OAuth2、OIDC都跑不出这个大框架。理解这个模型的关键在于“信任关系”。认证中心是唯一信任源所有应用都信任认证中心的判定结果。这里最容易犯的错是把用户信息直接放在各个系统里各自维护还指望SSO能一键打通。实际上SSO只能统一“认证”和“登录状态”用户基础数据、权限数据这些还是一定要收口到统一账号中心否则即使登录打通了各个系统的“张三”到底是同一个人还是同名同姓你又得头痛。1.2 常见方案对比我在选型的时候基本把主流方案都过了一遍从最早的票据共享、到CAS、再到目前流行的OAuth2和OIDC各有适用的场景。下面这张表是我比较常用的判断依据方案核心机制优点缺点适用场景Session共享统一存储Session如Redis实现简单老项目改造快耦合度高移动端不友好小规模内部系统CAS票据TGT/ST服务端集中认证成熟稳定Java生态友好定制化较重前后端分离不友好传统校园、企业内部系统OAuth2授权码模式授权码换令牌客户端和服务端分离开放标准生态成熟适合API场景流程稍复杂需理解授权模型微服务、开放平台OIDC在OAuth2上增加身份层能拿到标准用户信息支持JWT协议细节多选型成本略高新项目、多端场景如果只是几个老系统做统一登录CAS确实是稳妥路线尤其是已有大量服务端渲染页面的系统改造量小。但微服务环境下系统普遍是前后端分离前端是独立应用后端服务通过API互相调用这种情况下OAuth2优势更明显。要是还想顺手把用户身份信息标准化OIDC会是更现代的补充。但坦白讲对多数内部微服务系统来说OAuth2授权码模式加JWT已经足够了没必要一上来就把OIDC全家桶堆上去。1.3 我为什么选择OAuth2 JWT最终我落地的方案是OAuth2授权码模式负责“授权流程”JWT负责“令牌格式”Redis负责“可控会话状态”网关负责“统一校验”。这套组合的好处在于三件事能分开治理。第一授权流程交给OAuth2登录交互体验可以统一。用户访问任何一个接入系统都会被引导到认证中心完成登录认证中心拿到用户身份后发一个授权码前端拿授权码去换访问令牌这个流程对前端是完全透明的。第二JWT天然适合无状态微服务。用户信息、过期时间、权限标识都可以直接编码在令牌里资源服务本地验签就能拿到身份不必每次请求都远程调认证中心问“这个token有没有效”。第三网关统一校验避免每个服务重复造轮子服务内部不用关心登录逻辑专注业务。这样组合还能应对一个常见问题一个系统接入时可能只是几个页面要匿名几个接口要登录几个接口要管理员权限。如果把校验逻辑散落在各个服务里权限规则改一处漏三处而网关统一处理之后规则集中、变更可控。实际项目中我见过不少团队把JWT当万能药资源服务各验各的最后密钥一盘散沙出问题时连token在哪个环节失效都定位不了。2. 认证中心设计2.1 认证中心该有哪些数据认证中心本质是一个独立服务它不承载业务只专注三件事管理用户身份、管理客户端应用、颁发和吊销令牌。我的数据库设计里最少会包含五类表用户表、角色权限相关表、客户端应用表、授权记录表、令牌表。很多新手做SSO只建一个用户表和一个token表结果客户端应用标识不知道存哪、refresh token查不到、授权范围没法控制后期全是窟窿。客户端应用表是容易被忽略但极其重要的表它记录每个接入系统的client_id、client_secret、回调地址、允许的授权范围。回调地址必须在这里登记并做精确匹配这是防止授权码被第三方截获的关键防线。令牌表则要注意JWT虽然可以无状态但为了支持主动下线、强制退出、异常检测还是得有一份令牌的冗余存储只是读取上优先本地验签、必要时再查状态而不是每次请求都查库。用户从账号中心同步过来之后密码字段的保存规则也要统一。我见过微服务改造时直接把老系统明文密码倒进新库的项目这是极端危险的。不管原来怎么存迁到认证中心后至少要通过BCrypt或scrypt做哈希密码永不反解、永不写日志。2.2 授权码模式登录流程拆解流程看起来复杂但拆开就四步。第一步未登录用户访问业务系统前端前端判断没有token就重定向到认证中心拼上client_id、redirect_uri、response_typecode。第二步认证中心展示登录页用户输入账号密码认证通过后认证中心生成一个一次性授权码重定向回业务系统前端带着code参数。第三步前端把这个code发给自己的后端后端在服务端用client_secret去认证中心换token这个步骤必须放在服务端因为client_secret不能暴露给浏览器。第四步业务后端拿到access_token和refresh_token之后前端请求携带access_token访问微服务。这里有一个关键细节授权码是短时效、一次性使用的通常5分钟有效使用后立即失效。如果前端反复拿同一个code去换token认证中心应该直接报错。有些团队在联调时发现偶尔换不到token排查半天发现是前端在不经意间重复请求了接口而认证中心又没对code做一次性校验。无论授权码有效期多短服务端都必须落库记录使用状态这是标准动作。授权码模式中还有一个容易被业务方纠结的点用户信息到底什么时候返回。正确的做法是认证中心默认只返回身份标识比如user_id和基本用户名更详细的资料、角色权限由业务后端用自己的client_id去用户中心接口获取或者直接将权限信息编码进token。这样做的好处是认证中心不成为所有业务数据的咽喉接口压力可控。2.3 令牌设计与刷新策略令牌设计直接决定了用户体感和系统安全我的习惯是双令牌access_token和refresh_token。access_token有效期短一般30分钟到2小时refresh_token有效期长按业务需要7天到30天。前端访问资源时带access_token过期后就用refresh_token静默换取新的access_token用户无感知续期不用重新登录。access_token我选JWT格式payload里至少包含这几个字段user_id、user_name、client_id这个token是发给哪个应用的、scope授权范围、exp过期时间、jti唯一ID查禁用列表用。注意不能放密码、手机号这些敏感字段因为JWT默认只是Base64编码不是加密明文可读而且会随着每次请求头传过去泄露面比较广。真要放敏感信息至少用JWE做加密但一般场景完全没必要。refresh_token则没必要做成JWT用随机字符串存Redis更合适。原因是refresh_token的使用频率低必须支持服务端主动吊销而随机字符串天然可以被撤销。刷新令牌时不能简单地返回同一个refresh_token要做轮换策略每次刷新都生成新的refresh_token旧的立即失效。这样即使refresh_token在某个环节被泄露攻击者也只能使用很短一段时间。而access_token短时效能进一步缩小攻击窗口。3. 微服务网关与资源服务接入3.1 网关统一认证过滤器很多微服务项目都会引入Spring Cloud Gateway或APISIX这类网关SSO落地时最好的位置就是在网关层做统一认证。网关的作用不是自己完成登录而是拦截请求、校验access_token、把用户身份转换成下游服务能识别的信息。这样做最大的好处是接入新服务时不需要写认证逻辑服务只管业务。我落地过的网关校验逻辑大致是这样的先判断当前请求路径是否需要登录比如登录接口、健康检查、静态资源直接放行再判断请求头里有没有Authorization携带access_token没有就返回401并携带跳转认证中心的地址如果有则做JWT验签和过期校验通过后解析出用户信息以X-User-Id、X-User-Name这样的标准请求头传给下游服务最后记录一些审计日志。要注意的是网关统一认证不能解决所有安全职责。网关只是入口微服务之间也存在互相调用的可能服务A调用服务B时可能根本没有走网关。因此服务间调用要单独设计一套信任机制例如内部服务用mTLS或者服务B校验调用方来源IP和服务名。不能因为网关校验了下游就完全不设防。3.2 用户信息在下游怎么传递和消费网关校验完JWT后不能把原始token直接转发给所有下游那样每个服务都得解析一遍JWT重复劳动且性能浪费。更好的做法是网关把token解析结果转换为规范化的用户上下文通过HTTP Header传下去。Header的命名要统一比如X-User-Id、X-User-Name、X-Client-Id最好在内部文档里固定下来避免不同服务各叫各的。这条设计还有一个隐性好处引入一种轻量的“内部身份”机制。下游服务只要信任网关传递过来的Header即可不需要关心用户到底是从OAuth2流程来的还是管理员在后端模拟的还是定时任务触发的。我在项目里会刻意区分X-User-Id真实用户和X-User-Name操作者避免内部定时任务调用业务接口时因为缺少用户信息导致NPE满天飞。但Header传递也有风险最典型的是客户端伪造Header。比如外部请求直接自己带一个X-User-Idadmin发到网关如果网关不够严密这个伪造头就会穿透到下游。所以网关在接收到请求时必须先把请求里的X-User-Id、X-User-Name等内部Header全部清除再从JWT重新生成绝不能让客户端控制内部身份字段。这个清除逻辑放在过滤器最前面属于底线策略。3.3 JWT本地验签还是远程校验资源服务拿到用户上下文之后还要不要自己再验一遍token这是架构设计中一个常见争论。我的观点是外部请求必须经过网关校验但内部服务之间的请求不能只依赖网关。原因在于微服务调用中可能经过消息队列、定时任务、等等异步链路这些场景根本没有网关参与。服务之间需要校验调用方身份时我更推荐本地JWT验签方案。每个服务通过Nacos或配置中心共享同一个公钥本地解析和验证JWT签名不依赖认证中心在线。这样既避免每次请求都远程调用认证中心的高延迟和单点压力又能保证服务调用链路上的身份可信。不过本地验签也有前提条件必须保证密钥分发安全公钥放配置中心私钥在认证中心妥善保管。若发现私钥泄露需要立即轮换密钥并且要能容忍旧token的短暂失效期。这个场景下Redis的密钥版本号机制就很有用token里带上kid密钥ID服务端用kid选择对应的公钥进行验签轮换时增加新密钥标号即可平滑过渡不会导致所有用户瞬间被踢下线。4. 客户端应用接入与前端兼容4.1 前端跳转登录的回调链路客户端应用接入时前端是整个SSO体验的“脸面”跳转逻辑写不好用户就会觉得“这系统怎么回事一下就白屏了”。以一个Vue或React单页应用为例最基础的做法是在前端路由守卫里判断本地的access_token是否存在如果不存在就重定向到认证中心的登录地址带上client_id和redirect_uriredirect_uri指回当前页面。认证中心登录成功后回调到redirect_uri并且带上?codexxx参数。前端这时候有两种处理方式。一种是前端直接拿code去自己的后端接口换token这也是我推荐的方式因为前后端分离下client_secret只能在后端持有。另一种是前端通过隐藏在页面里的iframe对code复用完成多系统同步登录这种方式对第三方Cookie依赖太强现在主流浏览器已经逐渐放弃第三方Cookie我不再推荐。换到token之后就不要在URL上再带任何敏感信息了前端要立即清理URL里的code参数用history.replaceState把地址去掉。很多白屏问题就出在用户收藏夹里存了带code的地址下次访问时code已过期前端又没处理这个分支直接白屏。4.2 移动端内置浏览器SSO登录白屏问题这里特别想聊一个我实际遇到过、也在各种社区被反复提问的场景移动端App内置浏览器里访问门户跳SSO登录后登录成功了但页面一直白屏怎么调试都没有报错。这类问题大概率是“往返跳转”被内置浏览器拦了或者Cookie被限制导致会话无法衔接。典型原因有两种。第一种内置浏览器把跳转当成第三方页面禁用了CookieSSO登录页写入的会话Cookie丢失登录完回到业务系统又变成未登录。第二种链路上的重定向次数太多内置浏览器出于安全策略直接拦截。排查这类问题不能只在PC端浏览器试一定要拿真机、用内置浏览器开远程调试看网络请求、看Cookie是否写入、看跳转链条是否断在某一步。我最终采用的解决方案很土但很有效登录回调地址用顶层页面跳转而不是用iframe嵌套Cookie设置里把SameSite设为Lax或None还需加上Secure同时把认证中心域名和业务域名尽量收敛到同一个主域名下用域内Cookie做会话保持。还有一个补充手段是在登录成功后由前端主动通过JS轮询一次会话状态如果检测到会话丢失就自动再跳一次登录比让用户卡在白屏上强。4.3 单点登出流程单点登录做得再顺单点登出如果忽略用户照样会骂人。登出的核心是“一处退出处处退出”。最简单的方案是认证中心提供一个全局登出接口客户端调用它去清除认证中心的会话同时把该用户签发过的token标记为失效。问题在于微服务下token可能分散在各个资源服务本地缓存里认证中心没法实时通知所有服务。可行的处理方式是双管齐下一是认证中心维护一个黑名单里面记录被强制退出的token的jti二是网关校验token时除了验签和过期时间还要查一下黑名单。这个黑名单可以放在Redis里并设置过期时间避免数据无限膨胀。为了性能考虑网关可以本地缓存一份黑名单比如30秒同步一次牺牲极短时间的延迟换来更好的并发性能。真正严格的全端登出还需要接入系统的前端去清理本地的token并同步跳转到认证中心的登出地址。这一步不要指望纯后端能完成否则用户点击退出后本地localStorage里的旧token仍然存在下次页面刷新又自动带上造成“明明点了退出刷新又变成登录状态”的怪异体验。5. 安全加固与高可用落地5.1 令牌存储与XSS、CSRF防护SSO做得越多越要明白一个道理登录入口越集中攻击价值就越高。保护令牌和凭证是第一优先级。浏览器端存储access_token我推荐的方式是内存变量加localStorage辅助而不是直接塞Cookie。如果放Cookie就必须严格设置HttpOnly、Secure、SameSite属性避免被脚本读取。不过OAuth2流程下前端换取token后更常见的还是localStorage。LocalStorage的缺点是无法防XSS攻击一旦页面被注入恶意脚本token会被直接偷走。因此接入SSO的前端项目必须做基础的XSS防护比如对用户输入输出做转义、不信任任何富文本内容、使用CSP等。后端要防的则是CSRF在授权码模式下因为token是放在Authorization头里而不是自动携带的Cookie天然能防御大部分CSRF但仍要避免使用Cookie来保存token的旧式做法。还有一个容易被忽视的安全点是回调地址校验。认证中心在接收授权码请求时必须严格比对redirect_uri和客户端应用注册时填写的地址不能只判断域名前缀一致否则攻击者可以构造一个同域但不同路径的地址把授权码带到自己控制的页面。精确匹配回调地址是OAuth2协议明确要求的动作不要因为联调麻烦就去掉。5.2 认证中心的高可用设计认证中心是整个微服务体系的“命门”它挂了所有依赖SSO的系统都登录不了。因此认证中心不能当作普通业务服务一样只部署单节点至少要保证多实例部署、负载体均衡。但多实例会带来一个新问题用户登录成功后如果落在实例A下一次请求被负载均衡转到实例B实例B没有用户的会话怎么办。解决方案依然是用Redis做会话共享。登录态统一存Redisupstream的实例都能读到。授权码、refresh_token、黑名单这些数据天然都应该在Redis里认证中心实例本身要做到尽可能无状态。我还会给认证中心单独配置一个Redis集群和业务缓存隔离避免业务系统高峰期的缓存击穿把认证中心拖垮。另外一个高可用细节是限流和降级。认证中心最容易被打爆的是两个接口登录接口和换token接口。建议用Sentinel或网关层限流对这两个接口做定向限制比如单IP每秒最多几次登录尝试全局限流策略按容量设定。登录接口还要做防暴力破解连续失败N次锁定账号一段时间或者引入人机校验。这些措施要在早期设计否则线上被刷了才加用户已经被折磨完了。5.3 网关与认证中心的故障隔离即使认证中心挂了已经登录过的用户能不能继续访问业务系统这是一个需要提前决策的问题。从体验角度讲access_token的有效期内资源服务本地验签完全不需要访问认证中心所以已登录用户的请求不受影响这是JWT方案相对中心化Session方案的一个天然优势。但refresh_token刷新接口还是依赖认证中心如果认证中心宕机用户token过期后就无法续期会被迫重新登录。可以接受的降级方案是网关在认证中心不可用期间临时放行一部分低频操作或者返回明确的错误提示让用户知道是SSO系统在维护而不是业务系统坏了。千万不要让业务系统在认证中心不可用时也连锁崩掉体验雪崩比登录失败更可怕。实践中我会给网关的认证中心调用配置超时和熔断比如调用刷新接口超过1秒就放弃本次刷新返回401并提示稍后重试而不是无限制地重试把认证中心压垮。服务之间的调用超时设置是微服务高可用里最常见的细节但也是最容易被SSO方案忽视的。6. 常见问题与排查技巧实录6.1 登录后访问接口一直401这是我被问得最多的问题。登录成功、token也拿到了但访问微服务接口还是401。首先打开浏览器的Network面板看实际请求里是否带上了Authorization头。很多前端项目只在登录接口里写了token其他请求的拦截器没配导致后续请求没有带token。其次看JWT是否过期如果拿到token后很久才发请求exp已过网关当然拒绝。还有一种隐蔽情况是网关验签用的公钥和认证中心生成JWT的私钥不匹配。尤其是多环境多配置中心时环境A的微服务用的是环境B的公钥签名验不过去接口就一路401。排查方法很简单用认证中心签发的token在网关本地用当前公钥手动验一次签名能验过再查下游验不过直接怀疑密钥配置。6.2 出现无限重定向循环SSO接入后最常见的糟糕体验是访问业务系统跳到认证中心登录登录成功后回到业务系统又检测到未登录再次跳认证中心……如此循环用户直接崩溃。这个问题通常出在Cookie或token的写入与读取不一致。前端把token存进去了但路由守卫里读取用的key不一致或者认证中心的会话Cookie没有在业务域名下生效导致每次回调都感觉是新会话。无限重定向还有一个高发原因业务系统前端自己维护了一个“本地登录状态”它不是以token为准而是以是否从认证中心跳转回来为准。一旦回调链路中某个环节被浏览器拦截比如第三方Cookie被禁前端就一直认为自己没登录反复触发跳转。此时不要在前端继续加状态判断先从浏览器是否正常写入会话Cookie入手排查。6.3 第三方Cookie被禁导致的白屏前面提到过移动端内置浏览器的白屏其实在PC端的Chrome、Safari上也会出现。很多老旧的SSO方案喜欢用iframe Cookie的方式在多个域名之间同步登录态但浏览器对第三方Cookie的限制越来越严这种方案会逐渐失效。遇到白屏时先看开发者工具里有没有关于Cookie的警告再检查认证中心写Cookie时有没有设置SameSiteNone和Secure。如果你不想把认证中心和业务系统收敛到同一个主域名也不想动Cookie策略那就老老实实走标准的页面重定向流程。页面跳转不受第三方Cookie限制每次登录都让认证中心发一个一次性授权码业务系统拿code换token体验上慢几百毫秒但胜在稳定和兼容。在浏览器隐私策略不断收紧的当下重定向换code是更值得投的方案。6.4 网关内网服务互相调用时丢用户信息外部请求走网关没问题但微服务内部互相调用时服务A调服务BB拿不到用户上下文。这个问题的根因是网关只在入口处解析了一次token服务A内部继续调用服务B时不会自动携带X-User-Id这些Header。我的做法是封装一个内部调用工具它可以从当前线程上下文里取出用户信息然后覆盖到HTTP请求头里保证用户上下文在调用链上传递。这里要特别小心覆盖逻辑发起调用前先清空目标请求里已有的X-User-ID等内部Header再写入当前上下文的值防止恶意标记或旧数据残留。如果链路特别长服务A调B、B调C每一跳都要传递这时候可以考虑引入链路追踪来观察Header的流向避免排查时两眼一抹黑。7. 个人实操中的最终体会说句实在话SSO方案本身没有多少新技术含量真正磨人的是把它落地到真实业务和时间线里。我在多个项目里做过“将多个独立的若依系统改造为统一单点登录”的类似事情每次改造最先要说服的不是技术团队而是业务方为什么要等登录中心建好才能动工为什么要统一账号体系因为SSO一定是一个“先收口、再放开”的过程账号不统一、客户端应用不登记清楚接入再多系统都是表面功夫。如果让我给一个最小可行建议那就是不要刚开始就追求大而全。先做一个认证中心支持一个业务系统接入把授权码流程、网关校验、刷新令牌、登出这个完整闭环跑通再复制到第二个、第三个系统。中间用到Nacos做配置和发现、knife4j做调试、Sentinel做限流都可以但核心永远是那套稳定的协议和持续维护的信任关系。方案是死的踩坑是必经之路代码可以找一个简洁的开源项目参考但安全和兼容性的细节必须靠你自己在真实浏览器、真实设备上一个个验证。最后再分享一个经验每次踩完坑把问题记录成一份按现象索引的排查手册比看十遍官方文档都管用。下面几个问题建议你单独记录下来授权码没一次性校验、回调地址没精确匹配、网关没有清空外部Header、Cookie的SameSite设置错误、认证中心私钥和资源服务公钥不一致。这些坑我已经替你先踩了一遍你项目里大概率也会遇到其中一两个。到时候翻到这一节能帮你少折腾几小时也算这篇长文没白写。