用 OAuth2 和 PKCE 让用户登录 Delphi 应用
一个组件、一种授权类型、一次浏览器交接。本页带你从一个空白窗体走到用户已登录、手上握有可用访问令牌的状态,使用的是带 PKCE 的授权码流程(RFC 7636),也就是如今所有提供方对原生桌面应用所期望的流程。
一个组件、一种授权类型、一次浏览器交接。本页带你从一个空白窗体走到用户已登录、手上握有可用访问令牌的状态,使用的是带 PKCE 的授权码流程(RFC 7636),也就是如今所有提供方对原生桌面应用所期望的流程。
与提供方通信的只有一个非可视组件。你不需要 Web 服务器,不需要嵌入式浏览器,也不需要 REST 框架。
TsgcHTTP_OAuth2_Client,声明在单元 sgcHTTP 中,在代码里创建,所有示例都是这么做的。
OAuth2Options.GrantType := auth2CodePKCE。这一次赋值就打开了 PKCE。
Standard、Professional、Enterprise 和 All-Access。客户端不属于 Enterprise 功能,服务器才是。
Windows、macOS、Linux、iOS 和 Android。组件会打开该平台提供的浏览器。
PKCE 之所以存在,是因为桌面应用保不住密钥。它用一个客户端能证明自己在流程开始前就已知晓的值,取代了密钥。
一个高熵随机字符串。sgcWebSockets 向平台的 CSPRNG 取 32 字节并做 Base64URL 编码,得到 RFC 7636 所要求的 43 个字符的 verifier。
对 verifier 取 SHA-256,再做 Base64URL 编码。在授权请求中传输的是 challenge,因此监听重定向的窃听者永远看不到 verifier。
组件用 client_id、redirect_uri、scope、state、code_challenge 和 code_challenge_method=S256 构建授权 URL,然后启动系统浏览器。
授权同意发生在浏览器中,在提供方自己的域名上,使用用户已有的会话、密码管理器和双因素设备。你的应用永远看不到密码。
提供方重定向到你的 redirect_uri,并带上 code 和 state。在桌面端,该 URI 是一个回环地址,组件已经在上面监听。
组件把授权码连同原始的 code_verifier 一起 POST 到令牌端点。提供方重新计算 SHA-256 并比对。匹配成功,你就拿到了访问令牌。
授权码在它存活的那几秒里就是一个持有即可用的凭证。任何能观察到重定向的东西都可能把它偷走,比如注册了同一个自定义 URI scheme 的恶意应用、一个代理,或者一份共享日志。没有 PKCE 的话,偷到的授权码就足以换出一个令牌。
有了 PKCE,除非调用方同时出示那个 SHA-256 哈希与开始时发送的 challenge 相符的 verifier,否则令牌端点会拒绝这个授权码。攻击者只看到了哈希,所以偷到的授权码毫无价值。
这里没有一件事需要你自己写。把 GrantType 设为 auth2CodePKCE,组件就会替你完成第 1、2、3、5 和 6 步。下面是驱动它的代码,以及你确实需要自己拿主意的两个决定:重定向 URI,以及刷新令牌存放在哪里。
# 1. Browser is sent here (query wrapped for reading)
GET https://provider.com/oauth2/authorize
?response_type=code
&client_id=your-client-id
&redirect_uri=http://127.0.0.1:52413/
&scope=openid%20profile
&state=8F3B1C2A-...-9D4E
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
# 2. Provider redirects back to the loopback listener
GET http://127.0.0.1:52413/?code=4/0Ab_5q...&state=8F3B1C2A-...-9D4E
# 3. Component exchanges the code, adding the verifier
POST https://provider.com/oauth2/token
grant_type=authorization_code
&code=4/0Ab_5q...
&redirect_uri=http://127.0.0.1:52413/
&client_id=your-client-id
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
创建组件,选择 auth2CodePKCE,把它指向提供方的两个端点,绑定 OnAfterAccessToken,调用 Start。浏览器打开,用户同意授权,事件带着令牌触发。
uses
Classes, SysUtils,
// sgc
sgcHTTP, sgcHTTP_OAuth_Types;
// OAuth2 is a form field: OAuth2: TsgcHTTP_OAuth2_Client;
procedure TForm1.SignIn;
begin
OAuth2 := TsgcHTTP_OAuth2_Client.Create(nil);
OAuth2.OnAfterAccessToken := OnAfterAccessToken;
OAuth2.OnErrorAccessToken := OnErrorAccessToken;
// PKCE. The verifier and the S256 challenge are generated internally.
OAuth2.OAuth2Options.GrantType := auth2CodePKCE;
OAuth2.OAuth2Options.ClientId := 'your-client-id';
// The two endpoints from the provider's documentation.
OAuth2.AuthorizationServerOptions.AuthURL :=
'https://provider.com/oauth2/authorize';
OAuth2.AuthorizationServerOptions.TokenURL :=
'https://provider.com/oauth2/token';
OAuth2.AuthorizationServerOptions.Scope.Clear;
OAuth2.AuthorizationServerOptions.Scope.Add('openid');
OAuth2.AuthorizationServerOptions.Scope.Add('profile');
// Loopback redirect. Port 0 asks the OS for a free port.
OAuth2.LocalServerOptions.IP := '127.0.0.1';
OAuth2.LocalServerOptions.Port := 0;
OAuth2.Start; // opens the browser and returns immediately
end;
procedure TForm1.OnAfterAccessToken(Sender: TObject; const Access_Token,
Token_Type, Expires_In, Refresh_Token, Scope, RawParams: String;
var Handled: Boolean);
begin
Memo1.Lines.Add('Signed in. Token expires in ' + Expires_In + ' s');
SaveRefreshToken(Refresh_Token); // your own storage, see below
end;
procedure TForm1.OnErrorAccessToken(Sender: TObject; const Error,
Error_Description, Error_URI, RawParams: String);
begin
Memo1.Lines.Add('Sign-in failed: ' + Error + ' / ' + Error_Description);
end;
// include: sgcHTTP.hpp, sgcHTTP_OAuth_Types.hpp
TsgcHTTP_OAuth2_Client *OAuth2 = new TsgcHTTP_OAuth2_Client(this);
OAuth2->OnAfterAccessToken = OnAfterAccessToken;
OAuth2->OnErrorAccessToken = OnErrorAccessToken;
OAuth2->OAuth2Options->GrantType = auth2CodePKCE;
OAuth2->OAuth2Options->ClientId = "your-client-id";
OAuth2->AuthorizationServerOptions->AuthURL =
"https://provider.com/oauth2/authorize";
OAuth2->AuthorizationServerOptions->TokenURL =
"https://provider.com/oauth2/token";
OAuth2->AuthorizationServerOptions->Scope->Clear();
OAuth2->AuthorizationServerOptions->Scope->Add("openid");
OAuth2->AuthorizationServerOptions->Scope->Add("profile");
OAuth2->LocalServerOptions->IP = "127.0.0.1";
OAuth2->LocalServerOptions->Port = 0;
OAuth2->Start();
void __fastcall TForm1::OnAfterAccessToken(TObject *Sender,
const UnicodeString Access_Token, const UnicodeString Token_Type,
const UnicodeString Expires_In, const UnicodeString Refresh_Token,
const UnicodeString Scope, const UnicodeString RawParams, bool &Handled)
{
Memo1->Lines->Add("Signed in. Token expires in " + Expires_In + " s");
}
如果你是从 Web 端的 OAuth2 转过来的,这一部分没有显而易见的答案,而大多数人第一次尝试都会在这里出错。
桌面应用没有可供重定向的域名。公认的做法,也是这个组件所实现的做法,是回环重定向:应用在 127.0.0.1 上启动一个极小的 HTTP 监听器,把该地址注册为重定向 URI,并在授权码一到达就关闭监听器。
LocalServerOptions.IP 默认为 127.0.0.1,LocalServerOptions.Port 默认为 8080。对于正式发布的桌面应用,请改为设置 Port := 0:操作系统会分配一个空闲的临时端口,组件把该端口写进它发送的重定向 URI,于是同一台机器上运行的两份应用永远不会冲突。
如果提供方坚持要求一个精确注册的路径,而不只是主机加端口,就把 LocalServerOptions.RedirectURL 设为你注册的那个值,该字符串会覆盖计算得出的值。固定路径意味着固定端口,所以也要把端口一起注册,并放弃 Port := 0 这个技巧。
监听器只在流程等待期间开启。对于 auth2ClientCredentials、auth2ResourceOwnerPassword 和 auth2DeviceCode 它从不启动,因为这些方式根本不需要重定向。
// Recommended for a shipped desktop app:
// random free port, no collisions, no registration of a port
OAuth2.LocalServerOptions.IP := '127.0.0.1';
OAuth2.LocalServerOptions.Port := 0;
// When the provider requires an exact registered redirect URI:
OAuth2.LocalServerOptions.IP := '127.0.0.1';
OAuth2.LocalServerOptions.Port := 8080;
OAuth2.LocalServerOptions.RedirectURL := 'http://localhost:8080/oauth/';
// Replace the browser page the user is left looking at
OAuth2.OnHTTPResponse := OnHTTPResponse;
procedure TForm1.OnHTTPResponse(Sender: TObject; var Code: Integer;
var Text: String);
begin
Code := 200;
Text := '<html><body>You are signed in. ' +
'Close this tab and return to the app.</body></html>';
end;
OnAfterAccessToken 触发之后,同样的值会以只读属性的形式继续可用,组件还能把它们送给你的 HTTP 和 WebSocket 客户端,你完全不必去碰请求头。
事件参数很方便,但它们不是唯一的副本。只要组件还活着,AccessToken、TokenType、CurrentExpiresIn 和 CurrentRefreshToken 就保存着同样的值,因此你代码别处的处理程序可以直接读取,不必层层传递。
RawParams 是令牌端点返回的原始 JSON 正文。当提供方返回标准集合之外的内容时,比如用于 OpenID Connect 的 id_token,就从这里解析出来。组件不会替你解码 ID 令牌。
要让每个请求都自动携带令牌,把 OAuth2 组件赋给 TsgcHTTP1Client、TsgcHTTP2Client 或 TsgcWebSocketClient 上的 Authentication.Token.OAuth。客户端会代你发送 Authorization: Bearer <token>,并使用提供方返回的那个 token_type。
var
vHTTP: TsgcHTTP1Client;
begin
// Read the tokens at any time after the flow completed
Memo1.Lines.Add(OAuth2.AccessToken);
Memo1.Lines.Add(OAuth2.TokenType); // normally 'Bearer'
Memo1.Lines.Add(IntToStr(OAuth2.CurrentExpiresIn));
Memo1.Lines.Add(OAuth2.CurrentRefreshToken);
// Let the HTTP client attach the Authorization header itself
vHTTP := TsgcHTTP1Client.Create(nil);
vHTTP.Authentication.Token.OAuth := OAuth2;
Memo1.Lines.Add(vHTTP.Get('https://api.provider.com/v1/me'));
end;
访问令牌只活几分钟,刷新令牌能活几周甚至几个月。保住后者,才把一次登录变成一段会话。
在应用的一次运行期间,你什么都不用做。当令牌端点同时返回刷新令牌和 expires_in 时,组件会按大约一半的有效期布置一个内部定时器,定时器触发时发出 grant_type=refresh_token,远早于访问令牌失效。OnAfterRefreshToken 会带着新的令牌对触发,如果提供方拒绝,则触发 OnErrorRefreshToken。请不要动 OnAfterAccessToken 的 Handled 参数:把它设为 True 等于告诉组件由你接管,于是它既不会保存刷新令牌,也不会布置那个定时器。
跨重启则是你的问题,因为只有你知道在用户的机器上可以把密钥写到哪里。把刷新令牌持久化,下次启动时完全跳过 Start,用存下来的值调用 Refresh。浏览器不会打开,用户在你的主窗体绘制出来之前就已经登录。
会轮换刷新令牌的提供方每次续期都会给你一个新的,所以在每次 OnAfterRefreshToken 时都要覆盖你保存的那一份。当存下来的令牌最终被拒绝时,退回到 Start,让用户重新登录。
用 Revoke 让用户正确登出,用 Introspect 询问提供方某个令牌是否仍然有效。两者都需要在 AuthorizationServerOptions 中设置对应的端点。
procedure TForm1.FormCreate(Sender: TObject);
var
vStored: string;
begin
ConfigureOAuth2; // same settings as the QuickStart
OAuth2.OnAfterRefreshToken := OnAfterRefreshToken;
OAuth2.OnErrorRefreshToken := OnErrorRefreshToken;
vStored := LoadRefreshToken;
if vStored <> '' then
OAuth2.Refresh(vStored) // silent, no browser
else
OAuth2.Start; // first run, ask the user
end;
procedure TForm1.OnAfterRefreshToken(Sender: TObject; const Access_Token,
Token_Type, Expires_In, Refresh_Token, Scope, RawParams: String;
var Handled: Boolean);
begin
// providers that rotate hand back a new refresh token
if Refresh_Token <> '' then
SaveRefreshToken(Refresh_Token);
end;
procedure TForm1.OnErrorRefreshToken(Sender: TObject; const Error,
Error_Description, Error_URI, RawParams: String);
begin
ClearStoredRefreshToken;
OAuth2.Start; // the stored token is dead, prompt again
end;
// Signing out
OAuth2.AuthorizationServerOptions.RevocationURL :=
'https://provider.com/oauth2/revoke';
OAuth2.Revoke(OAuth2.CurrentRefreshToken, 'refresh_token');
sgcWebSockets 有意不提供令牌保险库。凭据可以写到哪里,是一个关于你的用户和你的部署方式的决定,因此这个库把令牌交给你之后就到此为止。
它几分钟就过期,而刷新令牌随时能换出新的。没有任何理由把它写到磁盘上,倒是有充分理由不写。
在 Windows 上,DPAPI(CryptProtectData)把密文绑定到 Windows 账户,因此被复制走的文件在另一台机器上毫无用处。macOS 有 Keychain,现代 Linux 桌面则有 Secret Service。
分发出去的可执行文件里的任何东西都是公开的,这正是 PKCE 的全部前提。如果你的提供方给桌面客户端签发了一个密钥,请把它当作标识符,而不是当作保护手段。
放在按用户划分的应用数据目录里,不要放在 Program Files,不要放在可执行文件旁边,不要放在共享网络路径上,也不要放进提交到源码管理的明文 INI 文件里。
调用 Revoke 让提供方作废该令牌,然后删除本地保存的副本。一个已被吊销却仍留在磁盘上的令牌,照样会成为审计问题。
HTTPClientOptions.LogOptions 会把与授权服务器之间的流量写入文件。在你把流程调通的过程中它非常有用,同时它也是一个装满令牌的文件。发布之前请把它关掉。
每一家 OAuth 2.0 提供方要的都是同样的少数几项设置:两个端点、一个 client id、若干 scope,以及一个注册好的重定向地址。Google 和 Microsoft 另外还有现成组件,它们会把端点填好,并直接返回一份用户资料。
TsgcHTTP_OAuth2_Client_Google 和 TsgcHTTP_OAuth2_Client_Microsoft 都派生自同一个基础组件,并且预先填好了端点。它们的 Authenticate 方法是阻塞式的:它跑完整个流程,等待浏览器往返,然后返回一个带有 Authenticated 和已填充 UserProfile 的数据对象。
这是回答“这个用户是谁”的最短路径。TsgcOAuth2_Google_Data.UserProfile 携带 _Name、Given_Name、Family_Name、Id、Locale 和 Picture。TsgcOAuth2_Microsoft_Data.UserProfile 携带 DisplayName、GivenName、Surname、Mail、JobTitle、OfficeLocation 等等。Microsoft 的 Authenticate 第一个参数是租户 id。
对于其他所有提供方,使用基础的 TsgcHTTP_OAuth2_Client,把两个 URL 从它们的文档里抄过来即可。此后就没有任何与提供方相关的差异了。
uses
sgcHTTP, sgcHTTP_OAuth2_Client_Google;
var
vClient: TsgcHTTP_OAuth2_Client_Google;
vData: TsgcOAuth2_Google_Data;
begin
vClient := TsgcHTTP_OAuth2_Client_Google.Create(nil);
try
vData := vClient.Authenticate('client-id', 'client-secret');
if vData.Authenticated then
begin
ShowMessage(vData.UserProfile._Name);
ShowMessage(vData.AccessToken);
end;
finally
vClient.Free;
end;
end;
| 提供方 | 组件 | 授权类型 | 重定向 | 客户端密钥 |
|---|---|---|---|---|
TsgcHTTP_OAuth2_Client_Google 或基础客户端 |
auth2CodePKCE |
回环地址,Port := 0 |
会为桌面客户端签发,如果你有就填上 | |
| Microsoft Entra ID | TsgcHTTP_OAuth2_Client_Microsoft 或基础客户端 |
auth2CodePKCE |
回环地址,注册为移动 / 桌面平台 | 公共客户端不使用,留空即可 |
| Auth0、Okta、Keycloak、AWS Cognito | TsgcHTTP_OAuth2_Client |
auth2CodePKCE |
回环地址,在应用上注册 | 取决于该应用是公共客户端还是机密客户端 |
| 后台作业与服务 | TsgcHTTP_OAuth2_Client |
auth2ClientCredentials |
无,不涉及浏览器 | 必需,而且是安全的,因为没有任何东西被分发出去 |
| 自助终端、电视、无界面设备 | TsgcHTTP_OAuth2_Client |
auth2DeviceCode(RFC 8628) |
无,用户在手机上完成 | 通常不需要 |
Gmail 和 Microsoft 365 已经不再接受通过 SMTP、IMAP 和 POP 提交的密码。替代方案就是你刚刚拿到的那个访问令牌,通过 SASL XOAUTH2 机制出示。
获取令牌就是上面那套流程:auth2CodePKCE、一个回环重定向,以及在 AuthorizationServerOptions.Scope 中加入诸如 https://mail.google.com/ 这样的邮件 scope。邮件这个场景不会改变 OAuth2 那一侧的任何东西。
出示令牌是另一半工作。sgcIndy 在单元 IdSASLXOAUTH2 中提供了 TIdSASLXOAUTH2。把它加入 TIdSMTP.SASLMechanisms,设置 AuthType := satSASL,并在它的 OnAuthenticate 事件里提供用户名和访问令牌。同一套机制也适用于 TIdIMAP4 和 TIdPOP3。
请在心里把这两个组件分开:OAuth2 客户端知道如何获取和续期令牌,SASL 机制知道如何出示令牌。两者都不需要知道对方的存在。
uses
IdSMTP, IdSASLXOAUTH2;
var
vSASL: TIdSASLXOAUTH2;
vSMTP: TIdSMTP;
begin
vSASL := TIdSASLXOAUTH2.Create(nil);
vSASL.OnAuthenticate := OnXOAuth2Authenticate;
vSMTP := TIdSMTP.Create(nil);
vSMTP.AuthType := satSASL;
vSMTP.SASLMechanisms.Clear;
vSMTP.SASLMechanisms.Add.SASL := vSASL;
end;
procedure TForm1.OnXOAuth2Authenticate(Sender: TObject;
var Username: string; var Token: string);
begin
Username := 'user@example.com';
Token := OAuth2.AccessToken; // from the PKCE flow above
end;
上面讲的全都是客户端。只有当签发令牌的人是你自己时,才需要后面这一半。
如果你是把用户登录到别人的身份提供方,比如 Google、Microsoft、Auth0、Okta、Keycloak、AWS Cognito,或者你公司自己的 SSO,那么除了 TsgcHTTP_OAuth2_Client 之外你什么都不需要。该组件已编译进 Standard 版本以及它之上的每一个版本。它也可以单独通过 sgcAuth 包获得,并附带它所需的运行时。
这是常见的情形,也是本页到目前为止的全部内容。
只有当你自己的应用就是授权服务器时,才需要服务器这一半:由你签发 client id,由你托管登录页面,由你签发和吊销随后被你的 API 信任的访问令牌。那就是 TsgcHTTP_OAuth2_Server,挂接在 TsgcWebSocketHTTPServer 上,属于 Enterprise 组件。
它默认校验 PKCE。OAuth2Options.PKCE 开箱即为 True,因此发送了 challenge 的客户端必须拿出匹配的 verifier,拿不出的会被拒绝。用 Apps.AddApp 注册客户端应用,在 OnOAuth2Authentication 中认证用户,用 AddToken 在重启后恢复令牌。
同一个 Enterprise 层级还包含 TsgcHTTP_JWT_Server,用于在你的端点上校验 JWT bearer 令牌,以及用于 passkey 的 TsgcWSAPIServer_WebAuthn。对应的客户端,也就是 TsgcHTTP_OAuth2_Client 和 TsgcHTTP_JWT_Client,从 Standard 起就有。客户端和服务器处在不同的版本层级,在围绕其中任何一个做规划之前,值得先确认这一点。
uses
sgcWebSocket, sgcWebSocket_Classes, sgcHTTP,
sgcHTTP_OAuth_Types, sgcHTTP_OAuth2_Server;
var
vOAuth2: TsgcHTTP_OAuth2_Server;
vServer: TsgcWebSocketHTTPServer;
begin
vOAuth2 := TsgcHTTP_OAuth2_Server.Create(nil);
vOAuth2.OAuth2Options.PKCE := True; // default
vOAuth2.OnOAuth2Authentication := OnOAuth2Authentication;
vOAuth2.Apps.AddApp('MyDesktopApp', 'http://127.0.0.1:8080',
'my-client-id', 'my-client-secret', 3600, True,
[auth2Code, auth2CodePKCE]);
vServer := TsgcWebSocketHTTPServer.Create(nil);
vServer.Authentication.Enabled := True;
vServer.Authentication.OAuth.OAuth2 := vOAuth2;
vServer.Port := 8080;
vServer.Active := True;
end;
procedure TForm1.OnOAuth2Authentication(Connection: TsgcWSConnection;
OAuth2: TsgcHTTPOAuth2Request; aUser, aPassword: String;
var Authenticated: Boolean);
begin
Authenticated := CheckUserInYourDatabase(aUser, aPassword);
end;
桌面端 OAuth2 第一次失败的原因,几乎都逃不出这六种。
redirect_uri_mismatch组件发送的 URI 必须与你注册的完全一致,逐字符相同,包括末尾的斜杠和端口。如果你注册的是一个固定 URI,就把 LocalServerOptions.RedirectURL 设为那个字符串本身,而不要依赖计算出来的值。如果提供方允许任意回环端口,就使用 Port := 0 并只注册主机部分。
要么端口被别的东西占着,要么某条防火墙规则挡住了回环监听器。设置 Port := 0,并检查上一次流程是用 Stop 结束的,而不是被留在监听状态。
invalid_grant授权码只能使用一次,而且存活时间很短。在重定向和交换之间下断点调试,会让授权码过期。请从 OnErrorAccessToken 读取失败信息,它会给你提供方自己的 error 和 error_description,不要靠猜。
提供方只有在你提出要求时才会签发。Google 要的是 access_type=offline,Microsoft 要的是 offline_access scope。把该 scope 加进 AuthorizationServerOptions.Scope,或者在 OnBeforeAuthorizeCode 中修改 URL 参数来追加查询参数。
令牌交换是一次 HTTPS POST,因此需要一个可用的 TLS 后端。HTTPClientOptions.TLSOptions.IOHandler 负责选择:iohOpenSSL、Windows 上无需部署 DLL 的 iohSChannel,或者 Enterprise 版本中的原生 iohAndroidTLS 和 iohAppleTLS 处理器。
处理 OnBeforeAuthorizeCode,设置 Handled := True,然后把你自己的 TsgcWebView2 或 TWebBrowser 导航到传给你的 URL。回环监听器仍然会接住重定向。请注意,现在有几家提供方拒绝在嵌入式浏览器里渲染它们的授权同意页面。
开发者在动手之前真正会去搜索的那些问题。
TsgcHTTP_OAuth2_Client,设置 OAuth2Options.GrantType := auth2CodePKCE,填写 OAuth2Options.ClientId、AuthorizationServerOptions.AuthURL、AuthorizationServerOptions.TokenURL 和 AuthorizationServerOptions.Scope,把 LocalServerOptions.IP 设为 127.0.0.1,把 LocalServerOptions.Port 设为 0,然后调用 Start。组件会生成 PKCE 相关的值,打开浏览器,在回环监听器上接住重定向,交换授权码,并带着令牌触发 OnAfterAccessToken。GrantType 为 auth2CodePKCE 时,TsgcHTTP_OAuth2_Client 会从平台的密码学随机源取 32 字节,用 Base64URL 编码成 43 个字符的 code verifier,把 code challenge 设为该 verifier 的 SHA-256 哈希的 Base64URL 编码,并把 code_challenge_method 固定为 S256。verifier 私密地保存在组件内部,并在令牌交换时重放,因此它绝不会出现在重定向里。如果你出于其他目的想手工构造这一对值,同样的原语也是公开的:单元 sgcCrypto_Random 中的 sgcRandomBytes,以及单元 sgcBase_Helpers 中的 GetHashSHA256 和 EncodeBase64URL。TsgcHTTP_OAuth2_Client 只在流程运行期间,在 LocalServerOptions.IP 和 LocalServerOptions.Port 上启动一个小型 HTTP 监听器,它发送的重定向 URI 就由这两个值构建。默认值是 127.0.0.1 和端口 8080。对于正式发布的应用,请把 Port 设为 0,让操作系统挑一个空闲的临时端口,这样两个实例就永远不会争抢同一个端口。如果提供方要求一个精确注册的 URI,把那个字符串放进 LocalServerOptions.RedirectURL,它会覆盖计算得出的值。OAuth2Options.ClientSecret 留空。有些提供方仍然会给桌面客户端签发一个,并期望在令牌请求中收到它。遇到这种情况就设上,但请把它当作标识符而不是保护手段,因为分发出去的可执行文件里的任何东西都可以被提取出来。Refresh,而不是调用 Start。你可以从 OnAfterAccessToken 的 Refresh_Token 参数读取它,或者稍后从 CurrentRefreshToken 属性读取。每次 OnAfterRefreshToken 时都要覆盖保存的副本,因为会轮换刷新令牌的提供方会让旧的失效。在一次运行之内你完全不用做任何事:组件会根据 expires_in 的值布置定时器,自行续期访问令牌。HTTPClientOptions.LogOptions,因为那份日志里含有令牌。https://mail.google.com/,然后通过 SASL XOAUTH2 出示它。sgcIndy 在单元 IdSASLXOAUTH2 中提供了 TIdSASLXOAUTH2。把它加入 TIdSMTP.SASLMechanisms,设置 AuthType := satSASL,并在它的 OnAuthenticate 事件中返回用户名和访问令牌。同一套机制也能为 TIdIMAP4 和 TIdPOP3 完成认证。TsgcHTTP_OAuth2_Server。它通过 OAuth2Options.PKCE 默认校验 PKCE,用 Apps.AddApp 注册应用,并通过 Authentication.OAuth.OAuth2 挂接到 TsgcWebSocketHTTPServer。OnBeforeAuthorizeCode,它会以 var 参数的形式收到构建完成的授权 URL,设置 Handled := True 让组件不去启动系统浏览器,然后把 TsgcWebView2 之类的嵌入式控件导航到那个 URL。回环监听器仍然会收到重定向,流程正常结束。请注意,现在有几家提供方在嵌入式浏览器中屏蔽了它们的授权同意页面,这也是系统浏览器成为默认选项的原因。TsgcHTTP_OAuth2_Client 可为 Windows、macOS、Linux、iOS 和 Android 编译,支持 VCL、FireMonkey 和 Lazarus / FPC,覆盖从 Delphi 7 到 Delphi 13 以及对应的 C++ Builder 版本。打开浏览器时使用该平台自带的方式。唯一与平台相关的选择是令牌交换所用的 TLS 后端,通过 HTTPClientOptions.TLSOptions.IOHandler 选定。组件参考文档、可直接运行的示例工程,以及比本页讲得更深的技术文档。
| 在线帮助,TsgcHTTP_OAuth2_Client 客户端组件的每一个属性、方法和事件,附带授权码 + PKCE 专题。 | 打开 | |
| 在线帮助,带 PKCE 的授权码 授权类型专题:PKCE 做了什么、配置表格,以及随机端口的建议。 | 打开 | |
| 示例工程,Demos\20.HTTP_Protocol\02.OAuth2_Authentication 客户端与服务器工程,内置可用的 Gmail、Google Pub/Sub、Azure AD、AWS Cognito、Dropbox 和 Auth0 预设,另有一个嵌入式浏览器版本。 | 打开 | |
| 技术文档,OAuth2 客户端(PDF) 功能介绍、快速入门、全部授权类型,以及 Delphi、C++ Builder 和 .NET 的代码示例。 | 打开 | |
| 技术文档,OAuth2 服务器(PDF) Enterprise 版的授权服务器组件:端点、应用注册、PKCE 校验与令牌生命周期。 | 打开 | |
| 用户手册(PDF) 覆盖库中每一个组件的完整手册。 | 打开 |
组件页面列出完整的功能清单,文章则覆盖本页只是点到为止的那些情形。
本页属于 Delphi 使用场景之一,每一篇都把一个任务从头做到尾。目前还有 从 Delphi 调用 LLM,以及 用 WebRTC 让两个应用点对点连接。