☰
SpringBoot3+OAuth2 企业开放平台:客户端、Token、Scope、接口审计与第三方接入怎么落地
2026/9/30 2:35:07 网站建设 项目流程

SpringBoot3+OAuth2 企业开放平台:客户端、Token、Scope、接口审计与第三方接入怎么落地

🌐文档地址:https://ruoyioffice.com
👇 文章底部获取源码和演示地址 👇
💬 :17156169080(获取产品咨询)

企业把订单、员工、库存或审批接口开放给第三方时,最危险的做法不是“没有上 OAuth2”,而是只有一个永不过期的固定 Token:合作方是谁、能调什么、何时失效、调用失败后去哪里查,全都说不清。真正可交付的开放平台,至少要把客户端身份、授权范围、令牌生命周期和访问留痕连成一条治理链。

▲ 第三方客户端先经过 Gateway,再凭 OAuth2 客户端护照进入受控 API,所有调用最终落到接口审计

先说明真实边界:当前工程交付的是 OAuth2/SSO 令牌底座,不是完整的开发者开放平台。client_credentials已能换票,但仓库内没有现成的 M2M 业务 API;Scope 仅在用户读写示例接口落地;通用 API 访问日志也不记录clientId。本文既拆解已有能力,也说明从“能换票”走到“可交付开放平台”还必须补什么。

引言:开放 API 为什么不能只发一串密钥

很多项目的第一版对接都很简单:在配置文件里放一个api-key,双方约定请求头,然后开始联调。接口只有两个时看不出问题,接入方增加以后,四个缺口会同时出现:

  • 身份混在一起:多个合作方共用同一串密钥,泄露后无法只停掉其中一家;
  • 权限没有边界:能查订单的 Token 顺手也能改员工,接口权限靠口头约定;
  • 生命周期不可控:Token 永不过期,人员离职或合同终止后仍然有效;
  • 故障无法追溯:只看到接口报错,不知道哪个客户端、哪次请求、耗时多久。

OAuth2 解决的是前三项中的“授权契约”,访问日志解决第四项中的“调用事实”。二者不能只做一半。

这篇文章不再重复浏览器 SSO 的授权码、同意页和单点登录,而是以系统到系统的client_credentials目标链路为主线:先验证现有代码已经做到哪里,再把独立开放 API、显式 Scope 和客户端维度审计补进落地清单。

一、现有 OAuth2 底座已经提供哪些控制面

1. 客户端管理:一套接入方一份契约

OAuth2 Client 不是普通“应用名称”。它至少同时约束:

配置项决定什么配错后的风险
clientId / secret谁在请求 Token多系统共用后无法单独撤销
grantTypes允许怎样换票把 password 模式误开放给第三方
scopes这张票最多申请什么Token 能力超过合作范围
Token 有效期多久必须重新换票时间过长扩大泄露窗口
redirectUris浏览器回调白名单授权码模式可能被劫持

管理端把这些字段放在同一张客户端表单中。client_credentials本身不发生浏览器跳转,但当前统一表单仍要求登记 Redirect URI;它是客户端完整契约的一部分,不代表客户端模式换票时会使用该地址。

▲ 客户端编号、密钥、授权类型、Scope、Access/Refresh 有效期和回调地址集中配置,业务代码不再硬编码合作方密钥

2. Token 管理:访问票必须可查、可撤、可过期

第三方拿到的不是永久密钥,而是一张有期限的访问票。管理端令牌台账可以按用户类型、客户端和过期时间检查当前会话,也可以删除单张 Token。

▲ Token 台账保留 Access、Refresh、客户端编号和过期时间;删除访问票后无需等待自然过期

截图中的令牌来自当前管理端登录,用户编号为1。机器凭证模式在现有实现中会以userId=0创建 Token,不能把这张截图误解为一次真实合作方调用。

3. Scope:声明“这张票能做什么”

完整开放平台的 Scope 应使用业务动作命名,例如:

  • order.read:读取订单;
  • order.write:创建或修改订单;
  • employee.read:读取允许开放的员工字段;
  • invoice.push:推送发票结果。

客户端配置的是“最大集合”,换票请求只能申请它的子集。接口再通过@PreAuthorize("@ss.hasScope('order.read')")声明所需 Scope,才能完成最后一道校验。

但当前仓库真正落地的只有user.read、user.write两个用户信息示例接口。order.read、invoice.push是本文给出的扩展示例,不是现成业务 API。

4. 接口日志:回答“这张票实际做了什么”

授权通过不等于调用成功。现有通用日志会保留 TraceId、请求地址、用户、请求耗时和结果码,但没有clientId与 Scope 字段。出现超时、重复推送或参数争议时,可以先按 TraceId 查调用事实;要按合作方审计,还需扩展客户端维度。

▲ 这是全站通用 API 访问日志,不是 OAuth2 专用审计;它能按 TraceId 追请求,但当前不能直接回答“是哪一个 Client 调用”

二、从“能换票”走到“业务可用”还差什么

第一步:给合作方登记独立 Client

不要用管理端默认 Client,也不要给多个合作方共用 Client。每个接入系统单独配置编号、密钥、授权模式、Scope 和有效期。

对机器接入,建议只开启client_credentials。如果同一个应用还承担用户 SSO,再单独评估是否开启authorization_code,不要为了“以后可能会用”把五种模式全选上。

第二步:使用 Basic Authentication 换取短期 Token

Token 接口从 HTTP Basic Authorization 中读取client_id:client_secret,请求体传授权类型和 Scope:

curl-XPOST"https://your-domain/admin-api/system/oauth2/token"\-u"partner-app:replace-with-secret"\-H"Content-Type: application/x-www-form-urlencoded"\-d"grant_type=client_credentials"\-d"scope=order.read invoice.push"

后端依次确认:

  1. 授权类型是否受支持;
  2. Client 是否存在且启用;
  3. 密钥是否一致;
  4. client_credentials是否在允许模式中;
  5. 本次申请的 Scope 是否为客户端 Scope 的子集。

任意一项失败都不发 Token。

第三步:带 Bearer Token 调受控 API

拿到 Access Token 后,第三方可以使用标准 Bearer 头。下面假设项目新增了一个显式开放的订单接口:

curl"https://your-domain/admin-api/open-api/orders/1001"\-H"Authorization: Bearer replace-with-access-token"

这里有两条必须说清的边界:

  1. 当前工程没有这条/open-api/orders/1001,它是推荐的目标形态;
  2. client_credentials创建的 Token 使用userId=0,而绝大多数现有管理 API 仍走用户 RBAC,因此机器 Token 不能直接当成“全业务通行证”。

真正对外开放的接口应建立独立/open-api/*Controller,显式写 Scope,只返回合作方需要的字段,并为机器主体设计可审计的客户端授权模型。

第四步:按 Client、Token 和 TraceId 追踪

接入出现异常时,排查顺序应固定:

  1. 在客户端管理确认 Client 状态、授权模式和 Scope;
  2. 在令牌管理确认 Token 的客户端、过期时间和是否被撤销;
  3. 在通用访问日志按时间、请求 URL、用户或 TraceId 定位一次调用;
  4. 再进入业务日志和业务单据检查幂等键、参数和结果。

如果要按合作方直接查询,还需先把clientId写入日志上下文和日志表;当前页面并不具备这个维度。

三、设计怎么落地

3.1 四张数据表分别承担什么

▲ Client 保存接入契约,Access/Refresh 保存访问票,API Access Log 保存调用事实;日志不是权限表

关键关系可以概括为:

  • 一个 Client 可以颁发多张 Access Token;
  • Access Token 关联 Client、Scope、主体和过期时间;
  • Refresh Token 用于需要续期的授权模式,client_credentials通常到期后重新换票;
  • API 访问日志记录通用请求事实,与 Token 表不建立强外键,当前也不保存clientId;开放平台若需合作方审计,应异步补充客户端维度。

3.2 换票时先校验客户端契约

下面这段对应第二步。Controller 不自己拼授权逻辑,而是先解析 Grant Type,再统一校验 Client,最后分派给对应授权服务:

List<String>scopes=OAuth2Utils.buildScopes(scope);OAuth2GrantTypeEnumgrantTypeEnum=OAuth2GrantTypeEnum.getByGrantType(grantType);if(grantTypeEnum==null){throwexception0(BAD_REQUEST.getCode(),StrUtil.format("未知授权类型({})",grantType));}String[]client=obtainBasicAuthorization(request);OAuth2ClientDOclientDO=oauth2ClientService.validOAuthClientFromCache(client[0],client[1],grantType,scopes,redirectUri);OAuth2AccessTokenDOaccessToken;switch(grantTypeEnum){caseCLIENT_CREDENTIALS:accessToken=oauth2GrantService.grantClientCredentials(clientDO.getClientId(),scopes);break;caseREFRESH_TOKEN:accessToken=oauth2GrantService.grantRefreshToken(refreshToken,clientDO.getClientId());break;default:thrownewIllegalArgumentException("当前示例只展示机器接入");}

validOAuthClientFromCache的价值在于把五类检查收口。新增开放接口时,不应再复制一套密钥和 Scope 判断。

OAuth2ClientDOclient=getSelf().getOAuth2ClientFromCache(clientId);if(client==null){throwexception(OAUTH2_CLIENT_NOT_EXISTS);}if(CommonStatusEnum.isDisable(client.getStatus())){throwexception(OAUTH2_CLIENT_DISABLE);}if(StrUtil.isNotEmpty(clientSecret)&&ObjectUtil.notEqual(client.getSecret(),clientSecret)){throwexception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR);}if(StrUtil.isNotEmpty(grantType)&&!CollUtil.contains(client.getAuthorizedGrantTypes(),grantType)){throwexception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS);}if(CollUtil.isNotEmpty(scopes)&&!CollUtil.containsAll(client.getScopes(),scopes)){throwexception(OAUTH2_CLIENT_SCOPE_OVER);}returnclient;

3.3 机器身份为什么使用 userId=0

客户端模式没有真实员工。当前实现使用管理员用户类型并把用户编号设为0,用它表达“这是机器主体”:

@OverridepublicOAuth2AccessTokenDOgrantClientCredentials(StringclientId,List<String>scopes){returnoauth2TokenService.createAccessToken(0L,UserTypeEnum.ADMIN.getValue(),clientId,scopes);}@OverridepublicbooleanrevokeToken(StringclientId,StringaccessToken){OAuth2AccessTokenDOtoken=oauth2TokenService.getAccessToken(accessToken);if(token==null||ObjectUtil.notEqual(clientId,token.getClientId())){returnfalse;}returnoauth2TokenService.removeAccessToken(accessToken)!=null;}

这种实现能在 Token 表中区分“用户票”和“机器票”,但还不足以构成可用的服务账号权限体系。审计报表若要精确统计合作方,应把clientId带入审计上下文;仅看userId=0无法区分多个外部系统。

3.4 Scope 必须落到接口声明

Scope 只有写在 Token 里没有意义。接口需要显式声明最小权限:

@RestController@RequestMapping("/system/oauth2/user")publicclassOAuth2UserController{@GetMapping("/get")@PreAuthorize("@ss.hasScope('user.read')")publicCommonResult<OAuth2UserInfoRespVO>getUserInfo(){LonguserId=getLoginUserId();AdminUserDOuser=userService.getUser(userId);returnsuccess(BeanUtils.toBean(user,OAuth2UserInfoRespVO.class));}@PutMapping("/update")@PreAuthorize("@ss.hasScope('user.write')")publicCommonResult<Boolean>updateUserInfo(@Valid@RequestBodyOAuth2UserUpdateReqVOreqVO){userService.updateUserProfile(getLoginUserId(),BeanUtils.toBean(reqVO,UserProfileUpdateReqVO.class));returnsuccess(true);}}

读取和修改拆成两个 Scope,是开放平台最基本的最小权限原则。

3.5 主调用时序

▲ 这是建议补齐后的目标链路:现有代码已覆盖 Basic 换票,显式 M2M 业务 API、完整 Scope 与客户端维度日志仍需建设

四、SSO 与开放平台不要混成一件事

对照项用户 SSO机器开放接入
主体有真实员工第三方应用
推荐模式authorization_codeclient_credentials
是否需要同意页需要或自动同意不需要
是否依赖 redirectUri依赖换票时不依赖
权限表达用户角色 + Scope显式开放 API + Client Scope(待补齐)
到期后处理Refresh Token 静默续期重新用 Client 换票

两者可以复用 Client、Token 和校验服务,但产品入口、风险模型和审计维度不同。当前 M2M 只有协议换票底座,不能把它宣传成已完成的开放 API 产品;同时也不应为了绕过userId=0的 RBAC 边界,强行给合作方创建普通员工账号。

五、上线前必须补齐的安全边界

Client Secret 不能明文散落

当前管理表单能够配置 Secret,但生产环境还应做到:

  • 只在创建或轮换时展示一次;
  • 数据库加密存储或只保存摘要,按协议能力选择;
  • Secret 通过密钥管理或 CI Secret 注入,不进入 Git;
  • 每个合作方有独立轮换与吊销流程。

开放 Controller 不应直接暴露全部管理 API

管理 API 往往包含内部字段、批量导出和越权风险。开放平台应使用独立 VO,只暴露合同约定字段,并为写接口增加业务幂等键。

Scope 不能代替租户和数据范围

order.read只说明能读订单,不说明能读哪个租户、哪个部门、哪家客户。多租户条件、数据权限和业务归属校验仍要执行。

访问日志需要脱敏

密码、Secret、身份证、银行卡和完整 Token 不应原样进入请求参数日志。开放平台的“可审计”不能以泄露敏感信息为代价。

六、快速体验

在线演示地址:

https://ruoyioffice.com/web/

账号:admin
密码:admin123

推荐按以下路径体验:

  1. 系统管理 → OAuth 2.0 → 应用管理,打开新增表单;
  2. 查看授权类型、Scope、Access/Refresh 有效期和回调 URI;
  3. 进入令牌管理,观察 Access Token、客户端编号和过期时间;
  4. 基础设施 → API 日志 → 访问日志,打开一条详情;
  5. 对照 TraceId、请求地址、用户、耗时和结果码。

常见问题(FAQ)

client_credentials 为什么不需要员工账号?

因为授权主体是应用本身。当前实现以userId=0表示机器主体,但现有业务 API 多数依赖用户 RBAC,所以“能换票”不等于“已经能调用业务”。还需要独立开放接口和客户端授权模型。

有了 OAuth2 Scope,还需要 RBAC 吗?

需要。Scope 应管理外部应用能调用哪类开放接口,RBAC 管内部用户角色;租户和数据权限还要继续限制数据范围。当前仓库只有两个 Scope 示例接口,不能把 Scope 描述成已覆盖全站。

Client Secret 可以直接放前端吗?

不可以。浏览器、小程序和 App 都无法真正保密 Client Secret。client_credentials应由可信后端服务调用。

为什么不直接给第三方一个永久 API Key?

短期 Token 可以过期、撤销和审计,Client 也能单独停用。永久 Key 泄露后的风险窗口几乎没有上限。

当前系统是否已经自动保护所有 OpenAPI Scope?

没有。框架提供hasScope能力和示例接口,但每个敏感开放接口仍需显式声明 Scope,并设计独立开放 VO。

结语

企业开放平台的核心不是“支持 OAuth2”这五个字,而是让每次第三方调用都能回答四个问题:谁在调用、能调什么、这张票何时失效、出问题到哪里追。

当前 OAuth2 Client、Token 与 Scope 示例已经提供了可靠起点;补齐独立 M2M API、客户端授权和clientId审计后,第三方接入才会从一次性联调,变成可以持续运营、轮换和审计的企业能力。

如果这篇对你有用,点个「在看」或收藏。

🌐演示地址
https://ruoyioffice.com/web
📦GitHub 源码
https://github.com/yuqing2026/ruoyi-office
📦Gitee 源码
https://gitee.com/yqzy1688/ruoyi-office
💬微信:17156169080(获取产品咨询)

打开演示地址直接查看系统。

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

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

立即咨询