使用OAuth2的通用RESTful业务操作
本文介绍如何在 InterSystems IRIS 中通过继承
EnsLib.HTTP.GenericOperation(或EnsLib.REST.GenericOperation)实现 OAuth2.0 支持,包括 OAuth2.0 Client 配置、Access Token 自动获取与 Header 注入,适用于各类第三方 REST API 集成场景。
在企业集成项目中,我们经常需要通过 REST API 对接第三方平台,例如 CRM、支付系统、云服务和 Open API 网关。
这些接口大多数采用 OAuth 2.0 作为授权机制。
虽然 InterSystems IRIS 提供了功能强大的通用 HTTP / REST 业务操作类:
EnsLib.HTTP.GenericOperationEnsLib.REST.GenericOperation
但目前它们不直接支持 OAuth2.0 Access Token 自动注入。
本文将介绍一种常见且推荐的实现方式:
通过继承 GenericOperation类,自定义一个支持 OAuth2.0 的通用业务操作类(Business Operation)
实现以下能力:
- 自动检查令牌(Token)是否有效
- 自动获访问令牌(Access Token)
- 自动注入授权头(Authorization Header)
- 对业务调用透明
一、先理解 OAuth2.0
先简单解释一下OAuth2.0,如果您已经知道,请直接跳过本章。
需要先明确一个常见概念:
OAuth2.0 不是认证(Authentication),而是授权(Authorization)
也就是说:
OAuth2.0解决的是授权问题,是给予用户访问令牌(access token)让它们可以访问受限的API。
如果是要解决单点登录、用户身份认证问题,那么用的是OpenID Connect。
OAuth2.0和OpenID Connect都提供令牌(token),但目的和类型是不同的:
| 类型 | 标准 | 用途 | 使用方 |
|---|---|---|---|
| 访问令牌(Access Token) | OAuth2 | 访问 API | 资源服务器(Resource Server) |
| 身份令牌(ID Token) | OpenID Connect | 用户身份信息 | 客户端应用(Client Application) |
OAuth2.0有多种授权类型(Grant Type),用于不同类型的应用场景,包括:
| 授权类型 | 访问用户资源 | 需要密钥 | 访问令牌刷新 | 说明 |
| Authorization code | 是 | 是 | 是 | 最常见的授权类型。授权服务器向客户端返回一个仅限一次使用的授权码。随后,客户端将该授权码兑换为访问令牌。 |
| Authorization code with PKCE | 是 | 否 | 是 | 基于代码交换的证明密钥(Proof Key for Code Exchange,简称 PKCE)是一种以安全为核心的 OAuth 授权类型。PKCE 背后的主要概念是“持有证明”。这基本上意味着,客户端应用在从授权服务器获取访问令牌之前,需要向其证明授权码是真实的。PKCE 流程包含代码验证器、代码挑战以及代码挑战方法。 |
| Client credentials | 否 | 是 | 否 | 此授权类型使用客户端凭据从资源服务器访问受保护的数据。它适用于机器间认证。 |
| Implicit | 是 | 否 | 否 | 在用户同意后,客户端应用程序会立即收到访问令牌。这种授权类型常见于客户端设备,因为此类设备无法安全地存储客户端凭据。它安全性弱与其它类型。 |
| Device Code | 该授权类型针对输入或显示能力有限的设备,例如智能电视。设备会向用户显示一个 URL 和一个设备代码。随后,用户在另一台设备上登录并输入该代码,之后该设备便会获取访问令牌。 |
通常OAuth2.0 服务器提供一系列端点(endpoints):
| 端点 | 说明 |
|---|---|
| Authorization(必须的) | 资源所有者向 OAuth 客户端授予访问受保护资源权限的授权 URL |
| Token(必须的) | OAuth 客户端使用授权令牌兑换访问令牌和可选刷新令牌的令牌请求 URL |
| Issuer endpoint(必须的)/Server Discovery | 发现 OAuth 2.0 / OpenID Connect 端点、功能、支持的加密算法和特性 |
| Userinfo | 用户信息端点是一个 OAuth 2.0 受保护资源,用于返回有关经过身份验证的最终用户的声明。这些声明通常由一个 JSON 对象表示,该对象包含每个声明的名称和值对集合。 |
| Token introspection | OAuth 客户端可通过该 URL 检查访问令牌 |
| Token revocation | 可用于撤销已发放给客户端的 OAuth 令牌的 URL |
| End session | 可通过撤销 access_token 来结束会话的 URL。必须在 Authorization 头中提供该令牌,或使用会话 Cookie |
| Client registration | 创建、访问、更新或删除客户端注册 |
| ... |
二、IRIS如何配置OAuth2.0 客户端
在我们介绍的用例中,InterSystems IRIS是作为OAuth 2.0的客户端的,所以我们要先配置OAuth2.0 客户端。
IRIS提供了完整的OAuth2.0客户端注册管理功能和管理页面。进入IRIS系统管理门户(SMP),选择系统管理>安全>OAuth 2.0>客户端
1)先配置相应的OAuth 2.0授权服务器信息
点击创建服务端描述按钮。
- 填写OAuth 2.0颁布者端点(Issuer endpoint),例如https://stg-id.singpass.gov.sg;
- 选择IRIS本地的SSL/TLS配置;
- 如果API颁布者端点同时是发现端点,可以直接点击发现并保存(Discover and Save)按钮,它会自动通过发现发现OAuth 2.0服务器的所有端点和JWT设置等,并自动完成注册。
- 如果API颁布者端点未提供发现端点能力,则点击人工(Edit)按钮,手动输入相应端点,并保存。注意,授权端点(Authorization endpoint)和令牌端点(Token endpoint)是必须要提供的,如下图。
2)然后配置OAuth 2.0客户端信息
在OAuth 2.0服务器描述列表中选择上一步创建的服务器描述,点击客户端配置按钮,然后点击新建客户端配置按钮。
在配置页面输入相关信息,以向OAuth2.0授权服务器提供客户端信息。根据授权服务器端授权类型,选择填写相关配置。我们以Authorization code类型为例,其最重要的是常规(General)和客户端凭据(Client Credentials)设置。
2.1) 在常规(General)页面,以下是必须输入的:
- 应用程序名称(Appplication name),你的应用名称
- 客户端类型(Client Type),客户端类型
- SSL/TLS配置(SSL/TLS configuration),用于加密通信的SSL/TLS设置
- 授予类型(Required grant type),授权服务器的授权类型(Grant Type),示例是Authorization code
- 身份验证类型(Authentication type),客户端需要如何向授权服务器证明自己身份,示例是form encoded body

2.2)在客户端凭据(Client Credentials)页面,以下是必须输入的:
- 客户端 ID(Client ID) ,这是向OAuth 2.0服务器注册你的应用时获得的应用ID
- 客户端密钥(Client secret) ,这是获得的应用密钥

三、创建支持 OAuth2 的通用业务操作类
EnsLib.HTTP.GenericOperation/EnsLib.REST.GenericOperation 非常适合做标准 HTTP/REST 调用,但当目标 API 使用 OAuth2 时,通常需要在请求头中加入:
Authorization: Bearer <access_token>
系统自带的 GenericOperation 并不会自动处理这一过程,因此需要自行实现:
- Token 获取
- Token 缓存
- Token 刷新
- Header 注入
我们可以创建EnsLib.HTTP.GenericOperation/EnsLib.REST.GenericOperation的子类来实现相应OAuth能力。
这个子类里,我们需要实现:
1)增加3个相关配置,这样可以在 Production 页面中直接配置它们:
- OAuth: 是否需要使用OAuth
- ApplicationName:注册过的应用名称。就是上一步注册的OAuth 2.0客户端应用名称
- Scope:需要的应用权限,例如api.read
2)重载OnMessage 方法,将访问令牌(access token)加入HTTP头
示例:
Class QP.REST.GenericOperation Extends EnsLib.HTTP.GenericOperation [ System = 4 ]
{
/// 是否启用 OAuth
Property OAuth As %Boolean [ InitialExpression = 0 ];
/// OAuth Client 名称
Property ApplicationName As %String;
/// Scope
Property Scope As %String(MAXLEN = 200);
Parameter SETTINGS = "ReadRawMode,WriteRawMode,OAuth,ApplicationName,Scope";
/// 获得访问令牌并加入HTTP头,这是整个方案的核心
Method AddOAuthHeader()
{
If (..OAuth=1) && (..ApplicationName '="")
{
// 检查当前 Token 是否有效
Set isAuth=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..ApplicationName,,..Scope,.accesstoken,.idtoken,.responseProperties,.error)
Quit:$isobject(error)
// 如果无 Token,则自动申请
if accesstoken=""
{
Set sc=##class(%SYS.OAuth2.Authorization).GetAccessTokenClient(..ApplicationName,..Scope,.properties,.error,.sessionId)
Set isAuth=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..ApplicationName,,..Scope,.accesstoken,.idtoken,.responseProperties,.error)
Quit:$isobject(error)
}
// 自动加入 Header
Set:(isAuth) sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(..%HttpRequest,,,..ApplicationName)
}
}
/// Invoke a remote HTTP Service given a generic HTTP request
Method OnMessage(pRequest As EnsLib.HTTP.GenericMessage, Output pResponse As EnsLib.HTTP.GenericMessage) As %Status
{
Set tSC=$$$OK, ..%HttpRequest.Location="", ..%HttpRequest.AcceptGzip=0, ..%HttpRequest.FollowRedirect=0, ..%HttpRequest.ReadRawMode=..ReadRawMode, ..%HttpRequest.WriteRawMode=..WriteRawMode
Do ..%HttpRequest.Reset()
Do ..%HttpRequest.RemoveHeader("HOST"), ..%HttpRequest.RemoveHeader("USER-AGENT"), ..%HttpRequest.RemoveHeader("REFERER")
#; Pass along selected HTTP headers
Set (tReq,tURL,tCT,tLen,tNParams,tRawParams,tApp,tCfg)="", tDoNotPassThrough=","_$ZCVT(..%ExcludeOutboundHeaders,"L")_",host,cspapplication,ensconfigname,ensattribute,url,httprequest,httpversion,content-length,content-type,charencoding,translationtable,iparams,params,rawparams,"
Set tHeaderKey="" For { Set tHeaderKey=pRequest.HTTPHeaders.Next(tHeaderKey) Quit:""=tHeaderKey Set tHeaderLwr=$ZCVT(tHeaderKey,"L")
Set tPass=(tDoNotPassThrough'[(","_tHeaderLwr_",")) Set:tPass&&(tHeaderLwr?1"iparams_"1.N) tPass=0
Do:tPass ..%HttpRequest.SetHeader(tHeaderKey,pRequest.HTTPHeaders.GetAt(tHeaderKey)) ; no need to handle multiple on one line
Set:tHeaderLwr="httprequest" tReq=pRequest.HTTPHeaders.GetAt(tHeaderKey)
Set:tHeaderLwr="url" tURL=pRequest.HTTPHeaders.GetAt(tHeaderKey)
Set:tHeaderLwr="content-type" tCT=$TR(pRequest.HTTPHeaders.GetAt(tHeaderKey),$C(34,39)) ; remove possible " and '
Set:tHeaderLwr="content-length" tLen=pRequest.HTTPHeaders.GetAt(tHeaderKey)
Set:tHeaderLwr="iparams" tNParams=pRequest.HTTPHeaders.GetAt(tHeaderKey)
Set:tHeaderLwr="rawparams" tRawParams=pRequest.HTTPHeaders.GetAt(tHeaderKey)
Set:tHeaderLwr="cspapplication" tApp=pRequest.HTTPHeaders.GetAt(tHeaderKey)
Set:tHeaderLwr="ensconfigname" tCfg=pRequest.HTTPHeaders.GetAt(tHeaderKey)
}
// 向http头增加访问令牌
Do ..AddOAuthHeader()
}
四、在 Production 中使用和配置这个业务操作
在将这个业务操作加入在 Production 后,可以通过新增的配置项对它进行配置:

总结
通过继承 EnsLib.HTTP.GenericOperation,我们可以非常优雅地为 通用REST业务操作增加 OAuth2.0 支持。