这段时间我一直在折腾一件事把我们内部几个各自为战的业务系统统一到一个登录入口底下。项目代号倒是很形象sward 负责守门soular 负责认人。说白了sward 是一个网关层soular 是一个身份认证中心两者联动起来之后用户只需要登录一次就能带着身份凭证在多个系统之间跳转这就是标题里说的统一登录。如果你正在给团队做单点登录或者手里有几个 OAuth2/OIDC 相关组件不知道怎么串起来这篇东西应该能省你不少探路的时间。统一登录这件事听起来简单真正落地时会碰一鼻子灰。比如回调地址对不上、token 丢失、跨域拿不到会话、刷新页面反复跳登录页……这些坑我基本都踩过一遍。所以这篇文章不打算只贴配置我会把 sward 和 soular 各自该干什么、登录链路里每一步发生了什么、配置时哪些字段必须较真、出问题后怎么定位全部摊开来讲。名字不是重点重点是这两个角色怎么配合用什么协议交互。只要把这套机制吃透换成任何网关和认证中心都能照猫画虎。1. 为什么是 sward soular——统一登录的整体设计思路1.1 统一登录到底在解决什么问题在没有统一登录之前业务系统多起来之后是一种什么样的体验我这边最典型的情况是OA 一套账号、报表一套账号、内部工具又一套账号每个系统都有自己的密码策略用户要么记不住要么干脆用同一个弱密码到处填。更麻烦的是如果某个系统升级了登录逻辑其他系统完全不知道每次排查都要挨个问。统一登录的核心就是把“你是谁”这件事从每个业务系统里抽出来交给一个专门的认证中心。soular 在这里就是那个认证中心它负责校验用户名密码、管理会话、签发身份凭证。而 sward 作为统一入口负责把用户请求拦下来判断你到底有没有登录过再决定是放行还是跳去登录。这样一来业务系统自己不需要保存密码也不需要实现复杂的登录页只需要认 sward 转发过来的身份信息就行。这样做好处很明显第一个是账号管理集中在 soular 一处开通、禁用、改密都只操作一次第二个是安全策略可以统一做比如强制双因子、登录失败锁定、会话超时时间第三个是用户体验提升登录一次就能在所有接入系统里无缝切换。我后来复盘时觉得统一登录的最大收益不是“少登几次”而是把认证逻辑收拢到一个可审计、可管控的地方。1.2 sward 和 soular 的职责边界很多团队做统一登录失败不是因为技术不会而是因为职责没分清楚。sward 和 soular 的关系可以理解成“门卫”和“证件签发中心”sward 在门口负责看每个人手里有没有通行证soular 在办公室负责发通行证、登记个人信息、判断通行证是否有效。门卫不需要知道这个人平时喜欢点哪家外卖证件签发中心也不关心门卫具体有几道门。具体到技术实现sward 只做三件事路由、会话维持、身份透传。路由就是把不同路径的请求转发给对应的后端服务会话维持就是管理用户在浏览器里的登录状态通常体现为一个加密 cookie身份透传就是把从 soular 拿到的用户信息以标准格式比如 JWT塞给下游服务。soular 也只做三件事认证、授权、签发 token。认证是确认“密码对不对”授权是确认“你能访问哪些系统”签发 token 是生成一段双方都能信任的凭证。这个边界划分非常重要。我曾经见过有人把 token 校验逻辑写在业务代码里结果每个服务都要去 soular 查一次认证中心压力大不说业务系统还耦合了一堆安全细节。正确的做法是 sward 统一校验 token业务系统只从请求头里读用户信息完全不需要关心 token 怎么来的、怎么验的。这样后续如果要换认证中心只要 sward 和 soular 的接口不变业务系统一行代码都不用改。1.3 登录流程的核心链路把 sward 和 soular 串起来完整的登录链路大致是这样用户在浏览器里访问任意一个接入 sward 的业务地址sward 发现这个用户没有携带有效的会话凭证于是返回一个重定向让浏览器跳到 soular 的登录页soular 验证用户密码通过后生成一个授权码再把浏览器重定向回 sward 指定的回调地址sward 收到授权码后拿着它去 soular 的后端接口换 token换到 token 后sward 自己维护一条会话记录同时把用户身份信息写入 cookie最后带着用户回到最初想访问的那个页面。这条链路走一遍最多也就两三秒但中间涉及三次重定向、两次服务端通信。第一次重定向是浏览器跳 soular第二次重定向是 soular 跳回 sward第三次是 sward 恢复用户原始访问路径。很多人配置失败就是因为把这三步的 URL 写错了或者漏了 HTTPS 导致 token 在跳转过程中被浏览器拦截。这里有个关键点sward 和 soular 之间换 token 的请求一定要在服务端完成不能在前端用 ajax 直接发。因为换 token 需要用到 client_secret这个值一旦暴露在浏览器端就等于把认证中心的钥匙交给了所有人。我在下文的实操环节会专门演示这个服务端换 token 的写法。2. 核心概念Token、回调与会话同步2.1 从一次登录看全局状态统一登录和传统单系统登录最大的区别在于“状态”的存放位置。传统登录状态存在应用服务器的 session 里统一登录状态分了两层一层是 soular 里的全局会话另一层是 sward 里的局部会话。全局会话代表“这个人在认证中心登录过”局部会话代表“这个人在当前网关下的会话有效”。一个很容易踩的坑是全局会话过期了局部会话还在用户表面上看起来是登录状态但一点击某个需要重新校验身份的功能就报错。反过来局部会话过期了全局会话还在用户被踢回登录页但重新登录时发现不用输入密码直接跳回来了体验上会觉得“系统抽风了”。我后来做了一个约定soular 的全局会话有效期必须大于 sward 的局部会话有效期这样即使局部会话失效用户也能安静地重新走一遍静默登录而不是被打断。另外多个接入系统之间的会话是独立的sward 给每个系统分配的会话标识不能互相串。尤其是在浏览器多标签页的场景里标签页 A 退出登录标签页 B 下一跳就收到 401这是合理的因为局部会话已经销毁了。但很多用户会以为是 bug建议在接入系统里把退出后的提示页面做得友好一点告诉用户“你在其他标签页已经退出”。2.2 soular 签发什么凭证soular 在认证通过后主要签发两类凭证一类是授权码另一类是 token。授权码是短命的通常五分钟内有效它不能直接证明身份只能用来换 tokentoken 才是真正的身份凭证一般会区分 access token 和 refresh token。access token 用于访问资源有效期短refresh token 用于在 access token 过期后重新换发有效期长。实际项目里我倾向于让 soular 签 JWT 格式的 access token。JWT 的好处是自带签名和用户信息sward 拿到后不用回 soular 校验本地验签就能知道用户是谁。验签需要用到 soular 公开出来的 JWKS 端点sward 启动时会去拉取公钥后续验签全部在本地完成性能好很多。不过 JWT 也有一个隐藏风险因为它是自包含的一旦签发就无法在过期前强制撤回。比如用户被禁用、改密码、踢下线旧的 JWT 在有效期内仍然能通过验证。对于这个问题我的处理方案是在 sward 的局部会话里额外维护一层黑名单如果 soular 通过 webhook 通知某个用户状态变更sward 就把对应的 jti 拉黑这样既保留了 JWT 的性能优势又能及时响应账号注销事件。2.3 sward 如何校验与透传身份sward 拿到 token 之后并不是简单夹在请求里往下游传而是要把 token 解析成结构化身份信息再以约定的 header 格式传给后端服务。我常用的字段叫 X-User-Id 和 X-User-Name后端服务只认这两个 header不直接碰原始 token。这样做的意义是业务系统不需要知道 token 的格式和签名算法未来就算从 JWT 换成其他格式业务系统也不用改动。校验流程上sward 需要做三步。第一步检查局部会话存在且有效第二步检查 token 的签名和有效期第三步检查权限范围。签名校验用 JWKS 公钥有效期校验看 exp 字段权限范围校验看 scope 或 audience 字段。很多配置问题出在第三步比如 token 签发了但 audience 不是当前系统的sward 应该拒绝放行否则就是越权。为了让调试方便我习惯在 sward 的日志里把每一步的结果打出来比如“auth redirect triggered, target systemreport”“token exchanged, userzhangsan, scopeopenid profile email”。实际排查问题时这些日志比什么抓包工具都直观。上线后这些日志要脱敏不能把原始 token 打出来否则日志泄露等于账号泄露。3. 实战从零配置一套统一的登录链路3.1 环境准备与版本选型开始配置前我建议先把版本和部署方式定下来。soular 我选的是最新稳定版支持 OIDC 协议存储用 PostgreSQLsward 作为网关层我部署在它前面的 Nginx 后面对外统一暴露 443 端口。soular 不直接对浏览器暴露sward 和 soular 之间的通信走内网地址避免认证中心被外部流量打爆。环境清单大致如下组件版本/参数说明sward2.6.1网关服务对外端口 8443soular4.3.0认证中心内网端口 8080数据库PostgreSQL 14存放用户、client、token 记录前置代理Nginx 1.24HTTPS 终止反向代理到 sward接入系统Spring Boot 3.2示例业务服务端口 9001为什么 sward 不直接暴露 80 端口因为统一登录对 HTTPS 有硬性要求。回调地址、cookie Secure 标志、token 传输过程都依赖 HTTPS如果前面没有证书浏览器会直接拦截或者把 cookie 丢弃。我踩过最惨的一次坑就是内网测试用 IP 加 HTTPsoular 回调地址填的却是 HTTP结果换上 HTTPS 之后所有回调全部失效浪费了半天排查。在开始前先把四个 URL 列在一张纸上soular 的授权端点、soular 的 token 端点、sward 的登录回调地址、业务系统首页地址。后面所有配置都围绕这四个 URL 来。3.2 soular 认证中心配置在 soular 管理后台注册一个客户端这个客户端代表 sward 网关。关键的配置项包括 client_id、client_secret、redirect_uri、grant_type、scope。redirect_uri 必须写 sward 的回调地址比如 https://sward.example.com/callback这里需要跟 sward 配置文件里完全一致差一个斜杠都会导致验证失败。我推荐用授权码模式authorization_code这是最安全的授权流程因为密码只经过 soular业务系统和 sward 都碰不到。scope 按实际需求开最少是 openid profile如果需要用户头像、邮箱再临时加。用最小化原则不要什么权限都申请soular 签发的 token 里信息越多泄露后的风险越大。配置示例# soular 客户端配置 clients: - client_id: sward-gateway client_secret: ${SWARD_CLIENT_SECRET} redirect_uris: - https://sward.example.com/callback grant_types: - authorization_code - refresh_token scopes: - openid - profile配置完成后记得做一次连通性测试浏览器直接访问 soular 的授权端点带上 client_id 和 redirect_uri看能不能正常跳到登录页。如果这一步都不通说明 soular 侧的基础配置有问题先解决这个再往下走。3.3 sward 网关接入配置sward 侧要配的内容稍微多一点上游服务路由、认证规则、会话管理、身份透传 header。先把基础路由配上再挂统一登录这样方便定位问题。一个最小化的 sward 配置长这样# sward 全局配置 server: port: 8443 ssl: enabled: true key-store: classpath:keystore.p12 key-store-password: ${SSL_PASSWORD} safety: login-enabled: true auth-server: https://soular.intra.example.com client-id: sward-gateway client-secret: ${SWARD_CLIENT_SECRET} redirect-uri: https://sward.example.com/callback jwks-url: https://soular.intra.example.com/oidc/jwks routes: - path: /report/** upstream: http://10.0.1.20:9001 auth-required: true headers: X-User-Id: $.sub X-User-Name: $.name - path: /healthz/** upstream: http://10.0.1.20:9001 auth-required: falseauth-required 字段很重要。像 /healthz 这类健康检查接口必须放行否则监控系统探测时会收到 302 跳转导致误报。而所有业务接口都建议收口到某个公共前缀下比如 /report、/oa、/tool每个前缀对应一个后端服务。这样 sward 的路由规则可以写得更简洁也方便单独控制哪些服务需要登录。启动 sward 后访问一个受保护路径如果能看到浏览器跳转到 soular 登录页说明前半段链路已经通了。接下来要处理的是登录成功的回调。3.4 第一个接入系统的改造后端服务不需要自己写登录逻辑但必须做一件事读取 sward 转发过来的身份 header 并解析。这里有个前提sward 必须保证所有进入上游服务的请求都已经验证过身份业务服务只需要信任这些 header。但谨防有人绕过 sward 直接访问上游服务所以业务服务所在网络要限制只允许 sward 的 IP 访问最好落到防火墙白名单里。以 Spring Boot 为例我习惯写一个简单的拦截器从请求头里提取用户信息Component public class AuthHeaderInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userId request.getHeader(X-User-Id); String userName request.getHeader(X-User-Name); if (userId null || userId.isEmpty()) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } AuthContext.set(userId, userName); return true; } }如果业务的容器框架是 Spring Security可以直接在 SecurityConfig 里配置这个过滤器优先级高于其他认证过滤器。注意不要让 token 直接进到业务系统业务系统只需要拿到用户 ID 和关键属性即可这样隔离层级更清晰。接入完成后测试一下完整流程从浏览器访问 /report/index应该跳到 soular登录成功后自动跳回原页面。这个过程里如果发现跳转后没有带身份信息优先排查 sward 的 token 换发是不是成功了在 sward 的日志里找token exchanged关键字。3.5 登录态刷新与退出登录实战里还要处理两个细节access token 过期了怎么办、用户主动退出怎么办。sward 和 soular 的联动方案是sward 的局部会话有效期设置成 2 小时soular 的全局会话有效期设置成 8 小时。当 sward 发现 access token 过期但 refresh token 还有效就自动拿着 refresh token 去 soular 换新 token不用打断用户。实现上sward 需要配置 refresh_token grant 的凭证# sward 令牌刷新配置 token-refresh: enabled: true refresh-token-path: /oauth2/refresh max-lifetime: 8h退出登录这块有两个层次。只退 sward 局部会话用户下次访问还会跳 soular 但能静默登录退全局会话才是真正的退出所有接入系统一起失效。我的建议是默认让用户执行全局退出先删 sward 的 cookie再重定向到 soular 的 logout 端点这样 soular 会清掉全局会话同时通知其他接入系统。不要怕麻烦统一登录的退出机制如果不彻底才更麻烦。4. 常见问题与排查技巧实录4.1 回调地址比对失败这是我在接入过程中遇到最多的报错soular 页面上直接提示 “redirect_uri 不匹配”。原因几乎都是字符级不一致有的多了一个斜杠有的是 http 和 https 混用有的是带上了默认端口还有的是 IP 地址和域名混写。soular 校验回调地址是非常严格的必须精确匹配注册值。排查方法很直接打开浏览器开发者工具看登录前那次 302 重定向的 Location 参数把 redirect_uri 参数和 soular 里注册的地址逐字对比。我自己的习惯是先复制地址到文本编辑器里对比肉眼经常看不出区别但编辑器能显示空格和转义符。这个步骤虽然笨但最可靠。提示回调地址建议统一用 HTTPS并且不要带路径参数只写到域名加固定路径比如 https://sward.example.com/callback。路径里尽量少一层转发避免 Nginx 再改写导致回调地址和真实地址不一致。4.2 Token 过期后页面刷新跳转循环症状是用户正在填写一个很长的表单刷新一下页面又跳回登录页登录完跳回来之后发现刚填的东西全丢了再刷新又跳。造成这个循环的原因通常是 access token 有效期设得太短而 sward 的局部会话又独立于 soular 全局会话导致 sward 认为会话失效、soular 又认为已经登录过。我的处理方案是三层配合soular 的 access token 有效期稍微拉长到 30 分钟refresh token 有效期 8 小时sward 的局部会话有效期跟随 refresh token页面前端保存一个用户正在编辑的 draft后端接口在返回 401 时不要立即跳登录页而是返回一个错误码前端收到后静默调用 sward 的刷新接口。如果刷新接口返回成功就原样重试刚才的请求只有刷新失败才跳登录页。还有一类循环是 sward 到自己 url 的登录回调被肝住了登录成功后跳回原地址但原地址也被判定为未登录于是又去登录。这种一般是 sward 的 cookie 没有正确种到浏览器。排查项包括 cookie Secure 标志、Domain 配置、SameSite 属性以及 sward 是否设置了Set-Cookie响应头。4.3 前端跨域拿不到登录态如果你的业务系统是前后端分离前端静态资源放在 CDN接口走 sward这种架构很容易遇到跨域问题。浏览器发现页面域名和接口域名不一致默认不会携带 cookie导致用户明明登录过前端却一直收到 401。这种问题的解法是前后端约定好跨域凭证放行规则。前端在所有请求里加上credentials: include后端 Nginx 和 sward 都要响应对应的 CORS 头并且Access-Control-Allow-Origin不能写星号必须写具体前端域名Access-Control-Allow-Credentials设置为 true。这里要注意 preflight 请求通常不带 cookiesward 需要允许 OPTIONS 请求通过但不能把业务接口完全放行。如果觉得 CORS 配置麻烦我更推荐让前端走同域策略静态资源也由 sward 或者 Nginx 代理页面和接口都在同一个域名下只是路径不同。这样可以彻底避开跨域 cookie 问题。很多内部系统不需要极致的 CDN 性能同域反而是最省心的方案。4.4 子服务时钟漂移导致验签失败JWT 验签时有个隐形杀手服务之间的系统时间不一致。如果 sward 和 soular 不在同一台机器而且没有启用 NTP 时间同步sward 验签时计算exp剩余时间可能出现偏差导致一个刚签发不久、实际没过期的 token 被判定为过期。这个坑不容易发现因为收到的是 “token expired” 错误。排查办法是在 sward 和 soular 的两台机器上分别执行date命令看时间差如果超过几十秒就要考虑时间同步服务。我目前的部署规范里所有走 auth 链路的主机都要配置 NTP这是统一登录稳定运行的基本前提。另外验签时钟偏差最好留一点余量。sward 在算法上允许一个 leniency 窗口比如接受 token 往后偏 30 秒内的过期时间这个字段在 JWT 校验库里通常叫clock_skew。设为 30 到 60 秒比较合理既能容忍轻微时钟漂移又不会让过期 token 长时间存活。4.5 问题速查表症状可能原因处理建议登录页跳转死循环cookie 没种上 / access token 有效期太短检查 Set-Cookie 属性调整有效期层级redirect_uri 不匹配注册地址与实际地址不一致逐个字符比对统一使用 HTTPS 域名登录后跳回 401前后端跨域没有携带 cookie配置 CORS credentials或改为同域部署日志提示 token expired时钟漂移 / token 实际过期同步 NTP 时钟设置 clock_skew退出登录后仍能访问只销毁局部会话全局会话仍在重定向到 soular 全局 logout 端点部分系统能登录部分不能audience 或 scope 不匹配检查 token 的 aud 字段是否包含当前系统这套速查表并不是覆盖所有问题但能覆盖 80% 的首次接入问题。剩下 20% 多半是网络隔离、防火墙、负载均衡会话保持这些基础设施问题需要结合各自技术栈去排查。最后分享一个小技巧不要一上来就集成所有系统。先搭一个最简单的 demo把 sward 和 soular 都装在本地用两个测试页面把登录和退出流程跑通再逐步接入真实业务系统。我后来反思当初就是因为急着把生产系统接进来导致第一次排障东一榔头西一棒槌。基础链路跑通之后再横向复制后面每个系统接入几乎都是十分钟的事。做基础架构类改造慢就是快先把骨架打好再往里填肉。