Skip to content

OAuth 2.0(SSO 单点登录)

RuoYi Office 内置 OAuth 2.0 授权服务,可以把平台作为企业内部统一认证中心,为门户、报表、大屏、客户自建系统等第三方 Web 应用提供 SSO 单点登录。当前推荐优先使用授权码模式:用户只在 RuoYi Office 登录与确认授权,第三方应用的后端拿 code 换取 access_token,再用 Bearer Token 访问开放接口。

本文基于当前仓库实现重写,重点覆盖授权码模式的接入流程;密码模式、客户端凭证模式和刷新令牌模式作为补充能力使用。

为什么优先选授权码模式

OAuth2 授权模式的选择,核心看两个问题:访问方是不是用户本人、客户端是否有可靠后端保存密钥。第三方 Web 系统做 SSO 时,浏览器侧不应该保存 client_secret,因此推荐走授权码模式,让第三方后端完成换 token。

OAuth2 授权模式选择

上图可以作为模式选择速查:如果是第三方系统接入、并且有服务端,就走授权码模式;如果只是平台内部前端登录,不需要额外 SSO 客户端;如果是服务之间机器调用,再考虑客户端凭证模式。

RuoYi Office OAuth2 可用授权模式

RuoYi Office 当前保留了 authorization_codepasswordclient_credentialsrefresh_token 等模式。本文的主线是授权码 SSO,原因是它最适合企业门户、报表、大屏等“用户从第三方系统跳到统一认证中心登录”的场景。

本地实现位置

位置
OAuth2 客户端页面ruoyi-office-vben/apps/web-antd/src/views/system/oauth2/client
访问令牌页面ruoyi-office-vben/apps/web-antd/src/views/system/oauth2/token
SSO 授权页ruoyi-office-vben/apps/web-antd/src/views/_core/authentication/sso-login.vue
前端开放 APIruoyi-office-vben/apps/web-antd/src/api/system/oauth2/open/index.ts
前端路由ruoyi-office-vben/apps/web-antd/src/router/routes/core.ts
OAuth2 开放 ControllerOAuth2OpenController.java
OAuth2 用户 ControllerOAuth2UserController.java
客户端校验OAuth2ClientServiceImpl.java
授权码与令牌OAuth2CodeServiceImpl.javaOAuth2GrantServiceImpl.javaOAuth2TokenServiceImpl.java

实现架构

OAuth2 SSO 实现架构

在 RuoYi Office 里,授权中心由 PC 管理端前端、System 后端模块和 Token/Scope 校验链路共同组成:

  1. 第三方系统负责发起授权跳转、接收 code、保存自己的登录态。
  2. web-antd/auth/sso-login 负责展示授权页和提交用户选择。
  3. 后端 OAuth2OpenController 负责发放授权码、换取令牌和校验令牌。
  4. 后续开放接口通过 Bearer Token 识别用户,再用 scope 控制访问范围。

授权码 SSO 总流程

授权码模式 SSO 整体流程

RuoYi Office 的前后端分离实现和传统 OAuth2 服务端 302 有一点差异:POST /system/oauth2/authorize 返回的是重定向 URL 字符串,sso-login.vue 再执行 location.href = data。这样可以让 Axios 正常接收统一 CommonResult<String>,也便于前端处理自动授权和手动授权两种场景。

第一步:创建 OAuth2 客户端

在管理端进入 系统管理 -> OAuth 2.0 -> 应用管理,新增一个客户端。授权码 SSO 建议先按下面配置:

字段示例说明
客户端编号ruoyioffice-sso-demo第三方应用唯一标识,对应请求里的 client_id
客户端密钥change-me-in-prod只保存在第三方应用后端,用于 Basic Authorization
应用名RuoYi Office SSO 示例授权页展示名称
应用图标上传本地图标授权页展示图标
状态开启禁用后所有授权与换 token 都会失败
访问令牌有效期7200单位秒,按安全等级调整
刷新令牌有效期2592000单位秒,常见为 30 天
授权类型authorization_coderefresh_tokenWeb SSO 至少需要授权码模式;需要续期时加刷新令牌
授权范围user.readuser.write只读用户资料可只给 user.read
自动授权范围user.read可选,历史已授权或自动授权时可跳过确认页
可重定向的 URI 地址http://127.0.0.1:18080/callback必须匹配第三方回调地址前缀

授权码模式客户端配置示例

这张图展示的是客户端配置最容易漏的几个点:授权类型必须包含 authorization_code,需要续期时再加 refresh_token;授权范围至少包含后续要访问接口需要的 scope;回调 URI 要和第三方系统实际 callback 地址保持一致。迁移到 RuoYi Office 时,客户端编号和密钥可以沿用企业自己的命名,不需要使用图里的示例值。

redirectUris 的校验使用前缀匹配:OAuth2ClientServiceImpl#validOAuthClientFromCache 会判断 redirect_uri 是否以客户端配置中的任一地址开头。因此生产环境建议把回调地址配置到具体路径或稳定前缀,不要配置过宽的域名根路径。

第二步:第三方应用跳转授权页

第三方应用未登录时,把用户浏览器跳转到 RuoYi Office 前端的 SSO 授权页:

text
http://127.0.0.1:5666/auth/sso-login
  ?response_type=code
  &client_id=ruoyioffice-sso-demo
  &redirect_uri=http%3A%2F%2F127.0.0.1%3A18080%2Fcallback
  &scope=user.read%20user.write
  &state=from-portal-001

参数说明:

参数是否必填说明
response_type授权码模式固定传 code
client_idOAuth2 客户端编号
redirect_uri授权完成后的回调地址,必须在客户端白名单内
scope建议传多个 scope 用空格分隔,例如 user.read user.write
state强烈建议传第三方应用生成随机值,回调后校验,防止 CSRF 和串单

如果用户尚未登录 RuoYi Office,会先进入普通登录页;登录成功后再回到 /auth/sso-login 继续授权。这一点由前端认证路由和请求拦截器共同完成,接入方无需自己处理平台登录态。

第三方系统未登录首页

第三方系统的未登录页只需要提供一个“跳转统一登录”的入口。真实项目里通常不是一个演示按钮,而是在访问受保护页面时由后端或前端路由守卫自动拼接授权 URL 并跳转。

第三步:用户确认授权

sso-login.vue 会做三件事:

  1. 解析 URL 上的 client_idredirect_uriresponse_typescopestate
  2. 调用 GET /system/oauth2/authorize?clientId=... 获取客户端名称、图标和历史授权 scope。
  3. 调用 POST /system/oauth2/authorize 提交用户勾选结果。

如果传入的 scope 已经全部满足自动授权条件,前端会先以 auto_approve=true 尝试提交授权;后端返回重定向 URL 后会直接跳回第三方应用。否则页面展示复选框,目前内置文案为:

scope授权页文案后端用途
user.read访问你的个人信息允许访问 GET /system/oauth2/user/get
user.write修改你的个人信息允许访问 PUT /system/oauth2/user/update

用户确认授权页面

确认授权页是用户能感知到的安全边界。图里的两个勾选项分别对应 user.readuser.write;如果业务只需要登录后读取用户身份,建议只申请 user.read,降低用户授权和后续审计压力。

更多 scope 可以在 sso-login.vue#formatScope 中扩展展示文案;真正的接口权限建议在后端通过 @PreAuthorize("@ss.hasScope('xxx')") 控制。

第四步:第三方回调接收 code

用户同意授权后,RuoYi Office 后端会创建授权码,并拼接回调地址:

text
http://127.0.0.1:18080/callback?code=24b35f...&state=from-portal-001

授权码回调页面

图中回调页 URL 上出现 code,说明 RuoYi Office 授权端已经完成用户确认并把浏览器跳回第三方系统。接下来应由第三方系统后端换 token,不要让前端直接携带 client_secret 调用 /token

授权码由 OAuth2CodeServiceImpl 生成,默认 5 分钟有效,并且在换取访问令牌时会被删除。第三方应用需要在回调页完成两件事:

  1. 校验回调里的 state 是否等于自己发起授权前保存的值。
  2. code 发送给第三方应用自己的后端,由后端换取 token。

不要在浏览器里保存或使用 client_secret。授权码模式的安全边界是“浏览器拿 code,后端拿 secret 换 token”。

第五步:后端使用 code 换 token

第三方应用后端调用 RuoYi Office 后端:

http
POST /admin-api/system/oauth2/token HTTP/1.1
Host: 127.0.0.1:48080
Authorization: Basic base64(ruoyioffice-sso-demo:change-me-in-prod)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=24b35f...&
redirect_uri=http%3A%2F%2F127.0.0.1%3A18080%2Fcallback&
state=from-portal-001

成功响应的 data 中会包含:

字段说明
access_token访问令牌,访问受保护接口时放到 Authorization: Bearer xxx
refresh_token刷新令牌,用于访问令牌过期后的续期
token_type固定为 bearer
expires_in访问令牌剩余有效秒数
scope本次最终授权范围

OAuth2GrantServiceImpl#grantAuthorizationCodeForAccessToken 会校验三组关键数据:clientId 必须和授权码记录一致、redirect_uri 必须和申请 code 时一致、state 必须一致。任一不一致都会拒绝换 token。

第六步:读取当前用户信息

第三方后端拿到 access_token 后,可以请求 OAuth2 用户接口:

http
GET /admin-api/system/oauth2/user/get HTTP/1.1
Host: 127.0.0.1:48080
Authorization: Bearer 24c1...

该接口由 OAuth2UserController#getUserInfo 提供,需要 user.read scope。返回内容包含后台用户基础信息,并会附带部门和岗位:

信息来源
用户基础资料AdminUserService#getUser
部门DeptService#getDept
岗位PostService#getPostList

授权码模式登录后首页

登录后,第三方系统通常会把 RuoYi Office 返回的用户资料映射到自己的本地会话,例如昵称、部门、岗位或员工编号。图中“修改昵称、刷新令牌、退出登录”等操作分别对应 user.writerefresh_token 和撤销 token 能力。

如需让第三方应用修改用户昵称、头像等资料,可以授权 user.write,然后调用 PUT /system/oauth2/user/update。生产场景更建议把写权限拆细,按业务接口单独定义 scope。

第七步:刷新与退出

如果客户端启用了 refresh_token 授权类型,可以在访问令牌过期后刷新:

http
POST /admin-api/system/oauth2/token HTTP/1.1
Authorization: Basic base64(ruoyioffice-sso-demo:change-me-in-prod)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=8d7b...

退出登录或解除授权时,第三方应用后端可以撤销令牌:

http
DELETE /admin-api/system/oauth2/token?token=24c1... HTTP/1.1
Authorization: Basic base64(ruoyioffice-sso-demo:change-me-in-prod)

管理端 系统管理 -> OAuth 2.0 -> 访问令牌 页面也可以按用户编号、用户类型和客户端编号筛选令牌,并删除单个或批量删除令牌,用于强制下线和应急处置。

开放接口清单

接口调用方用途
GET /system/oauth2/authorizeRuoYi Office 前端获取授权页所需客户端与历史授权信息
POST /system/oauth2/authorizeRuoYi Office 前端提交授权确认,返回重定向 URL
POST /system/oauth2/token第三方应用后端换取或刷新访问令牌,需要 Basic Authorization
DELETE /system/oauth2/token第三方应用后端撤销访问令牌,需要 Basic Authorization
POST /system/oauth2/check-token第三方应用后端校验访问令牌,需要 Basic Authorization
GET /system/oauth2/user/get第三方应用后端获取当前用户信息,需要 Bearer Token 和 user.read
PUT /system/oauth2/user/update第三方应用后端更新当前用户资料,需要 Bearer Token 和 user.write

表结构与持久化

说明
system_oauth2_client客户端配置,包含密钥、授权模式、scope、回调地址和令牌有效期
system_oauth2_code授权码记录,默认 5 分钟有效,换 token 时一次性消费
system_oauth2_approve用户对客户端 scope 的授权记录,用于自动授权判断
system_oauth2_access_token访问令牌记录,Redis 会缓存有效 token
system_oauth2_refresh_token刷新令牌记录,刷新 access token 时复用

OAuth2 表结构

表结构里最重要的关系是:客户端定义允许的授权模式和 scope,授权码记录一次性登录过程,授权记录支撑自动授权,访问令牌和刷新令牌支撑后续接口访问与续期。排查 SSO 问题时,通常按 client -> code -> access_token -> refresh_token 的顺序追。

多租户场景下,OAuth2TokenServiceImpl 会把租户编号写入令牌记录。第三方应用如果跨租户接入,需要明确 tenant-id 传递、客户端归属和用户登录入口,避免不同租户下的授权混用。

生产接入建议

  1. 只在服务端保存密钥client_secret 不要出现在浏览器、移动端包体或公开配置里。
  2. 回调地址尽量精确redirectUris 使用具体域名和路径,减少开放重定向风险。
  3. state 必须校验:第三方应用应为每次授权生成随机 state,回调时核对并销毁。
  4. scope 最小化:只读登录场景优先只授予 user.read;写接口必须单独评估。
  5. 令牌短、刷新长:访问令牌设置较短有效期,刷新令牌按用户体验和风险配置。
  6. 异常时可强制撤销:客户端泄露、员工离职或发现异常访问时,先禁用客户端或删除访问令牌。
  7. 对外 API 单独收敛:如果第三方系统要访问更多业务数据,建议新建 open API,并用独立 scope 控制,不要直接暴露管理端高权限接口。

排查清单

现象排查方向
跳到登录页而不是授权页用户在 RuoYi Office 未登录,先登录后会继续回到 /auth/sso-login
授权页不显示应用信息检查 client_id 是否存在、客户端状态是否开启
点击同意后没有跳转检查 response_type 是否为 coderedirect_uri 是否匹配客户端白名单
token 接口提示客户端无效检查 Basic Authorization 是否为 base64(clientId:secret),密钥是否正确
token 接口提示授权类型无效客户端 authorizedGrantTypes 是否包含 authorization_coderefresh_token
code 换 token 失败检查 code 是否超过 5 分钟、是否已使用、redirect_uristate 是否与申请时一致
读取用户信息提示无权限检查本次授权 scope 是否包含 user.read,接口是否带 Bearer Token
refresh token 失败检查客户端是否允许 refresh_token,刷新令牌是否过期或客户端编号是否一致
多租户下拿不到用户检查登录时的租户、令牌记录的 tenantId 和请求头租户是否一致
联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版