☰
OAuth2统一登录实战:从授权码到令牌刷新,打通SSO全流程
2026/10/18 10:21:07 网站建设 项目流程

最近在公司做内部系统整合,手里一个业务后台叫 Kanass,要跟内部的统一认证平台 soular 打通,目标是实现所有系统一次登录到处访问。折腾了一圈,把 OAuth2 授权码流程、回调校验、令牌刷新、跨域 Cookie 这些点挨个踩了一遍,最终跑通。整个过程比想象中要琐碎,但理清之后其实很规整。如果你也在给自研系统接统一登录,可以参考这套做法。

这里我不讲泛泛的概念,直接拿 Kanass 对接 soular 这个真实场景拆解。整个联调过程分了三个层面:第一是设计层面明确 soular 和 Kanass 各自的边界;第二是配置层面把 soular 侧的应用注册和 Kanass 侧的认证源配好;第三是代码层面处理回调、换取 token、保存会话。围绕这三层,我整理了完整的操作步骤、参数对照,以及我在实际环境中遇到的坑。

适合看的同学:后端开发、前端负责人、运维,或者任何需要给自己的系统接统一身份认证的人。不需要你之前用过 soular,但最好知道 OAuth2 大概长什么样,完全没概念的话,第二部分的解释可以帮你补上。

1. 项目背景与整体思路

在做 Kanass 对接 soular 之前,内部已经跑了三年多的老账号系统。每个系统都有一套自己的用户表,员工入职要分别申请账号,离职要逐个注销,安全审计的时候账号清单对不上。这次目标很明确:把 Kanass 这类业务应用全部统一到 soular 上,用户只要在 soular 登录一次,进 Kanass 就不用再输入账号密码。这是典型的 SSO 场景,但 SSO 不是简单把登录页搬过去,它涉及协议、令牌、回调、会话管理一串东西。

1.1 为什么一定要让 Kanass 和 soular 打通

先说业务痛点。Kanass 是一个内部管理系统,里面挂了权限、审批、报表模块,原来自己做了一套登录逻辑,数据库里存用户表和密码摘要。问题是 Kanass 的账号体系和别的系统互不相通,每个系统都在重复建设账号模块。用户记密码难,管理员重置密码难,安全团队想做统一风控也无从下手。

打通以后,账号数据只在 soular 维护,Kanass 不再关心密码怎么校验,只关心当前请求的用户是谁、有没有权限。soular 登录成功后,浏览器会带着一个有效的认证凭证,Kanass 通过协议拿到用户身份,再建立自己的业务会话。这样新增一个应用时,不需要重新设计用户系统,只需要按标准协议接入即可。

还有一个容易被忽略的原因:统一登录之后,退出登录也能统一。以前每个系统有独立的退出按钮,用户以为退出了公司账号,其实只是退出了一个子系统。soular 接入后,可以由认证中心统一发起退出,Kanass 本地会话一并清理,安全边界更清晰。

1.2 Kanass 和 soular 的职责划分

明确边界是这个项目里最关键的事。soular 是认证服务端,负责的事包括:登录页面展示、账号密码校验、会话保持、授权码下发、令牌颁发、用户信息提供。它不关心 Kanass 的业务数据,也不应该碰 Kanass 的业务表。

Kanass 是业务应用,负责的事包括:引导用户跳转到 soular、接收回调解码授权码、换取访问令牌、读取用户信息、建立本地登录态、做权限控制。它不保存密码,也不能私自信任一个前端传递的用户 ID,所有身份来源必须经过 soular 的令牌校验。

这个边界直接决定代码放到哪一层。登录页必须由 soular 控制,Kanass 不能自己画一个登录页再偷偷传给 soular,否则账号密码会经过业务系统,又回到老问题的老路。Kanass 只保留“登录控制器”,也就是把请求重定向到 soular 的入口,以及处理 soular 回调回来的令牌。

1.3 为什么选 OAuth2/OIDC 这类标准协议

方案选型的时候考虑过自己写一个简单的 token 方案:用户登录 soular 后,生成一个随机字符串存 Redis,Kanass 拿着这个字符串去 soular 校验。听起来简单,但问题是每个系统对接方式都不一样,后续接入新应用得重复造轮子,安全性也没法统一保证。

后来决定走 OAuth2 标准协议,并在此基础上用 OIDC 获取身份信息。原因是生态成熟,Java、Go、Node 都有现成客户端库,soular 侧也有标准端点。Kanass 接入时只需要实现两次 HTTP 请求:一次拿授权码,一次拿令牌,代码量很少。

具体流程是标准的授权码模式:Kanass 跳转到 soular 的授权端点,用户登录后 soular 回调 Kanass 的 redirect_uri,在 URL 里带一个 code;Kanass 后端用这个 code 加上 client_id、client_secret,去 soular 的 token 端点换取 access_token 和 id_token;最后再用 access_token 调用户信息端点,拿到用户唯一标识。整个链路中,只有 Kanass 后端持有 client_secret,前端页面全程接触不到,安全性有基本保障。

提示:如果你的系统要求强身份令牌,建议直接启用 OIDC,业务系统可以通过 id_token 里的标准声明拿到用户 ID、邮箱、姓名,省去一次用户信息接口调用。

2. 核心概念拆解:从会话到令牌

很多同学第一次接统一登录,会把注意力放在“跳转”上,以为只要把登录按钮指到 soular 就行。实际不是这样。跳转只是入口,真正核心的是两个概念:会话怎么建,令牌怎么验。搞懂这两个,后面配置和排错都会顺畅很多。

2.1 会话、Cookie 与 SSO 的本质

soular 登录成功后,会在自己的域名下种一个 Cookie,这个 Cookie 是认证中心的会话凭证。因为 Cookie 是域名级别的,所以 soular 能识别“这个浏览器是否已经登录”。但 Kanass 的业务域名跟 soular 不一样,浏览器不会把 soular 的 Cookie 自动发给 Kanass,所以 Kanass 不能直接复用这个会话。

这就是授权码模式存在的意义。用户访问 Kanass 受保护页面,如果 Kanass 发现本地没有会话,就先把用户送到 soular。soular 检查到认证中心会话有效,就不展示登录页,而是直接生成一个授权码,通过回调地址把用户带回 Kanass。Kanass 拿到授权码以后再换令牌、建自己的会话。

可以用门卫模型去理解。soular 是公司门卫,Kanass 是某个办公室的负责人。员工先在门卫处亮证件,门卫给一张临时访客单;员工拿着访客单到办公室,办公室负责人打电话给门卫确认这张单有效,才放行。访客单就是授权码,电话确认就是后端换令牌。整个过程里,办公室不需要员工把工牌押给他,只需要确认门卫开过单。

2.2 soular 认证中心的工作流程

一次完整的登录流程,按顺序拆开是七个环节。

第一,用户访问 Kanass 的受保护资源,Kanass 后端发现本地会话不存在或已过期,生成一个随机的 state 参数并缓存,然后拼接授权 URL,返回 302 重定向到 soular。

第二,浏览器带上重定向后的地址,访问 soular 授权端点。这时候如果 soular 会话有效,它会直接进入授权确认页或者直接跳转;如果会话无效,用户看到的是登录页。

第三,用户输入账号密码登录,soular 校验通过后建立全局会话,并生成一次性授权码 code。这个 code 会挂在回调 URL 的 query 参数上,同时把 state 原样带回。

第四,浏览器跟随重定向访问 Kanass 的回调地址 /auth/callback。Kanass 后端先校验 state 是否与自己缓存的随机值一致,防止跨站请求伪造。校验通过,取出 code。

第五,Kanass 后端用 code 加上 client_secret 向 soular 的 token 端点发起 POST 请求,换取 access_token、refresh_token 和 id_token。这一步必须放在后端,因为 client_secret 不能暴露在浏览器端。

第六,Kanass 后端拿到 access_token 后,调用 soular 的用户信息端点,拿到用户的 opened、账号、部门等信息。如果启用了 OIDC,也可以解析 id_token 里的标准声明,但为了兼容后续版本,我建议还是调一次用户信息接口,毕竟身份字段可能扩展。

第七,Kanass 后端根据用户信息建立本地会话,种下自己的会话 Cookie,然后跳转到用户最初想访问的页面。之后再访问 Kanass 就不需要再走 soular,直到本地会话过期。

2.3 Kanass 业务系统如何信任 soular

信任关系建立在整个对接的注册信息上。soular 给 Kanass 分配一个 client_id 和 client_secret,它们相当于 Kanass 这个“客户端应用”的身份证和密钥。只有当请求里携带正确的 client_id,并且能证明自己持有 client_secret 时,soular 才认为这是合法的 Kanass 在请求令牌。

回调地址 redirect_uri 也是信任链的一环。soular 在授权阶段就检查回调地址是否与注册时完全一致,防止攻击者把授权码带到恶意域名上。实际踩坑最多的就是这里,多一个斜杠、大小写不一致、端口没带全,都会导致回调校验失败。

令牌校验时,Kanass 可以选择两种方式。一种是每次请求都拿着 access_token 调用 soular 的校验接口,简单直接,但每次都要打一次网络请求。另一种是信任 id_token 的签名,Kanass 配置 soular 的公钥或 JWT 签名密钥,本地解析令牌,减少网络依赖。初期建议保守一点,优先用接口校验,稳定后再切换到本地验签。

注意:client_secret 一定要保存在 Kanass 后端的环境变量或配置中心,不能写进前端代码,也不能提交到代码仓库。一旦泄露,相当于把统一登录的入口钥匙给了别人。

3. 动手实现:Kanass 与 soular 关联配置全流程

理论说完,开始配。这部分我按实际操作的顺序写,包含环境准备、soular 侧注册、Kanass 侧配置、联调验证。每一步都配上参数和解释,方便直接复现。

3.1 准备环境与参数清单

对接前先确认两边的环境。soular 需要有一套可访问的认证服务地址,可以是测试环境的独立域名;Kanass 需要有一个可以被浏览器访问的回调地址,最好也配上独立域名,不要用 IP 加乱端口,因为回调地址对端口敏感。

我这次在测试环境用的两组域名:

项目地址
soular 认证中心http://soular.example.com
Kanass 业务系统http://kanass.example.com
Kanass 回调地址http://kanass.example.com/auth/callback

域名不一定要公网可解析,内网 DNS 能通就行,但测试过程中要保证浏览器访问的域名和 soular 回调的域名完全匹配,包括协议 http/https、域名、端口。

还需要从 soular 管理后台确认几个端点地址。一般标准 OAuth2 服务都提供元数据文档,比如http://soular.example.com/.well-known/openid-configuration,里面能直接看到授权端点、令牌端点、用户信息端点。我把关键项列在这里:

参数示例值
授权端点http://soular.example.com/oauth2/authorize
令牌端点http://soular.example.com/oauth2/token
用户信息端点http://soular.example.com/oauth2/userinfo
client_id在注册应用后生成
client_secret在注册应用后生成
scopeopenid profile custom_attrs

3.2 soular 侧注册 Kanass 客户端

在 soular 管理后台找到“应用管理”或“客户端管理”,新建一个应用。应用名称填 Kanass,类型选 Web 应用,授权类型勾选 Authorization Code。回调地址填http://kanass.example.com/auth/callback。

这里有两个细节。第一,回调地址必须精确匹配。我当时因为填了http://kanass.example.com/auth/callback/末尾带斜杠,导致回调校验始终失败,浪费了大半天。第二,soular 支持多个回调地址时,通常以注册为准,任何页面拼接都必须以这里登记的地址开头。

保存后,soular 会生成 client_id 和 client_secret。client_id 是公开的,可以出现在前端跳转 URL 里;client_secret 只在保存完成时展示一次,一定要复制到后端配置里。如果需要授权 scope,建议至少包含openid和profile,如果有自定义部门、岗位字段,再加一个custom_attrs。scope 越大意味着系统开放的数据越多,不要把敏感字段全部授权出去。

3.3 Kanass 侧配置认证源与回调地址

Kanass 这边分前端和后端两部分。

前端要做的只是生成一个跳转地址,格式大致如下:

http://soular.example.com/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http%3A%2F%2Fkanass.example.com%2Fauth%2Fcallback&response_type=code&scope=openid%20profile%20custom_attrs&state=RANDOM_STRING

这里state是前端或后端生成的一个随机字符串,需要在跳转前存到临时会话或 Redis 里。回调时 soular 会把它原样带回来,后端需要比对是否一致。没有 state,等于给 CSRF 攻击留了一个门。

后端需要提供一个回调接口,我用的 Node.js 示例代码大致如下:

const crypto = require('crypto'); // GET /auth/callback async function handleCallback(req, res) { const { code, state } = req.query; // 1. 校验 state,防止 CSRF const savedState = req.session.oauthState; if (!state || state !== savedState) { return res.status(400).send('state mismatch'); } // 2. 用 code 换 token const tokenRes = await fetch('http://soular.example.com/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: 'http://kanass.example.com/auth/callback', client_id: process.env.SOULAR_CLIENT_ID, client_secret: process.env.SOULAR_CLIENT_SECRET }) }); const tokenData = await tokenRes.json(); const accessToken = tokenData.access_token; // 3. 拉取用户信息 const userRes = await fetch('http://soular.example.com/oauth2/userinfo', { headers: { Authorization: `Bearer ${accessToken}` } }); const user = await userRes.json(); // 4. 建立本地会话 req.session.user = { id: user.sub, name: user.name, email: user.email, department: user.department }; res.redirect('/dashboard'); }

这段代码省去了 token 缓存和刷新逻辑,但核心链路都在。建议在项目里单独封装一个OAuthClient,把换 token、刷新 token、拉取用户信息都集中管理,不要散落到业务代码里。

3.4 登录跳转与令牌校验的联调要点

配置完成后,联调不是直接点登录按钮就行,我建议按三步走。

第一步验证跳转。在浏览器里直接请求 Kanass 的受保护地址,观察 302 是否指向 soular。此时如果 soular 登录页正常显示,说明前端跳转地址里 client_id、redirect_uri 基本没问题。如果出现 error 页面,最常见的是unauthorized_client或redirect_uri_mismatch。

第二步验证回调。在 soular 上登录后,观察浏览器是否回到http://kanass.example.com/auth/callback?code=xxx&state=xxx。如果回不来,检查 Kanass 服务有没有启动、回调地址是否正确、有没有被网关拦截。如果回来了但 Kanass 报 state 错误,看是不是前后端跳转时 state 生成和校验用的不是同一个存储。

第三步验证令牌。后端换 token 的时候,可以用 curl 手动模拟一遍,确认 soular 端点可用:

curl -X POST "http://soular.example.com/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code&code=CODE&redirect_uri=http%3A%2F%2Fkanass.example.com%2Fauth%2Fcallback&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"

如果返回了 access_token,再拿这个 token 调 userinfo:

curl -H "Authorization: Bearer ACCESS_TOKEN" \ "http://soular.example.com/oauth2/userinfo"

能拿到 JSON 形式的用户信息,说明整个链路已经走通。剩下的工作就是把用户信息映射到 Kanass 本地用户表,并建立会话。

4. 登录流程中的关键细节与踩坑记录

整体流程跑通以后,我开始处理各种边界场景。这里记录的是实际遇到并解决的问题,每一条都值得仔细看,因为这些问题在文档里不太容易被提前警告。

4.1 回调地址不一致导致的频繁跳转

最典型的现象是用户登录成功后,被跳回 Kanass,Kanass 发现没有会话,又跳到 soular,soular 发现已经登录,又生成授权码,又跳回 Kanass,如此循环。从用户角度看,页面一直在两个域名之间闪烁,永远进不去。

我排查时发现,问题不在登录功能,而在于 soular 回调 Kanass 的地址是http://kanass.example.com/auth/callback,但 Kanass 代码里拼接的地址却是http://Kanass.example.com/auth/callback,域名首字母大小写不一样。浏览器和服务器对域名大小写宽容程度不同,soular 侧却把它当成了两个地址。最终统一改成小写,再重启应用,问题消失。

另一个常见的循环原因是回调接口所在的网关把路径改写了。比如反代配置从/auth/callback转发到后端/callback,但 soular 注册的回调地址网关上无法精确匹配。建议注册回调地址时,直接确认这个地址能被外部访问并能到达后端服务,不要依赖内部路径改写。

4.2 令牌有效期与刷新策略

access_token 默认有效期通常只有几十分钟到几个小时。如果 Kanass 只把 access_token 拿到手,不处理 refresh_token,用户本地会话过期后就得重新跳转 soular 登录。虽然这也是统一登录可接受的行为,体验上却会显得“明明登录过还要再登录”,尤其在内网环境里很烦。

我的处理策略是双轨并行。本地会话有效期设置得比 access_token 短,一旦本地会话过期,Kanass 后端检查是否持有 refresh_token,如果有,就主动调用 soular 的 token 端点刷新 access_token,再拉一次用户信息比对身份,比对通过后重新建立本地会话。

刷新接口的调用方式跟换 token 类似:

POST /oauth2/token grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=CLIENT_ID&client_secret=CLIENT_SECRET

需要注意,refresh_token 比 access_token 更敏感,一旦泄露等同于长期访问凭证。Kanass 后端在存储 refresh_token 时至少要加密,并且最好绑定客户端设备信息,防止被拿到其他环境重放。

4.3 跨域 Cookie 与自定义域名问题

刚开始有同事提了一个方案:让 soular 登录成功后把 Cookie 种到顶级域名下,Kanass 直接读取。听起来省事,但实际不可靠。企业内部可能不同系统分布在不同域名,甚至不同一级域名,共享 Cookie 的方案会直接把安全边界打开。

正确的做法就是走授权码模式。Kanass 不依赖 soular 的 Cookie,只在回调后获取 code 并换令牌。这样 Kanass 和 soular 即使完全不共享域名、不共享 Cookie,也能建立信任。

还有一个相关问题是跨域请求。如果 Kanass 前端需要直接调用 soular 的用户信息接口,浏览器会触发 CORS。soular 侧需要配置允许的来源域名,并确认是否需要携带凭证。能避免就避免这种设计,因为前端调用意味着 access_token 可能被前端脚本访问,XSS 风险更高。

4.4 测试账号与生产环境隔离

联调时最怕用同一个 soular 环境,测试账号和生产账号混在一起。比如你在测试环境把某个账号的部门改了,反过来影响生产数据,或者测试回调地址里带了生产 client_id,导致线上环境串号。

我这边采用了两套 soular 应用配置。一套是测试应用,client_id 后缀带-dev,回调地址指向测试 Kanass。另一套是生产应用,回调地址指向生产 Kanass。中间通过环境变量隔离,部署时由 CI 自动替换。这样测试时可以随意注册临时账号,不影响生产认证数据。

另外,日志里不要打印完整 code 和 access_token。我在排错时曾经把整个回调 URL 打到日志里,结果里面带 code,虽然只是内部环境,但安全习惯还是要早点养成。建议只打印 token 的前几位和用户 ID。

5. 常见问题速查与解决建议

接入过程中肯定要面对各种报错。这里把我在 Kanass 对接 soular 时遇到的高频问题整理成一张速查表,并讲讲快速定位问题的通用方法,让下一次排错可以少走弯路。

5.1 快速定位问题的方法

遇到统一登录问题,我习惯用“三步定位法”。

第一步看浏览器网络面板。打开开发者工具,切换到 Network 标签,观察从访问 Kanass 到跳转 soular 再到返回 Kanass 的完整请求链。如果第一个请求是 302,说明 Kanass 的登录拦截和跳转逻辑生效。如果请求一直停在一个地址打转,大概率是回调匹配问题。

第二步看 soular 侧日志。soular 会自动记录授权请求的来源、client_id、回调地址和错误码。通过错误码能区分是 client 配置问题还是用户凭证问题。我这边遇到过一个invalid_request,soular 给出的原因是缺少 scope 参数,很明显是前端跳转 URL 拼接不完整。

第三步看 Kanass 侧日志。重点看回调接口有没有被调用、收到了什么参数、后端换 token 是否成功。如果 Kanass 日志里没有回调记录,说明请求根本没到后端,需要查网关路由;如果收到 code 但换 token 失败,需要检查 client_secret 和 redirect_uri。

5.2 典型错误对照表

下面这个表格是我踩坑过程中整理出来的,基本可以覆盖第一轮对接的大部分报错。

错误现象可能原因解决办法
跳转 soular 时提示invalid_request授权 URL 缺参数或参数名写错对照文档检查 client_id、redirect_uri、response_type、scope
提示unauthorized_clientclient_id 不存在或未启用在 soular 后台确认应用状态,重新生成 client_id
提示redirect_uri_mismatch回调地址与注册不一致统一协议、域名、端口、末尾斜杠,两边严格一致
回调后报state mismatchstate 存储和校验没对齐检查是存在 Cookie 还是 Redis,确认取到的对象是否一致
换 token 时提示invalid_grant授权码已使用、已过期或回调地址不匹配确认 code 只使用一次,检查回调地址,必要时重新登录
拿到 token 后用户信息接口返回 401access_token 过期或未正确传 Bearer 头确认 token 是否过期,刷新后再试
登录成功但用户名总是 nullscope 没包含 profile 或自定义属性未授权在 soular 应用配置里追加 scope 并重新授权
线上环境偶发无法登录服务器时钟与 soular 不同步同步 NTP 时间,检查 JWT 签名允许的时间偏移

这张表不能替代日志分析,但它能在你两眼一抹黑的时候给出方向。处理任何报错,先把错误码找到,再回看配置,比瞎试要有效得多。

6. 一点实践经验

Kanass 和 soular 的对接已经稳定运行了一段时间。回过头看,技术本身不难,难的是边界设计和配置一致性。下面这些经验是我个人的体会,供后面再接手同类需求的同学参考。

6.1 上线前要做的检查

上线前请务必跑一遍完整流程清单,每一项都确认通过之后再切流量。我习惯列四类检查:未登录访问受保护页能否跳转、登录后能否正确回跳原页面、本地会话过期后能否无感刷新、主动退出后再次访问是否会被拦截。

实际操作中,最容易漏的是“旧会话”的处理。Kanass 老系统里有存量登录态,上线统一登录后,老会话要不要失效?如果不处理,可能出现用户从 soular 登出,但 Kanass 老会话还能访问接口的情况。我这边采取的办法是给 Kanass 的会话增加版本号,每次 soular 退出通知后,把版本号加一,旧会话全部失效。

还有一个建议:上线前在 soular 后台配置一个临时的维护白名单,保证就算认证中心短暂不可用,运维账号也能直接进入 Kanass 做紧急处理。这不算正规设计,但关键时刻能救命。

6.2 你可能会忽略的两个细节

第一个是统一退出。soular 登录一次,不代表点 Kanass 的退出按钮就能退出全局。需要调用 soular 的 logout 端点,让它清掉认证中心的会话,同时 Kanass 本地会话也要同步清理。否则用户以为退出了,实际还是全局登录状态,下次访问其他系统直接就通过了。

第二个是账号关联。如果 Kanass 原本就有用户表,不能简单拿着 soular 返回的用户 ID 直接当主键。建议在 Kanass 里维护一张映射表,存 soular 的 sub 和 Kanass 本地用户 ID 的对应关系。在一些边界情况里,比如用户被停用后重新开通,映射表能避免产生多个重复账号。

关于 Kanass 与 soular 的统一登录实践,核心思路就是这些。最后再说句实在话:这种对接工作,不要想着一次就能完美,按流程逐步验证,多留意日志里的 error 参数,基本都能顺利跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询