Authorization Request 特性总结
OAuth 2.0 原始的授权请求(Authorization Request)像一张明文写就、由不可信快递员(浏览器)传递的明信片——内容裸奔、可以被篡改、写不下复杂信息。
针对 OAuth 2.0 Authorization Request 存在的问题,在 IETF 陆续新增三个 RFC 标准,每个 RFC 标准各自解决了 Authorization Request 中某个维度的问题:
RFC |
简称 |
一句话定位 |
解决的核心痛点 |
RFC 9101 |
JAR |
把明信片装进密封信封 |
请求参数可被篡改和窃听 |
RFC 9126 |
PAR |
改用安全专线提前递交 |
参数经过浏览器不安全 + URL太长 |
RFC 9396 |
RAR |
把勾选框升级为详细表单 |
|
前置知识:标准 OAuth 2.0 授权请求的问题
给产品经理:一个生活化比喻
标准的 OAuth 2.0 授权请求,就像你写了一张明信片,交给一个你不太信任的快递员(浏览器),让他帮你送到银行(授权服务器)。
收件人:银行授权窗口 寄件人:某App(client_id=app123) 请求内容:我想要 scope=read_account(读取账户信息的权限) 回复地址:https://app.com/callback
问题在于:快递员能看到所有内容、能偷偷修改内容、明信片太小写不下复杂信息。
标准授权请求长什么样
一个标准的 OAuth 2.0 Authorization Code Grant 授权请求就是一次浏览器 302 重定向:
GET /authorize? response_type=code &client_id=s6BhdRkqt3 &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=read_account &state=af0ifjsldkj HTTP/1.1 Host: auth.bank.com
所有参数以 URL Query String 明文传递,面临三个技术风险:
风险一:参数篡改(Tampering)。 浏览器扩展、恶意代理或中间人可以修改 URL 中的任何参数。例如将 scope=read 改为 scope=read+write+admin。授权服务器无法区分这是客户端的原始意图还是被篡改后的结果。
风险二:参数泄露(Leakage)。 URL 参数会出现在浏览器地址栏、历史记录、HTTP Referer 头、代理服务器日志和网络监控中。如果参数中包含敏感信息(如交易金额、PII 数据),泄露面很广。
风险三:URL 长度受限。 浏览器和中间件对 URL 长度有不同限制(IE 为 2083 字符,Nginx 默认 8KB),当授权参数携带结构化数据时极易超限。
RFC 9101 — JAR:JWT-Secured Authorization Request
把申请单装进"密封信封"
原来你是把申请内容直接写在明信片上。JAR 的做法是:先把内容写好,放进一个带防伪封条的密封信封(JWT 签名),如果特别敏感,还加一把锁(JWT 加密)。快递员还是那个浏览器,但他现在只是搬运工——搬的是密封信封,既看不到内容,也没法修改。
技术原理
JAR 的核心是用 JWS(JSON Web Signature) 和/或 JWE(JSON Web Encryption) 封装授权请求参数。客户端将所有授权参数编码为 JWT 的 Claims,用自己的私钥签名(确保完整性和来源认证),可选地用授权服务器的公钥加密(确保机密性)。
Request Object 的 JWT 结构示例:
// JWT Header { "alg": "RS256", "kid": "client-key-2024" } // JWT Payload (Claims) { "iss": "s6BhdRkqt3", // 客户端标识(=client_id) "aud": "https://auth.bank.com", // 授权服务器标识 "response_type": "code", "client_id": "s6BhdRkqt3", "redirect_uri": "https://app.example.com/callback", "scope": "read_account transfer", "state": "af0ifjsldkj", "nonce": "n-0S6_WzA2Mj", "iat": 1710000000, "exp": 1710000300, // 5分钟后过期 "nbf": 1710000000 }
工作流程
JAR 提供两种传递方式:
方式一:Pass-by-Value(按值传递)
客户端将 JWT 直接放在 request 参数中传递。客户端构造授权参数并用私钥签名生成 JWT,然后通过浏览器 302 重定向将 JWT 放在 request 参数中发送到授权服务器。授权服务器验证 JWT 签名后,解析 Claims 为授权参数,正常处理授权流程。
实际的 HTTP 请求:
GET /authorize?client_id=s6BhdRkqt3&request=eyJhbGciOiJSUzI1NiIsImtpZCI6ImNsaWVudC 1rZXktMjAyNCJ9.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9hdXRoLmJhbmsuY29tIi wicmVzcG9uc2VfdHlwZSI6ImNvZGUiLCJjbGllbnRfaWQiOiJzNkJoZFJrcXQzIiwicmVkaXJlY3RfdXJp IjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJzY29wZSI6InJlYWRfYWNjb3VudCJ9. 签名部分 HTTP/1.1 Host: auth.bank.com
方式二:Pass-by-Reference(按引用传递)
客户端将 JWT 托管在 HTTPS 端点,只传递一个 URI 引用。客户端先将签名的 JWT 托管到自己的 HTTPS 端点上(如 https://app.example.com/requests/abc123),然后浏览器重定向时只携带 request_uri 指向该地址。授权服务器收到后,通过 HTTP GET 请求获取 JWT,验证签名后解析处理。
注意: JAR 的 request_uri 由客户端提供并托管,客户端需维护一个 HTTPS 端点。而 PAR(RFC 9126)的 request_uri 由授权服务器生成并存储,不需要客户端托管。这是两者的关键区别。
安全考虑
参数优先级冲突。 当 request JWT 内的参数与 URL Query String 中的参数冲突时,RFC 9101 规定以 JWT 内部的参数为准。但 client_id 必须同时出现在 URL 和 JWT 中且值一致,用于授权服务器在解析 JWT 之前确定使用哪个客户端的公钥来验证签名。
# URL 中的 scope=read 和 JWT 中的 scope=read+write 冲突时 # → 以 JWT 中的 scope=read+write 为准
降级攻击。 如果授权服务器同时支持带 JAR 和不带 JAR 的请求,攻击者可以剥离 request 参数,伪造一个不带签名的普通请求。防御措施:授权服务器应通过元数据 require_signed_request_object 声明强制要求 JAR,对不含 request/request_uri 的请求直接拒绝。
request_uri 的 SSRF 风险。 Pass-by-Reference 模式下,授权服务器需要向 request_uri 发起 HTTP 请求。如果不做限制,攻击者可能利用这一行为让授权服务器访问内网资源(SSRF 攻击)。防御措施:授权服务器应维护 request_uri 的白名单(通常限制为客户端预注册的域名),并设置超时、限制响应大小、禁止重定向。
JWT 重放攻击。 攻击者截获一个有效的 Request Object JWT 后重复使用。防御措施:JWT 中应包含 exp(过期时间,建议不超过 5 分钟)、iat(签发时间)和 jti(唯一标识)声明,授权服务器应检查并拒绝过期或重复的 JWT。
适用场景
场景一:开放银行合规。 UK Open Banking 和欧盟 Berlin Group 规范要求所有授权请求必须经过签名保护。JAR 是满足这一要求的 IETF 标准方案。
场景二:防止授权提升攻击。 浏览器插件或恶意代理把 scope=read 篡改为 scope=admin。JAR 的签名保护让任何篡改都会导致验签失败。
场景三:可信第三方背书。 企业安全审计平台统一签发 Request Object JWT,授权服务器只需信任该平台的签名密钥,实现集中式请求审计。
RFC 9126 — PAR:Pushed Authorization Requests
改用"安全专线"提前递交
JAR 是"把信装进密封信封,但还是让快递员送"。PAR 更彻底——你自己开车把信直接送到银行。银行给你一个"取件条"编号,你再让快递员带着编号去窗口报到。快递员全程只接触一个无意义的编号,看不到任何实际内容。
技术原理
PAR 在授权服务器上引入一个新的 HTTPS 端点(PAR Endpoint),客户端通过后端 HTTP POST(而非浏览器重定向)将完整的授权参数直接发送到该端点,同时进行客户端身份认证。授权服务器验证后返回一个不透明的 request_uri 引用和有效期。后续浏览器重定向只携带 client_id 和 request_uri。
PAR Endpoint 的发现: 授权服务器通过 OAuth 2.0 Authorization Server Metadata(RFC 8414)中的 pushed_authorization_request_endpoint 字段声明 PAR 端点地址。
工作流程
步骤1 — PAR 请求(后端到后端):
POST /as/par HTTP/1.1 Host: auth.bank.com Content-Type: application/x-www-form-urlencoded Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW # 客户端认证 response_type=code &client_id=s6BhdRkqt3 &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=read_account%20transfer &state=af0ifjsldkj &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256
步骤2 — PAR 响应:
HTTP/1.1 201 Created Content-Type: application/json Cache-Control: no-cache, no-store { "request_uri": "urn:ietf:params:oauth:request_uri:bwc4JK-ESC0w8acc191e-Y1LTC2", "expires_in": 60 }
步骤3 — 浏览器重定向(极短的 URL):
GET /authorize? client_id=s6BhdRkqt3 &request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3Abwc4JK-ESC0w8acc191e-Y1LTC2 HTTP/1.1 Host: auth.bank.com
PAR + JAR 组合使用: PAR 请求体中也可以包含 request 参数(即一个签名的 JWT),实现"通过安全通道传递签名请求"的双重保护:
POST /as/par HTTP/1.1 Host: auth.bank.com Content-Type: application/x-www-form-urlencoded Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW client_id=s6BhdRkqt3 &request=eyJhbGciOiJSUzI1NiJ9.eyJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF...
安全考虑
request_uri 猜测攻击。 如果 request_uri 的值可以被猜测(如递增的数字),攻击者可以尝试使用其他客户端的 request_uri。防御措施:request_uri 必须使用密码学安全的伪随机算法生成,至少包含 128 位熵,且必须绑定到发起 PAR 请求的 client_id。
# 好的 request_uri — 高熵、不可预测 urn:ietf:params:oauth:request_uri:bwc4JK-ESC0w8acc191e-Y1LTC2 # 坏的 request_uri — 可枚举 urn:example:request:1001
request_uri 重放攻击。 攻击者截获 request_uri 后在有效期内重复使用。防御措施:授权服务器应将 request_uri 设为一次性使用,被消费后立即失效。同时配合 PKCE 的 code_challenge 绑定,即使 request_uri 被重放,攻击者也无法完成令牌交换。
request_uri 替换攻击。 攻击者用一个低安全级别的 request_uri(例如请求范围更广的权限)替换浏览器重定向中的 request_uri。防御措施:客户端应使用 state 参数(或 OpenID Connect nonce)将授权请求与授权响应绑定,同时配合 PKCE 确保攻击者即使截获授权码也无法兑换令牌。
开放重定向风险。 标准 OAuth 2.0 要求 redirect_uri 预先注册以防止开放重定向。PAR 因为在经过认证的后端通道中接收参数,授权服务器可以放宽这个限制——但仅限于已认证的机密客户端。公开客户端仍应强制预注册。
有效期控制。 expires_in 应尽量短(建议 60~600 秒),以减少 request_uri 被截获后的攻击窗口。
适用场景
场景一:大请求体(与 RAR 组合)。 authorization_details 的 JSON 可能非常大,URL 编码后轻松超过浏览器限制。PAR 通过 POST 传递,完全不受 URL 长度约束。
场景二:移动 App 系统浏览器。 iOS/Android 上的系统浏览器(ASWebAuthenticationSession / Chrome Custom Tabs)初始请求通常限制为 GET,PAR 让复杂参数在后端完成传递,浏览器只处理极短 URL。
场景三:请求前置认证。 PAR 在步骤1就要求客户端认证(client_secret、private_key_jwt、mTLS 等),授权服务器可以在用户看到任何页面之前拒绝无效客户端,大幅降低钓鱼风险。
场景四:动态回调地址。 SaaS 多租户场景中 redirect_uri 可能动态变化。PAR 在认证通道中传递,授权服务器可以放宽对已认证客户端的 redirect_uri 限制。
RFC 9396 — RAR:Rich Authorization Requests
把"勾选框"升级为"详细表单"
原来的 scope 参数就像一个极简勾选框:
☑ 读取账户 ☐ 转账 ☐ 查看历史
你只能勾选"我要转账权限",没法写"转给谁、转多少"。RAR 引入的 authorization_details 是一个结构化 JSON 表单:
授权类型:转账支付 ├── 从哪个账户转:尾号 1234 的储蓄账户 ├── 转给谁:IBAN DE89370400440532013000(商户B) ├── 转多少:123.50 欧元 └── 用途说明:订单 #A7832
用户在授权页面看到的不再是模糊的"允许转账",而是精确的交易详情。
技术原理
RAR 引入新参数 authorization_details,其值是一个 JSON 数组,每个元素是一个描述特定授权需求的 JSON 对象。每个对象必须包含 type 字段(确定对象允许的内容结构),其他字段由 type 对应的 API 规范自行定义。
authorization_details 的完整数据模型:
[ { // ===== 通用字段(RFC 9396 定义)===== "type": "payment_initiation", // 必填,授权类型标识 "locations": [ // 可选,目标资源服务器 URI "https://api.bank.com/payments" ], "actions": ["initiate", "status"], // 可选,请求的操作 "datatypes": ["payment_order"], // 可选,请求的数据类型 "identifier": "txn-2024-001", // 可选,特定资源标识符 "privileges": ["owner"], // 可选,特权级别 // ===== 自定义字段(由 type 对应的 API 定义)===== "instructedAmount": { "currency": "EUR", "amount": "123.50" }, "creditorName": "Merchant A", "creditorAccount": { "iban": "DE02100100109307118603" } } ]
type 字段的注册机制: type 的值应在 IANA "OAuth Authorization Details Types" 注册表中注册,或使用 URI 格式避免命名冲突(如 https://api.bank.com/payment_initiation)。
工作流程
步骤1 — 授权请求(URL 编码后的 JSON):
GET /authorize? response_type=code &client_id=s6BhdRkqt3 &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &state=af0ifjsldkj &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &authorization_details=%5B%7B%22type%22%3A%22payment_initiation%22%2C %22instructedAmount%22%3A%7B%22currency%22%3A%22EUR%22%2C %22amount%22%3A%22123.50%22%7D%2C%22creditorName%22%3A %22Merchant%20A%22%7D%5D HTTP/1.1 Host: auth.bank.com
实际中推荐与 PAR 组合使用,通过 POST 传递 JSON 数据,避免 URL 编码和长度问题。
步骤6 — 令牌响应(包含丰富化后的 authorization_details):
HTTP/1.1 200 OK Content-Type: application/json { "access_token": "2YotnFZFEjr1zCsicMWpAA", "token_type": "Bearer", "expires_in": 3600, "authorization_details": [ { "type": "payment_initiation", "instructedAmount": { "currency": "EUR", "amount": "123.50" }, "creditorName": "Merchant A", "creditorAccount": { "iban": "DE02100100109307118603" }, "debtorAccount": { "iban": "DE89370400440532013000" } } ] }
注意 debtorAccount 是授权服务器在用户选择具体付款账户后补充的——这就是"丰富化"(Enrichment)机制。
authorization_details 如何传递给资源服务器?
资源服务器需要知道 authorization_details 才能做细粒度的访问控制。RFC 定义了两种方式:
方式一:JWT Access Token 的内嵌声明
// JWT Access Token Payload { "iss": "https://auth.bank.com", "sub": "user123", "aud": "https://api.bank.com", "exp": 1710003600, "authorization_details": [ { "type": "payment_initiation", "instructedAmount": { "currency": "EUR", "amount": "123.50" }, "creditorAccount": { "iban": "DE02100100109307118603" } } ] }
方式二:Token Introspection(令牌内省)
POST /introspect HTTP/1.1 Host: auth.bank.com Content-Type: application/x-www-form-urlencoded Authorization: Basic ... token=2YotnFZFEjr1zCsicMWpAA
// 内省响应 { "active": true, "sub": "user123", "authorization_details": [ { "type": "payment_initiation", "instructedAmount": { "currency": "EUR", "amount": "123.50" } } ] }
authorization_details 与 scope 的关系
authorization_details 和 scope 可以共存。典型的使用模式:
# scope 控制粗粒度的功能权限 # authorization_details 描述细粒度的业务参数 scope=openid profile &authorization_details=[{"type":"payment_initiation","amount":"123.50",...}]
授权服务器需要综合评估两者来决定授权范围。
安全考虑
参数篡改。 authorization_details 通过浏览器传递时,和普通参数一样面临篡改风险。攻击者可能修改金额、收款方等关键字段。这是 RAR 必须与 JAR 或 PAR 组合使用的根本原因——RAR 自身不提供完整性保护。
# 攻击场景:攻击者修改转账金额 原始: {"amount": "10.00", "creditorName": "Merchant A"} 篡改: {"amount": "10000.00", "creditorName": "Attacker B"} # 防御:使用 JAR 签名保护整个请求,或使用 PAR 通过后端传递
注入攻击。 authorization_details 的 JSON 中可能包含恶意内容(如 SQL 注入、XSS 载荷)。授权服务器和资源服务器必须对所有字段进行严格的输入验证和转义处理。
权限提升(Token Refresh)。 当客户端用 refresh_token 请求新的 access_token 时,可以携带新的 authorization_details。授权服务器必须确保新请求的权限范围不超过原始授权范围:
# 原始授权:读取账户余额 # Refresh 请求中试图扩展为:读取余额 + 发起转账 # → 授权服务器应拒绝
隐私泄露。 authorization_details 可能包含高度敏感的信息(交易金额、收款方身份、医疗数据等)。通过 URL 传递时,这些信息可能通过 Referer 头、浏览器历史等泄露。强烈建议与 PAR 配合使用,让敏感数据仅在后端通道传递。
类型注册与验证。 授权服务器应维护一个受支持的 type 列表,对未知的 type 值返回 invalid_authorization_details 错误,防止客户端传入恶意或未授权的类型。
适用场景
场景一:开放银行 - 支付发起(PSD2)。 欧盟 PSD2 要求授权页面展示交易金额、收款方和账户。scope=payment 无法携带这些细节,authorization_details 天然适配。
场景二:电子健康 - 精细化数据访问。 医生访问患者病历,权限精确到"哪个患者的哪类数据":type=medical_record, patient_id=P123, datatypes=["lab_results"]。
场景三:电子签名 - 远程签署。 精确描述要签的文档哈希、签名算法、签名策略。
场景四:税务数据 - 特定年度。 type=tax_data, years=[2022, 2023], actions=["read"] 比 scope=tax_data 精确得多。
三者如何协同工作?
这三个 RFC 不是互相替代的关系,而是正交互补、组合使用的:
RAR(RFC 9396)位于内容层,提供"要授权什么"的详细内容(authorization_details JSON)。这些内容向下分别流向两条保护路径:JAR(RFC 9101)用 JWT 签名/加密保护参数完整性和机密性;PAR(RFC 9126)通过后端安全通道传递参数,并获得一个短引用编号。三者组合实现从内容构造到安全传递的完整覆盖。
"全副武装"的完整流程(以开放银行支付为例)
1. App 构造 authorization_details(RAR),精确描述"向 Merchant A 转账 123.50 EUR" 2. App 把 authorization_details + 其他参数打包成签名 JWT(JAR),确保不可篡改 3. App 后端直接向授权服务器的 PAR 端点 POST 这个 JWT(PAR),获得 request_uri 4. 浏览器只带 client_id + request_uri 跳转到授权页面 5. 用户看到完整、准确、不可被篡改的交易详情,做出知情授权决策 6. 授权服务器返回授权码,App 完成令牌交换 7. 令牌中携带丰富化后的 authorization_details,资源服务器据此精确执行
对比总结
维度 |
标准 OAuth 2.0 |
+ JAR (9101) |
+ PAR (9126) |
+ RAR (9396) |
参数传递方式 |
URL Query String |
JWT(签名/加密) |
后端 POST → 引用 URI |
不改变传递方式 |
完整性保护 |
无 |
JWT 签名 |
后端通道不经浏览器 |
自身无保护,依赖 JAR/PAR |
机密性保护 |
无 |
JWT 加密(可选) |
参数不出现在浏览器 |
自身无保护,依赖 JAR/PAR |
URL 长度限制 |
受限 |
按引用可缓解 |
完全不受限 |
可能加重(JSON 更大) |
授权粒度 |
scope(粗标签) |
不改变 |
不改变 |
authorization_details(结构化 JSON) |
客户端预认证 |
否 |
否 |
是 |
否 |
新增端点 |
无 |
无 |
PAR Endpoint |
无 |
实现复杂度 |
基线 |
中(需 JWT 库 + 密钥管理) |
中(需新端点 + 客户端认证) |
中(需 JSON Schema + 业务适配) |
选择指南
"我是一个普通 Web 应用,不涉及金融和敏感数据" → 标准 OAuth 2.0 + PKCE 通常足够,暂不需要这三个扩展。
"我需要满足金融级安全合规(PSD2、Open Banking、FAPI)" → JAR + PAR + RAR 三件套通常都是必选项。
"我的授权请求参数很多/很长" → PAR 是首选方案。
"我需要用户授权时看到精确的交易详情" → RAR 就是为此而生。
"我需要防止授权参数被篡改" → JAR 或 PAR 二选一。JAR 更轻量(不需要额外端点),PAR 更安全(参数完全不经过浏览器)。
"我在做移动 App,系统浏览器对 URL 有限制" → PAR 能把复杂请求"藏"在后端。
附录A:术语速查表
术语 |
通俗解释 |
技术说明 |
Authorization Request |
"请给我权限"的请求 |
客户端通过浏览器重定向发送到授权端点的 HTTP 请求 |
Authorization Server |
发权限的银行 |
负责认证资源所有者、获取授权、签发令牌的服务器 |
Resource Server |
保管数据的金库 |
托管受保护资源的服务器,接受并验证 access_token |
User Agent |
快递员 |
浏览器,在客户端和授权服务器之间传递 HTTP 重定向 |
scope |
粗粒度权限标签 |
空格分隔的字符串列表,如 |
authorization_details |
细粒度权限表单 |
JSON 数组,每个元素描述一项结构化授权需求 |
JWT |
数字密封信封 |
Base64URL 编码的 JSON 结构,支持 JWS 签名和 JWE 加密 |
request |
JAR 的 JWT 参数 |
按值传递的 Request Object(JWT 字符串) |
request_uri |
JAR/PAR 的引用参数 |
JAR: 客户端托管的 JWT URL; PAR: 服务器生成的不透明引用 |
PAR Endpoint |
安全递交窗口 |
授权服务器接收推送式授权请求的 HTTPS 端点 |
附录B:各规范速查卡片
RFC 9101 — JAR
状态: IETF 正式标准 (2021年8月发布) 引入参数: request(JWT 字符串)或 request_uri(JWT 的远程 URI) 核心机制: JWS 签名 + 可选 JWE 加密 保护目标: 请求参数的完整性、来源认证和机密性 前置依赖: JWT/JWS/JWE 库、客户端密钥对 兼容性: 可与 PAR、RAR、PKCE 等所有扩展组合使用 规范链接: https://www.rfc-editor.org/rfc/rfc9101
RFC 9126 — PAR
状态: IETF 正式标准 (2021年9月发布) 引入端点: pushed_authorization_request_endpoint(通过 RFC 8414 元数据发现) 引入参数: request_uri(由授权服务器生成的一次性引用) 核心机制: 后端安全通道 + 客户端认证 + 引用令牌 保护目标: 请求参数不经过浏览器,客户端预认证 前置依赖: 客户端认证机制(client_secret / private_key_jwt / mTLS) 兼容性: 可与 JAR、RAR、PKCE 等所有扩展组合使用 规范链接: https://www.rfc-editor.org/rfc/rfc9126
RFC 9396 — RAR
状态: IETF 正式标准 (2023年5月发布) 引入参数: authorization_details(JSON 数组) 核心机制: 结构化 JSON 替代粗粒度 scope,支持丰富化(Enrichment) 保护目标: 授权粒度精细化,用户知情同意 前置依赖: 业务 type 定义 + JSON Schema;建议配合 JAR 或 PAR 保护完整性 兼容性: 可与 JAR、PAR、PKCE、Resource Indicators 等组合使用 规范链接: https://www.rfc-editor.org/rfc/rfc9396