OAuth 2.0
这篇解决什么
要把管理端登录态发给其它系统,或给第三方换一张带 scope 的令牌。读完能新增客户端、走授权码 / 密码换票、在开放接口上挂 @ss.hasScope,以及在令牌页把会话踢掉。
默认端口 48080,管理端前缀 /admin-api。账号密码登录也走同一套访问令牌,不是另一套 JWT。
示意图:请求先过过滤器解析 Token,再进 system 模块的授权与令牌接口。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 开放授权 | @RequestMapping("/system/oauth2"),发码、换票、校验 | ruoyi-office/yudao-module-system/.../controller/admin/oauth2/OAuth2OpenController.java |
| 客户端 | @RequestMapping("/system/oauth2-client") | 同目录 OAuth2ClientController.java |
| 令牌页 | @RequestMapping("/system/oauth2-token"),只列未过期 | 同目录 OAuth2TokenController.java |
| 用户信息 | @ss.hasScope('user.read') / user.write | 同目录 OAuth2UserController.java |
| 应用页 | 菜单「应用管理」,路由 /system/oauth2/oauth2/application | ruoyi-office-vben/apps/web-antd/src/views/system/oauth2/client/ |
| 令牌页 | 菜单「令牌管理」,路由 /system/oauth2/token | 同目录 token/ |
| SSO 页 | /auth/sso-login | .../views/_core/authentication/sso-login.vue |
授权模式
枚举在 OAuth2GrantTypeEnum。换票走 POST /system/oauth2/token;简化模式不走这个接口,只走授权页的 response_type=token。
示意图:机器用客户端模式;人且第三方必须授权码;人且自家系统可用密码或授权码。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
authorization_code | 授权码。第三方先拿 code,再在服务端换票 | OAuth2GrantTypeEnum.AUTHORIZATION_CODE |
password | 密码。客户端直接拿账号密码换票 | PASSWORD |
client_credentials | 客户端。机器调用,用户编号写成 0 | CLIENT_CREDENTIALS |
implicit | 简化。授权完成后前端直接拿到 Token | IMPLICIT |
refresh_token | 用刷新令牌换新的访问令牌 | REFRESH_TOKEN |
换票时必须带客户端身份:Authorization: Basic base64(client_id:client_secret),或表单字段 client_id + client_secret。解析在 HttpUtils.obtainBasicAuthorization。
Token 接口不接受 implicit
grant_type=implicit 会直接 400。简化模式只允许授权页 response_type=token,由 approveOrDeny 拼重定向 URL。
内部登录与外部授权
管理后台自己登录,不走密码模式。AdminAuthServiceImpl 登录成功后,用内置 client_id = default 调 createAccessToken。其它系统要来拿用户,走授权码 SSO。
示意图:内部登录和外部授权最后都落到 system_oauth2_access_token。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 登录 | @RequestMapping("/system/auth"),POST /login,@PermitAll | .../controller/admin/auth/AuthController.java |
| 默认客户端 | OAuth2ClientConstants.CLIENT_ID_DEFAULT = "default" | yudao-module-system-api/.../enums/oauth2/OAuth2ClientConstants.java |
| 过期时间 | 读该客户端的 accessTokenValiditySeconds / refreshTokenValiditySeconds | OAuth2ClientDO |
default 的访问令牌默认 1800 秒,刷新令牌 2592000 秒。改客户端配置即改全站登录时效。
为什么登录也复用 OAuth 令牌
登出、改密、令牌页删除都会同时清库和 Redis,会话立刻作废。无状态 JWT 做不到这一点。
应用与令牌
先在应用管理里登记客户端,再让接入方用它的 clientId / secret 换票。令牌管理只展示未过期的访问令牌,删除会走 AdminAuthService.logout。

本系统截图:系统管理 → OAuth 2.0 → 应用管理。内置 default 供管理端登录;其它行是接入方客户端。

本系统截图:系统管理 → OAuth 2.0 → 令牌管理。删除一行等于踢掉该会话。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 客户端字段 | authorizedGrantTypes、scopes、redirectUris、autoApproveScopes | .../dal/dataobject/oauth2/OAuth2ClientDO.java |
| 客户端缓存 | @Cacheable(cacheNames = "oauth_client", key = "#clientId") | .../service/oauth2/OAuth2ClientServiceImpl.java |
| 权限标识 | system:oauth2-client:create 等 | OAuth2ClientController |
| 令牌删除 | system:oauth2-token:delete | OAuth2TokenController |
| 字典 | 授权类型 system_oauth2_grant_type | 应用表单 client/data.ts |
validOAuthClientFromCache 会核状态、密钥、授权类型、scope 是否越权,以及回调地址是否匹配。
回调地址校验两套规则
授权时 redirect_uri 按前缀匹配 redirectUris(startWithAny)。换票时必须和授权码上记下的地址完全相等。多写一个斜杠就会 400。
改客户端后缓存会 @CacheEvict。OAuth2ClientDO 标了 @TenantIgnore,客户端配置跨租户共用。
授权码 SSO
接入方把用户送到 /auth/sso-login,带上 response_type=code、client_id、redirect_uri、scope、可选 state。未登录会先落到 /auth/login,登完再回到授权页。
示意图:授权页只返回重定向 URL,不让 Axios 跟 302。接入方服务端再拿 code 换票。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 授权信息 | GET /system/oauth2/authorize?clientId= | OAuth2OpenController#authorize |
| 申请授权 | POST /system/oauth2/authorize,scope 是 JSON 对象 | approveOrDeny |
| 换票 | POST /system/oauth2/token | postAccessToken |
| 授权码 | 表 system_oauth2_code,用一次即删 | OAuth2CodeServiceImpl |
| 批准记录 | 表 system_oauth2_approve,默认 30 天 | OAuth2ApproveServiceImpl |
| 前端调用 | getAuthorize / authorize | .../api/system/oauth2/open/index.ts |
SSO 页若 URL 已带 scope,会先 auto_approve=true 试一次。全部命中 autoApproveScopes,或用户 30 天内同意过,就不再弹勾选。否则展示「访问你的个人信息」「修改你的个人信息」,对应 user.read / user.write。
用户点「同意授权」后,后端发码并返回
{redirect_uri}?code={授权码}&state={state}接入方服务端再换票。code、redirect_uri、state 必须和发码时一致,client_id 也必须对上。
POST /admin-api/system/oauth2/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=...&redirect_uri=...&state=...成功体是 CommonResult,data 里有 accessToken、refreshToken、expiresTime。后续请求:
Authorization: Bearer {accessToken}自动授权和手动授权
auto_approve=true 只检查历史批准和客户端 autoApproveScopes,失败返回空 URL,前端停在勾选页。false 才按用户勾选写入 system_oauth2_approve。
开放给第三方的用户接口用 scope,不用菜单权限:
@GetMapping("/get")
@PreAuthorize("@ss.hasScope('user.read')")
public CommonResult<OAuth2UserInfoRespVO> getUserInfo() { /* ... */ }
@PutMapping("/update")
@PreAuthorize("@ss.hasScope('user.write')")
public CommonResult<Boolean> updateUserInfo(@Valid @RequestBody OAuth2UserUpdateReqVO reqVO) { /* ... */ }hasScope 读的是 LoginUser.scopes,来自令牌,不是角色菜单。
密码模式
自家 App 或内部页面可以直接拿账号密码换票。第三方不要用:它会见到明文密码。
示意图:密码模式没有授权码,也没有 SSO 勾选页。
POST /admin-api/system/oauth2/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=admin&password=...&scope=user.read user.writeOAuth2GrantServiceImpl#grantPassword 调 AdminAuthService.authenticate,用户类型固定管理端。刷新用 grant_type=refresh_token;作废用 DELETE /system/oauth2/token?token=。
客户端模式给机器用:grant_type=client_credentials,生成的令牌 userId = 0。
Token 校验
每个 /admin-api/**、/app-api/** 请求都会进 TokenAuthenticationFilter。过滤器先看网关透传的 login-user 头;没有再从 Authorization 取 Bearer。
示意图:热路径只读 Redis。Key 是令牌本身,租户在 JSON 里。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 过滤器 | 校验 Token,写入 LoginUser | ruoyi-office/yudao-framework/yudao-spring-boot-starter-security/.../filter/TokenAuthenticationFilter.java |
| Redis Key | oauth2_access_token:%s,TTL 等于剩余秒数 | .../dal/redis/RedisKeyConstants.java |
| RedisDAO | StringRedisTemplate + JsonUtils | .../dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java |
| 回源 | 先访问令牌表,没有再把未过期刷新令牌当访问令牌 | OAuth2TokenServiceImpl#getAccessToken |
| 请求头 | Authorization: Bearer {accessToken} | SecurityProperties.tokenHeader |
积木报表、WebSocket 往往只能带一个 token。刷新令牌未过期时,getAccessToken 会把它当成访问令牌用,避免再传 refresh_token。
本地联调可开模拟 Token。yudao.security.mock-enable: true 时,Bearer test1 表示用户 1。生产必须关掉。
删令牌要库和 Redis 一起清
removeAccessToken 会删访问令牌、刷新令牌,并 oauth2AccessTokenRedisDAO.delete。只删表或只删 Key,过滤器仍可能放行。
表与关键类
五张表。客户端是配置;批准是用户勾选记录;授权码短命;访问令牌和刷新令牌带租户。
按本仓库生成的 ER:client_id 存的是客户端编号字符串,不是表主键 id。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
system_oauth2_client | 客户端。@TenantIgnore | OAuth2ClientDO |
system_oauth2_access_token | 访问令牌。userInfo 给 LoginUser | OAuth2AccessTokenDO |
system_oauth2_refresh_token | 刷新令牌,有效期更长 | OAuth2RefreshTokenDO |
system_oauth2_code | 授权码,5 分钟、一次性 | OAuth2CodeDO |
system_oauth2_approve | 用户是否接受某个 scope | OAuth2ApproveDO |
| 授予 | 按模式发码或发令牌 | OAuth2GrantServiceImpl |
| 令牌 | 创建、刷新、校验、删除 | OAuth2TokenServiceImpl |
@TableName("system_oauth2_access_token") 的 scopes、userInfo 用 JacksonTypeHandler。userType 区分管理端和会员。客户端模式没有真实用户,不要按 userId 去查员工档案。
校验、刷新、删除也可以走开放接口:POST /system/oauth2/check-token、DELETE /system/oauth2/token,同样要带客户端 Basic。
配置与操作
管理员在 系统管理 → OAuth 2.0 → 应用管理 建客户端:clientId、密钥、授权模式、回调。保存后立刻能换码。令牌页只做查询和踢线,不改客户端。
改回调地址或授权模式要保存客户端,已发出的 Token 仍有效到过期。开启见 系统功能设计。截图见上文应用和令牌列表。
