如何通过第三方 .NET 应用中的按钮自动登录 PHP 网站?
结论
优先采用 OpenID Connect(OIDC)实现单点登录。用户在桌面软件中点击按钮,通过浏览器打开网站登录入口,由 PHP 网站完成身份认证,并为浏览器建立网站登录会话。
如果客户已有统一身份提供方(Identity Provider,简称 IdP),先确认它是否支持 OIDC 或 SAML,以及能否对接现有 LDAP 目录。PHP 网站即使没有使用框架,也可以接入这些协议。
桌面软件发送的 Email、Username 或姓名只能用于查找账户,不能作为登录凭据,因为这些字段无法证明请求者就是该用户。
点击后能否直接进入个人区域,取决于浏览器能否利用现有登录状态或企业集成认证完成认证。桌面软件已登录,不代表浏览器也已登录。
OAuth 2.0、OIDC 和 LDAP 各负责什么?
OAuth 2.0 主要处理授权,例如允许桌面软件调用某个用户的受保护 API。网站需要确认当前用户身份时,应使用建立在 OAuth 2.0 之上的 OIDC,它提供身份认证所需的协议和 ID Token。
LDAP 是目录访问协议。即使桌面软件从 LDAP 查到了真实姓名和邮箱,PHP 网站仍需核实这些信息的提供方是否可信,以及信息是否对应当前操作的用户。JSON 是数据格式,cURL 是请求工具,都无法提供这种身份保证。
可以采用以下流程:
.NET 桌面软件打开网站
↓
PHP 网站将浏览器重定向到 IdP
↓
IdP 完成认证,返回授权码
↓
PHP 网站在服务器端兑换并验证身份结果
↓
映射本地账户,建立 PHP Session
↓
浏览器进入个人区域
LDAP 可以由 IdP 在后端对接,目录访问和用户认证都交给 IdP 处理。
谁是 client,应该选择哪种流程?
| 参与方 | 角色 |
|---|---|
| PHP 网站 | OIDC Relying Party,同时也是 OAuth 2.0 client |
| 统一身份提供方 | OpenID Provider,同时承担授权服务器角色 |
| 浏览器 | 承载重定向和网站 Cookie |
| .NET 桌面软件 | 打开网站入口;仅此用途时,无需参与 OAuth 流程 |
网站应使用 Authorization Code Flow,并配合 PKCE。PHP 网站能在服务器端保存凭据,还应按 IdP 的注册配置完成客户端认证。PKCE 和客户端认证各有用途。
传统的“四种流程”在这个场景中的适用范围是:
- Authorization Code Flow 适合网站通过重定向完成登录。
- Implicit Flow 不应作为新登录集成的选择。
- Resource Owner Password Credentials Grant 不应用于让桌面软件收集并转交用户密码。
- Client Credentials Grant 表示应用自身的身份,不能表示某个用户登录。
只有当桌面软件还需要独立调用受保护 API 时,才需要另行注册为 native client,使用 Authorization Code Flow + PKCE。分发到用户电脑上的程序无法可靠地保守内置的 client_secret,应按 public client 设计。
推荐的接入步骤
- 确认客户是否已有 IdP,以及桌面软件目前如何认证用户。仅知道使用了 LDAP,还无法确定浏览器如何实现单点登录。
- 在 IdP 中注册 PHP 网站,配置精确的 HTTPS 回调地址,例如
https://portal.example.com/sso/callback.php。 - 在
/sso/start.php中使用 OIDC 库生成认证请求,将本次流程所需的state、nonce和 PKCEcode_verifier保存在服务端,再重定向浏览器。 - 收到回调后校验
state,使用授权码和code_verifier兑换令牌,再由库验证 ID Token 的签名、iss、aud、有效期、nonce等内容。 - 使用经过验证的
iss与sub组合定位外部身份,找到对应的网站账户,并检查账户是否启用、是否有访问权限。 - 更新 PHP Session ID,写入本地用户 ID,再重定向到个人区域。
解码 JWT 不等于验证其内容。必须完成签名、签发方和接收方等验证,应使用支持相应流程的 OIDC 库,避免自行实现令牌验证。
账户关联规则也要提前确定。邮箱可能变化,不同组织可能使用相同的用户名。已有账户首次绑定外部身份时,应要求用户验证原账户,或由管理员预先建立可信映射,避免按邮箱匹配后直接自动绑定。
代码示例
.NET:通过默认浏览器打开网站入口
在按钮的事件处理代码中执行:
using System.Diagnostics;
Process.Start(new ProcessStartInfo
{
FileName = "https://portal.example.com/sso/start.php",
UseShellExecute = true
});
打开固定的网站地址即可,无需传递用户名、密码或令牌。
网站随后发起 OIDC 登录。如果浏览器已有有效的 IdP 会话,且策略允许复用,用户可能无需再次输入凭据。首次登录、会话过期或需要多因素认证时,用户仍需完成相应交互。
PHP:认证完成后建立本地会话
以下代码只处理会话,不包含 OIDC 回调验证,不能单独作为完整的登录接口使用。
将会话配置放在网站共用的初始化代码中,在任何输出和 OIDC 流程处理之前执行:
<?php
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
完成 OIDC 验证、本地账户映射和访问资格检查后,才能调用以下函数:
<?php
function completeWebsiteLogin(int $localUserId): void
{
if ($localUserId <= 0) {
throw new InvalidArgumentException('Invalid local user ID');
}
if (!session_regenerate_id(true)) {
throw new RuntimeException('Failed to regenerate session ID');
}
$_SESSION = [
'user_id' => $localUserId,
];
header('Cache-Control: no-store');
header('Location: /account/', true, 303);
exit;
}
$localUserId 必须来自服务器端查找到的可信本地账户,不能直接使用 $_GET['user_id']、客户端提交的邮箱或未经验证的令牌字段。
示例中的 SameSite=Lax 适用于常见的跨站顶层 GET 回调。如果使用 response_mode=form_post 等跨站 POST 回调,需要重新评估 Cookie 配置,否则回调时可能无法取得保存 state 的会话。
桌面程序通过 HttpClient 请求登录接口,即使收到成功响应和 Cookie,这些 Cookie 通常也只保存在该 HTTP 客户端中。默认浏览器使用自己的 Cookie 存储,因此仍需让浏览器访问网站,接收网站设置的会话 Cookie。
没有统一 IdP,能否使用一次性登录票据?
可以考虑使用一次性登录票据,前提是双方有明确的信任关系,并单独完成安全设计。
例如,客户有可信后端,且后端已经完成用户认证;网站能够认证该后端,并验证它提供的用户身份。在这个前提下,双方可以实现短期、一次性登录票据:
- 客户后端根据已认证的用户申请票据,网站按事先建立的身份映射确定本地账户。
- 网站生成不可预测的随机票据,在服务器端保存其哈希、目标用户、有效期和使用状态。
- 桌面软件打开网站的票据兑换入口。
- 网站验证并原子性地消费票据,再建立浏览器会话。
有效期可以设为 60 秒,但这只是设计示例,具体时长应根据实际跳转耗时和风险确定。一次性消费必须由数据库事务、条件更新等机制保证,不能先查询,再无条件标记为已使用。
如果客户只有安装在用户电脑上的桌面程序,就不能把程序内置的共享密钥当作可信后端凭据,也不能允许程序提交任意用户名来换取登录票据。
还要防范登录 CSRF。攻击者可能将自己的登录链接发送给其他人,让对方的浏览器进入攻击者的账户。通常应先由浏览器建立登录事务,再将票据绑定到该事务;无法绑定时,需要设置明确的账户确认环节。短期有效和一次性消费本身无法解决这个问题。
这种方案可以满足特定合作系统的需求,但票据泄露、重放、账户绑定和会话安全都需要自行设计处理。已有统一身份系统时,优先沿用 OIDC 或 SAML。
上线前需要注意什么?
- 全程使用 HTTPS,精确校验回调地址。登录后的目标地址使用固定路径或白名单,避免开放重定向。
- 不要将密码、长期访问令牌、ID Token 或 PHP Session ID 放进 URL。一次性票据如果通过 URL 传递,也要控制日志记录、设置
Referrer-Policy,并尽快跳转到不含票据的地址。 - 将
state、nonce和 PKCE 参数绑定到同一次登录事务,并在使用后使其失效。 - 外部认证通过后,网站仍需检查本地账户状态和权限。通过身份认证,不代表用户有权访问个人区域中的所有资源。
- 明确浏览器已有账户与桌面账户不一致时如何处理,避免在用户不知情的情况下切换身份。
- 分别设计网站退出、IdP 退出和桌面软件退出。关闭其中一方的会话,其他会话未必会自动结束。
备注:内容仅供参考。
