OAuth 2.0 概述
RFC 6749——《OAuth 2.0 授权框架》规定了一种通过第三方应用程序对服务进行授权的协议。 OAuth 2.0 通过令牌将授权过程从服务和用户中抽象出来。由于服务所有者无需处理用户凭据,因此这种方法更加安全。它取代了RFC 5849中的 OAuth 1.0。
OAuth 2.0 框架定义了两种客户端类型(公共客户端和私有客户端),以及授权代码流、隐式代码授权流等多种授权流程。典型的 Qt 应用程序被视为公共原生应用程序。 公共客户端应用程序是指无法被信任将敏感信息(如密码)存储在随应用发布的二进制文件中。
RFC 8252《适用于原生应用的 OAuth 2.0》进一步定义了原生应用的最佳实践。具体而言,RFC 8252 推荐采用基于浏览器的授权流程。因此,QtNetworkAuth 类提供了该流程的具体实现。
作为 Qt 6.9 的新功能,QtNetworkAuth 提供了对RFC 8628(OAuth 2.0 设备授权授予)的支持。此设备流程适用于输入能力有限或不切实际的设备。 在此授权流程中,授权授予将使用智能手机等辅助设备,而非目标设备本身。此类设备的示例包括电视、媒体控制台、机器人机界面(HMI)以及物联网(IoT)设备。用户随后可通过智能手机上的应用程序对目标设备进行授权。
下表列出了Qt Network 授权所支持的两种OAuth 2.0流程:
| 方面 | 授权码流程 | 设备授权流程 |
|---|---|---|
| 网络连接 | 是 | 是 |
| 用户交互 | 同一设备上的浏览器/用户代理 | 位于不同设备上的浏览器/用户代理 |
| 需要处理重定向 | 是 | 否 |
| 设备上的输入功能 | 丰富的输入功能 | 输入功能有限或无 |
| 目标 | 桌面和移动应用 | 电视、游戏主机、人机界面、物联网设备 |
OAuth 2.0 要求使用用户代理,通常为浏览器。有关详细信息,请参阅《Qt OAuth2 浏览器支持》。
OAuth 2.0 类
Qt Network Authorization 提供了具体的和抽象的 OAuth 2.0 类。抽象类用于实现自定义流程,而具体类则提供了具体的实现。
有关 C++ 类的列表,请参阅QtNetworkAuth 页面。
Qt Network “授权”模块提供了两个用于实现 OAuth 2.0 流程的抽象类:
- OAuth 2.0 流程实现类提供主要 API,并作为流程的协调者。其抽象类为QAbstractOAuth2 ,具体实现类包括QOAuth2AuthorizationCodeFlow 和QOAuth2DeviceAuthorizationFlow 。
- 一个响应处理程序类,用于处理来自授权服务器的重定向和响应。响应处理程序的抽象类是QAbstractOAuthReplyHandler ,其具体实现类为QOAuthHttpServerReplyHandler 和QOAuthUriSchemeReplyHandler 。这些响应处理程序的主要区别在于它们处理的重定向类型。QOAuth2AuthorizationCodeFlow 使用响应处理程序来处理重定向,而QOAuth2DeviceAuthorizationFlow (不基于重定向)则不使用响应处理程序。
授权码流程
本节概述了RFC 6749《授权码》和RFC 8252《来自原生应用的授权请求》中针对原生应用的授权码流程。
请考虑以下示例配置:
QOAuth2AuthorizationCodeFlow m_oauth;
QOAuthUriSchemeReplyHandler m_handler;
m_oauth.setAuthorizationUrl(QUrl(authorizationUrl));
m_oauth.setTokenUrl(QUrl(accessTokenUrl));
m_oauth.setClientIdentifier(clientIdentifier);
m_oauth.setRequestedScopeTokens({scope});
connect(&m_oauth, &QAbstractOAuth::authorizeWithBrowser, this, &QDesktopServices::openUrl);
connect(&m_oauth, &QAbstractOAuth::granted, this, [this]() {
// Here we use QNetworkRequestFactory to store the access token
m_api.setBearerToken(m_oauth.token().toLatin1());
m_handler.close();
});
m_handler.setRedirectUrl(QUrl{"com.example.myqtapp://oauth2redirect"_L1});
m_oauth.setReplyHandler(&m_handler);
// Initiate the authorization
if (m_handler.listen()) {
m_oauth.grant();
}授权流程阶段
RFC 6749 授权码流程包含两个主要阶段:资源授权(包括任何必要的用户身份验证),随后是访问令牌请求。之后可选地进行访问令牌的使用和访问令牌的刷新。下图说明了这些阶段:

- 在授权阶段,系统会对用户进行身份验证,用户同时授权访问资源。此过程需要用户通过浏览器进行交互。
- 授权完成后,将使用收到的授权码来请求访问令牌,并可选地请求刷新令牌。
- 获取访问令牌后,应用程序将使用它来访问目标资源。访问令牌会被包含在资源请求中,而令牌有效性的验证则由资源服务器负责。对于承载式令牌,有多种方式可将其作为请求的一部分包含进去。将令牌包含在HTTP 的 `
Authorization` 头中,可以说是其中最常见的方式。 - 访问令牌刷新。访问令牌通常过期较快,例如一小时后。如果应用程序在获取访问令牌的同时还收到了刷新令牌,则可使用该刷新令牌请求新的访问令牌。应用程序可以持久化保存有效期更长的刷新令牌,从而避免需要重新进行授权阶段(进而避免再次与浏览器交互)。
详细信息与自定义
OAuth 2.0 流程是动态的,最初实现这些规范可能会有些棘手。下图展示了成功授权码流程的主要细节。

为清晰起见,该图省略了部分信号,但整体上展示了流程细节和主要定制点。这些定制点包括应用程序可使用的各种信号和插槽,以及可通过QAbstractOAuth::setModifyParametersFunction() 和QAbstractOAuth2::setNetworkRequestModifier() 设置的回调函数。
选择响应处理程序
选择使用哪个处理程序取决于redirect_uri元素。redirect_uri 被设置为授权阶段结束后浏览器将被重定向到的地址。
对于在原生应用程序中接收授权响应,RFC 8252规定了三种主要的响应 URI 方案:私有用途、回环和 https。
- 私有用途 URI:如果操作系统允许应用程序注册自定义 URI 方案,则可以使用。尝试打开带有此类自定义方案的 URL 将启动相关的原生应用程序。参见QOAuthUriSchemeReplyHandler 。
- HTTPS URI:如果操作系统允许应用程序注册自定义 HTTPS URL,则可以使用。尝试打开此 URL 将启动相关的原生应用程序。如果操作系统支持,建议使用此方案。参见QOAuthUriSchemeReplyHandler 。
- 环回接口:这些接口通常用于桌面应用程序以及开发阶段的应用程序。QOAuthHttpServerReplyHandler 通过设置本地服务器来处理重定向,从而支持此类 URI。
选择取决于以下几个因素:
- 授权服务器供应商支持的重定向 URI。各供应商的支持情况各不相同,且通常与特定的客户端类型和操作系统相关。此外,支持情况还可能因应用程序是否已发布而有所不同。
- 目标平台支持的重定向 URI 方案。
- 应用程序特有的易用性、安全性及其他要求。
RFC 8252建议使用https 方案,因为与其他方法相比,该方案在安全性和易用性方面更具优势。
OAuth 2.0 设备授权授予
RFC 8628OAuth 2.0 设备授权授予流程适用于输入能力受限的联网设备,或不适合使用用户代理(浏览器)的场景。采用此流程的设备示例包括需要借助外部设备进行授权的智能家电。
请考虑以下示例设置:
m_deviceFlow.setAuthorizationUrl(QUrl(authorizationUrl));
m_deviceFlow.setTokenUrl(QUrl(accessTokenUrl));
m_deviceFlow.setRequestedScopeTokens({scope});
m_deviceFlow.setClientIdentifier(clientIdentifier);
// 是否需要客户端密钥取决于授权服务器
m_deviceFlow.setClientIdentifierSharedKey(clientSecret);
connect(&m_deviceFlow, &QOAuth2DeviceAuthorizationFlow::authorizeWithUserCode, this,
[](constQUrl&verificationUrl, constQString&userCode, constQUrl&completeVerificationUrl) {
if(completeVerificationUrl.isValid()) {
// 如果授权服务器提供的完整 URL
// 已将必要数据作为 URL 参数包含其中,
// 则可以选择使用该 URL
qDebug() << "Complete verification uri:" << completeVerificationUrl;
}else{
// 授权服务器仅提供了验证 URL;使用该 URL
qDebug() << "Verification uri and usercode:" << verificationUrl << userCode;
}
}
);
connect(&m_deviceFlow, &QAbstractOAuth::granted, this, [this](){
// 这里我们使用 QNetworkRequestFactory 来存储访问令牌
m_api.setBearerToken(m_deviceFlow.token().toLatin1());
});
m_deviceFlow.grant();设备授权授予阶段
设备授权授予流程主要包括三个阶段:初始化授权、轮询令牌以及完成授权。随后可选地进行令牌使用和令牌刷新。下图说明了这些阶段:

- 通过向授权服务器发送 HTTP 请求来初始化授权。授权服务器会在响应中提供用户代码、验证 URL 以及设备代码。
- 授权初始化完成后,系统会向用户提供用户代码和验证 URL,以便用户完成授权。面向最终用户的实现方式多种多样:可以是屏幕上显示的 URL、二维码、电子邮件等。更多信息请参阅RFC 8628《用户交互》。
- 在等待最终用户完成授权期间,设备流程会向授权服务器轮询令牌。上一步骤收到的设备代码用于匹配授权会话。轮询间隔由授权服务器决定,通常为 5 秒。
- 一旦最终用户接受或拒绝了授权,授权服务器将对轮询请求作出响应:若授权成功则返回所请求的令牌,若被拒绝则返回错误代码,至此授权流程即告完成。
详细信息与自定义
下图更详细地说明了设备授权授予流程。图中展示了主要自定义点,这些自定义点有时是必要的。例如,专有参数或额外的身份验证凭据。

刷新令牌
刷新令牌要求授权服务器在授权过程中提供刷新令牌。是否提供刷新令牌由授权服务器自行决定:有些服务器可能选择始终提供,有些则可能从不提供,还有些服务器仅在授权请求中包含特定的scope 时才会提供。
下图更详细地说明了令牌刷新过程:

如上图所示,在刷新令牌时,通常的自定义点同样可用。
若要在应用程序启动后刷新令牌,应用程序需要将刷新令牌安全地持久化,并通过 `QAbstractOAuth2::setRefreshToken` 方法将其设置。随后可调用 `QAbstractOAuth2::refreshTokens ` 方法来请求新的令牌。
Qt 6.9 引入了一项新功能,应用程序可以自动刷新令牌——请参阅QAbstractOAuth2::accessTokenAboutToExpire 、QAbstractOAuth2::autoRefresh 以及QAbstractOAuth2::refreshLeadTime 。
授权服务器通常不会明确标注刷新令牌的过期时间(除非在服务器文档中另有说明)。其有效期可能为数天、数月甚至更长时间。此外,与其他令牌一样,刷新令牌可由用户随时撤销,从而失效。 因此,正确检测使用QAbstractOAuth::requestFailed 或QAbstractOAuth2::serverReportedErrorOccurred 进行刷新尝试失败的情况非常重要。
OAuth 2.0 流程需要多次用户交互,这可能会对用户体验造成干扰。为了尽量减少这些交互,可以为用户静默刷新令牌。更多信息请参阅RFC 6749——访问令牌刷新。
Qt 对 OpenID Connect 的支持
OpenID Connect(OIDC)是构建在 OAuth 2.0 之上的一个简易身份验证层。OIDC 可利用授权服务器来验证用户的身份。此外,通过 OIDC 还可以访问简单的用户个人资料信息。
目前,Qt 对 OIDC 的支持仅限于获取 ID 令牌。ID 令牌是一种JSON Web Token(JWT),其中包含有关身份验证事件的声明。
注意: 目前尚未实现ID 令牌的验证或解密功能。您必须使用第三方 JWT 库来对 JWT 令牌进行签名或验证。
假设应用程序能够验证收到的令牌,则该令牌可用于可靠地确认用户的身份(前提是 OIDC 提供商本身是可信的)。
ID 令牌属于敏感信息,应作为机密信息严格保管,且与访问令牌不同。ID 令牌不应用于 API 调用中——访问令牌才是为此目的而设计的。 请注意,某些供应商可能会为访问令牌使用相同的 JWT 格式,但切勿将其与采用相同格式的实际身份令牌混淆。对于身份令牌,接收令牌的客户端负责验证令牌;而对于访问令牌,接受令牌的资源服务器则负责验证。
获取身份令牌
获取 ID 令牌与获取访问令牌类似。首先,我们需要设置适当的范围。授权服务器供应商可能支持其他范围指定符,例如profile 和email ,但所有 OIDC 请求都必须包含openid 范围:
m_oauth.setRequestedScopeTokens({"openid"});对于 OIDC,强烈建议使用nonce 参数。实现方法是确保已设置适当的NonceMode 。
// This is for illustrative purposes, 'Automatic' is the default mode
m_oauth.setNonceMode(QAbstractOAuth2::NonceMode::Automatic);作为最后一步,我们可以监听QAbstractOAuth2::granted 信号,或者直接监听QAbstractOAuth2::idTokenChanged :
connect(&m_oauth, &QAbstractOAuth2::idTokenChanged, this, [this](const QString &token) {
Q_UNUSED(token); // Handle token
});验证身份令牌
验证收到的ID令牌是身份验证流程中的关键环节,完全实现起来是一项相当复杂的任务。请参阅《OpenID Connect ID验证》中的完整说明。
简而言之,验证包括以下步骤:
- 如有必要,对令牌进行解密(参见 JWE)
- 提取令牌的头部、有效载荷和签名
- 验证签名
- 验证有效负载的字段(例如
aud, iss, exp, nonce, iat)
Qt 目前不支持 ID 令牌验证,但有第三方 JWT 库,例如jwt-cpp。
ID令牌验证示例
本节介绍一个简单的验证示例。作为先决条件,开发环境中需安装OpenSSL库,并且jwt-cpp需位于应用程序项目源代码目录下的 include 文件夹中。
在应用程序项目的CMakeLists.txt 文件中,我们首先检查是否满足先决条件:
find_package(OpenSSL 1.0.0 QUIET)
set(JWT_CPP_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include")
if(OPENSSL_FOUND AND EXISTS "${JWT_CPP_INCLUDE_DIR}/jwt-cpp/jwt.h")然后,我们添加必要的头文件和库:
target_include_directories(networkauth_oauth_snippets PRIVATE "${JWT_CPP_INCLUDE_DIR}")
target_link_libraries(networkauth_oauth_snippets PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(networkauth_oauth_snippets PRIVATE JWT_CPP_AVAILABLE)在应用程序源文件中,包含验证库:
#ifdef JWT_CPP_AVAILABLE
#include "jwt-cpp/jwt.h"
#endif应用程序收到 ID 令牌后,就需要对其进行验证。首先,我们从 JSON Web Key Sets (JWKS) 中查找匹配的密钥(参见OpenID Connect 发现)。
try {
const auto jwt = jwt::decode(m_oauth.idToken().toStdString());
const auto jwks = jwt::parse_jwks(m_jwks->toJson(QJsonDocument::Compact).toStdString());
const auto jwk = jwks.get_jwk(jwt.get_key_id());然后进行实际的验证:
// 在此处,我们使用模数和指数来推导密钥
const auton=jwk.get_jwk_claim("n").as_string();// 模数
const autoe=jwk.get_jwk_claim("e").as_string();// 指数
if(n.empty()||e.empty()) {
qWarning() << "Modulus or exponent empty";
return false;
}
if(jwt.get_algorithm()!= "RS256") {// 此示例仅支持 RS256
qWarning() << "Unsupported algorithm:" << jwt.get_algorithm();
return false;
}
if(jwk.get_jwk_claim("kty").as_string()!= "RSA") {
qWarning() << "Unsupported key type:" << jwk.get_jwk_claim("kty").as_string();
return false;
}
if(jwk.has_jwk_claim("use")&&jwk.get_jwk_claim("use").as_string()!= "sig") {
qWarning() << "Key not for signature" << jwk.get_jwk_claim("use").as_string();
return false;
}
// 简单的基本验证(省略了特殊情况以及例如“sub”验证)。
// 如果存在“exp”、“iat”和“nbf”,jwt-cpp 也会对其进行验证。
const autokeyPEM=jwt::helper::create_public_key_from_rsa_components(n,e);
autoverifier=jwt::verify()
.allow_algorithm(jwt::algorithm::rs256(keyPEM))
.with_claim("nonce",jwt::claim(m_oauth.nonce().toStdString()))
.with_issuer(m_oidcConfig->value("issuer"_L1).toString().toStdString())
.with_audience(std::string(clientIdentifier.data()))
.leeway(60UL);
verifier.verify(jwt);
qDebug() << "ID Token verified successfully";
return true;
}catch(conststd::exception&e) {
// 处理错误。或者将错误参数传递给 jwt-cpp 调用
qWarning() << "ID Token verification failed" << e.what();
return false;
}读取ID令牌值
ID 令牌采用JSON Web Token(JWT)格式,由标头、有效载荷和签名三部分组成,各部分之间以点号分隔. 。
读取 ID 令牌的值非常简单。例如,假设有一个结构体:
struct IDToken {
QJsonObject header;
QJsonObject payload;
QByteArray signature;
};以及一个函数:
std::optional<IDToken> parseIDToken(const QString &token) const;可以使用以下方式提取该令牌:
if (token.isEmpty())
return std::nullopt;
QList<QByteArray> parts = token.toLatin1().split('.');
if (parts.size() != 3)
return std::nullopt;
QJsonParseError parsing;
QJsonDocument header = QJsonDocument::fromJson(
QByteArray::fromBase64(parts.at(0), QByteArray::Base64UrlEncoding), &parsing);
if (parsing.error != QJsonParseError::NoError || !header.isObject())
return std::nullopt;
QJsonDocument payload = QJsonDocument::fromJson(
QByteArray::fromBase64(parts.at(1), QByteArray::Base64UrlEncoding), &parsing);
if (parsing.error != QJsonParseError::NoError || !payload.isObject())
return std::nullopt;
QByteArray signature = QByteArray::fromBase64(parts.at(2), QByteArray::Base64UrlEncoding);
return IDToken{header.object(), payload.object(), signature};在某些情况下,令牌可能以JSON Web Encryption(JWE)格式加密,其内部包含一个 JWT 令牌。此时,必须先对令牌进行解密。
OpenID Connect 发现
OpenID Connect 发现机制定义了发现所需 OpenID 提供商详细信息的方法,以便与之进行交互。这包括authorization_endpoint 和token_endpoint 等 URL 信息。
虽然这些提供商详细信息可以在应用程序中进行静态配置,但在运行时发现这些详细信息,在与各种提供商交互时可能提供更大的灵活性和更强的健壮性。
获取发现文档只需发送一个简单的 HTTP GET 请求。该文档通常位于https://<domain name>/.well-known/openid_configuration 。
m_network->get(request, this, [this](QRestReply &reply) {
if (reply.isSuccess()) {
if (auto doc = reply.readJson(); doc && doc->isObject())
m_oidcConfig = doc->object(); // Store the configuration
}
});值得注意的是,在令牌验证过程中,jwks_uri字段提供了一个用于访问当前(公开)安全凭据的链接。 利用该链接,无需在应用程序中直接硬编码此类凭据。这也有助于密钥轮换;供应商可能会不时更改所使用的密钥,因此确保密钥保持最新至关重要。
获取密钥同样只需一个简单的 HTTP GET 请求:
m_network->get(request, this, [this](QRestReply &reply) {
if (reply.isSuccess()) {
if (auto doc = reply.readJson(); doc && doc->isObject())
m_jwks = doc; // Use the keys later to verify tokens
}
});密钥集通常包含多个密钥。正确的密钥会在 JWT 头中指明(需注意正确匹配密钥,仅检查密钥 ID 字段kid 是不够的)。
OpenID UserInfo 端点
另一种获取用户信息的方法是使用OpenID Connect UserInfo 端点,前提是 OIDC 提供商支持该功能。userinfo 的 URL 位于OpenID Connect 发现文档的userinfo_endpoint 字段中。
UserInfo 端点不使用 ID 令牌,而是通过访问令牌进行访问。访问 UserInfo 的方式与使用访问令牌访问任何其他资源类似。
假设已获取访问令牌,并将其设置为(例如):
QNetworkRequestFactory userInfoApi(url);
userInfoApi.setBearerToken(m_oauth.token().toLatin1());那么访问 UserInfo 即为一个 HTTP GET 请求:
m_network->get(userInfoApi.createRequest(), this, [this](QRestReply&reply) {
if(reply.isSuccess()) {
if(autodoc=reply.readJson(); doc&& doc->isObject())
qDebug() << doc->object(); // Use the userinfo
}
});© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.