DPoP 详解:给浏览器里的令牌也上一把“偷不走”的锁

Posted on 六 01 8月 2026 in Tech

Abstract DPoP 详解:给浏览器里的令牌也上一把“偷不走”的锁
Authors Walter Fan
Category learning note
Status v1.0
Updated 2026-08-01
License CC-BY-NC-ND 4.0

上一篇聊 PoP 和证书绑定令牌时,我留了个尾巴:服务间通信用 mTLS + 证书绑定很香,但浏览器和手机 App 怎么办?

这不是抬杠。mTLS 要求客户端持有一张 X.509 证书和对应私钥。你让一个跑在别人浏览器里的单页应用(SPA)去管理客户端证书试试?给每个用户签发、安装、轮换证书,光是想想运维就头皮发麻。移动 App 稍好一点,但也远谈不上优雅。

于是就有了 DPoP(Demonstrating Proof of Possession,持有证明演示),RFC 9449 在 2023 年正式定稿。一句话:

DPoP = 不需要 mTLS 的 PoP。客户端自己生成一对公私钥,每次请求都用私钥签一张“临时证明”,证明“这令牌是我的”。

这篇文章想把 DPoP 拆到你能照着动手的程度:它到底往请求里塞了什么、服务端怎么验、防重放怎么做、以及实际接的时候会踩哪些坑。

提示:文中的 JWT、JWS、JWK、指纹这些词,第一次出现我都会用一句大白话解释。没接过 OAuth 的读者也能顺下来,代码/JSON 块可以跳过。


1. 一分钟看懂 DPoP 在解决什么

先把上一篇的结论接上。防令牌盗用,核心就一句:让令牌绑上一把“只有客户端自己有、偷不走”的私钥。

  • mTLS + 证书绑定:私钥藏在 X.509 证书里,走 TLS 层面证明。适合机房里的服务。
  • DPoP:私钥是客户端自己临时生成的一对密钥,走应用层(HTTP header)证明。适合浏览器、App、SPA。

关键差别在这一行:DPoP 把“持有证明”从 TLS 层挪到了应用层。 它不碰 TLS 握手,只是在每个 HTTP 请求里多加两个头。这就是它能在浏览器里跑起来的原因——JavaScript 摸不到 TLS 层,但完全可以用 Web Crypto API 生成密钥、签 JWT。

flowchart LR
    subgraph 服务间
      A[服务 A] -->|mTLS 层| B[服务 B]
    end
    subgraph 公网客户端
      C[浏览器/App] -->|DPoP: 应用层 header| D[API]
    end

2. 核心概念:DPoP proof 是什么

DPoP 引入的唯一新东西,叫 DPoP proof(DPoP 证明)

它本质上是一个由客户端现场生成、用客户端私钥签名的小 JWT,放在 HTTP 请求的 DPoP 头里。每次请求都要新签一个。

拿人话打比方:令牌是你的门禁卡,DPoP proof 是你每次刷卡时当场按下的一个指纹。卡可以被复制,但指纹是活体的、一次性的、别人按不出来。攻击者就算偷走了你的卡(令牌),没有你的手指(私钥),也按不出配套的指纹(proof)。

一个 DPoP proof 大致长这样,分头部和载荷两段:

// header
{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." }
}
// payload
{
  "jti": "e1j3V_bK...",
  "htm": "GET",
  "htu": "https://api.example.com/orders",
  "iat": 1705123456,
  "ath": "fUHyO2r2Z3DZ53Es..."
}

先记住两个头部字段:

  • typ 固定是 dpop+jwt,让服务端一眼认出“这是个 DPoP proof,别当普通 JWT 处理”。
  • jwk客户端的公钥(JWK = JSON Web Key,公钥的 JSON 表示)。注意:只放公钥,绝不能放私钥。 服务端就靠这个公钥来验签,并算出它的指纹。

载荷里的字段我们下一节逐个拆。


3. DPoP proof 里的字段,逐个拆

RFC 9449 规定 payload 里至少要有这几个:

字段 含义 作用
jti 本次 proof 的唯一 ID 防重放(服务端记下用过的,不许再用)
htm 本次请求的 HTTP 方法(GET/POST…) 把 proof 钉死在这个方法上
htu 本次请求的目标 URI(去掉 query 和 fragment) 把 proof 钉死在这个地址上
iat 签发时间戳 判断 proof 是不是太老了

带着访问令牌去访问受保护资源时,还必须多一个:

字段 含义 作用
ath 访问令牌的 SHA-256 哈希(base64url 编码) 把 proof 和具体这个令牌绑在一起

如果服务端要求用 nonce(下面第 6 节讲),还得再加:

字段 含义
nonce 服务端通过 DPoP-Nonce 响应头下发的一次性随机值

htm + htu 这一对是 DPoP 的精髓:proof 里写死了“我这张证明只对 GET https://api.example.com/orders 有效”。攻击者就算截获了整个请求,也没法把这张 proof 挪到别的接口或别的方法上用——换个动作、换个地址,签名就对不上了。

ath 则把 proof 和令牌本身拴死:proof 里存着令牌的哈希,服务端会核对“你这张 proof 里说的令牌哈希,是不是就是你这次带来的令牌”。这样连“拿 A 令牌配 B proof”的错配都堵死了。

jti(唯一 ID)按规范建议至少 96 位伪随机数据,或直接用一个 v4 UUID。它是防重放的钥匙。


4. 完整流程:两段式

RFC 9449 把 DPoP 拆成两个可分开的部分,理解这一点特别重要:

  • 第一段(取令牌):客户端向授权服务器换一个 DPoP-bound(DPoP 绑定)的访问令牌
  • 第二段(用令牌):客户端拿着这个令牌访问资源服务器,并证明自己控制那把私钥。

第一段:换一个 DPoP 绑定的令牌

sequenceDiagram
    participant C as 客户端(浏览器)
    participant AS as 授权服务器
    C->>C: 生成一对公私钥(ES256)
    C->>C: 签 DPoP proof(htm=POST, htu=token端点)
    C->>AS: POST /token + DPoP: <proof>
    AS->>AS: 验 proof, 取出公钥算指纹 jkt
    AS-->>C: 访问令牌(cnf.jkt = 公钥指纹)

关键在最后一步:授权服务器把客户端公钥的 SHA-256 指纹记进令牌的 cnf(确认声明)里,字段名叫 jkt:

{
  "sub": "alice",
  "cnf": { "jkt": "0ZcOCORZNYy-..." }
}

jkt = JWK SHA-256 Thumbprint(JWK 的 SHA-256 指纹,算法见 RFC 7638)。这一步就完成了“绑定”:这令牌从此只认指纹为 0ZcOCORZNYy-... 的那把公钥。

对照上一篇:证书绑定用的是 cnf.x5t#S256(证书指纹),DPoP 用的是 cnf.jkt(公钥指纹)。骨架都是 cnf,区别只在绑证书还是绑公钥。

第二段:拿令牌访问资源

sequenceDiagram
    participant C as 客户端
    participant RS as 资源服务器
    C->>C: 新签一个 DPoP proof(htm=GET, htu=/orders, ath=令牌哈希)
    C->>RS: GET /orders<br/>Authorization: DPoP <token><br/>DPoP: <proof>
    RS->>RS: ① 验 proof 签名(用 proof 里的 jwk)
    RS->>RS: ② 算 jwk 指纹, 比对令牌里的 cnf.jkt
    RS->>RS: ③ 核对 htm/htu 是否匹配本次请求
    RS->>RS: ④ 核对 ath 是否等于本次令牌的哈希
    RS->>RS: ⑤ 检查 jti 没被用过、iat 没过期
    RS-->>C: 全过 → 放行 ✅

注意这里有个容易忽略的细节:访问资源时,请求头的 认证方案从 Bearer 换成了 DPoP:

Authorization: DPoP eyJhbGci...   ← 令牌本身
DPoP: eyJ0eXAi...                  ← 本次的 proof

一个头放令牌,一个头放 proof。服务端两个都要验。


5. 服务端到底验哪几步

把第二段那五步展开说清楚,这是接 DPoP 时最容易漏的地方。资源服务器收到请求后:

  1. 验 proof 是个合法 JWT:typdpop+jwt,alg 是允许的非对称算法(别接受 none,也别接受对称算法)。
  2. 用 proof 头里的 jwk 验签名:签名对,说明这张 proof 确实是这把私钥签的。
  3. jwk 的指纹,和令牌里的 cnf.jkt 比对:一致,才说明“签 proof 的私钥”和“令牌绑定的公钥”是一对。这一步是防盗用的核心。
  4. 核对 htm / htu:和本次请求的实际方法、URI 一致。防止 proof 被挪用到别的接口。
  5. 核对 ath:等于本次带来的访问令牌的 SHA-256 哈希。防止令牌和 proof 错配。
  6. 检查 iat 在可接受的时间窗内(比如几秒到几分钟),jti 没在窗口内出现过。防重放。
  7. (若启用 nonce)核对 nonce 是不是服务端最近下发的那个。

漏掉第 3 步,DPoP 就退化成了普通 Bearer——proof 签得再漂亮,不和令牌指纹比对,等于白签。这是我见过最常见的实现错误。


6. 防重放:jti、时间窗与 DPoP-Nonce

DPoP proof 是明文放在 HTTP 头里的,理论上会被中间人看到。所以“别人录下一个 proof、原样再发一次”这种重放攻击必须防住。RFC 9449 给了三层防线:

第一层:htm + htu proof 钉死了方法和地址,重放只能重放到同一个接口的同一个动作上,先砍掉一大片攻击面。

第二层:jti + 时间窗。 服务端在一个短时间窗(比如 60 秒)内,记住见过的 jti;重复出现就拒绝。窗口一过,iat 太老的 proof 本身也失效了,记录可以清掉,不至于让服务端无限存 jti

第三层:DPoP-Nonce(可选,但推荐)。 前两层依赖服务端存状态(记 jti)。如果服务端是无状态的、或者水平扩展了好多台,记 jti 就变麻烦。这时可以让服务端主动下发一个 nonce:

sequenceDiagram
    participant C as 客户端
    participant S as 服务端
    C->>S: 请求(proof 里没 nonce)
    S-->>C: 401 + DPoP-Nonce: abc123
    C->>C: 重签 proof, 加上 nonce=abc123
    C->>S: 重新请求(proof 带 nonce)
    S-->>S: nonce 是我发的且新鲜 → 放行 ✅

nonce 由服务端掌控、可自带时效,相当于服务端说“这张 proof 必须用我刚给你的这个暗号”,把“proof 何时有效”的主动权收回到服务端手里。代价是每次可能多一轮 401 往返(客户端可以缓存 nonce 复用一段时间来摊薄)。


7. DPoP vs 证书绑定,到底怎么选

放一张对比表收口:

维度 Certificate-Bound(mTLS) DPoP
绑定对象 X.509 客户端证书 客户端自生成的公钥
cnf 字段 x5t#S256(证书指纹) jkt(公钥指纹)
证明发生在 TLS 层(握手) 应用层(HTTP 头)
需要客户端证书吗 需要 不需要
适合谁 机房里的服务、服务间通信 浏览器、SPA、移动 App
运维负担 证书签发/分发/轮换 客户端自己生成密钥,几乎无运维
每次请求开销 TLS 层承担,应用无感 每请求多签一个 JWT + 多两个头

一句话决策:

客户端能优雅地管 X.509 证书吗?能——上 mTLS + 证书绑定;不能(浏览器/App)——上 DPoP。

两者不是对立的,大型系统里常常并存:南北向(用户到网关)走 DPoP,东西向(服务到服务)走 mTLS。


8. 后台服务之间调用,适合用 DPoP 吗?

这是我被追问最多的一个问题:“DPoP 就是一对公私钥 + 签个 JWT,后台服务之间也能生成密钥、也能签 JWT,那服务间调用干脆全上 DPoP 不就完了,还省得折腾证书?”

技术上完全可行,DPoP 并没有规定“只准浏览器用”。但在标准的、南北向的服务间通信里,它通常不是首选。原因得掰开看。

为什么服务间通常不首选 DPoP

DPoP 是为“摸不到 TLS 层”的客户端设计的——浏览器里的 JS 没法要求用某张客户端证书握手,只能退而求其次,在应用层自己签证明。而后台服务之间,恰恰优雅地控制 TLS 层:配一张客户端证书对服务来说是常规操作,Kubernetes、服务网格更是把这事自动化了。既然能在更底层、更省事地解决,就没必要绕到应用层。

具体差在这几点:

维度 服务间用 mTLS + 证书绑定 服务间用 DPoP
证明发生在 TLS 握手,一次连接谈一次 每个请求都要现签一个 proof JWT
每请求开销 复用 TLS 连接,应用几乎无感 每请求多一次签名 + 多两个头
连接安全 天然要求加密通道(mTLS) DPoP 本身不管传输,仍需另配 TLS
生态适配 服务网格/K8s 原生支持 要自己实现 proof 生成与校验
运维 证书签发/轮换,但已有成熟工具 密钥管理要自己扛

一句话:服务能管好证书的场合,mTLS 在更低的层把事办了,还顺带保证了通道加密;DPoP 在这里等于“用应用层的力气,去做 TLS 层本可以做的事”,每个请求还多一份签名开销。

那什么时候服务间也值得用 DPoP?

也别一棍子打死。下面几种情况,服务间用 DPoP 反而合理:

  • 中间有 TLS 终止(TLS termination)。 如果流量在负载均衡、API 网关、CDN 处就把 TLS 卸载了,后端拿到的是明文 HTTP,mTLS 的客户端证书信息在这一跳就“断了”。这时应用层的 DPoP 能穿透这些中间层,因为它绑在 HTTP 消息里,不依赖端到端的 TLS。
  • 调用方本身就是个“类浏览器”客户端。 比如一个 BFF(Backend for Frontend)、或某些 Serverless 函数,拿不到稳定的客户端证书身份,那它对下游的角色更接近公网客户端,用 DPoP 顺理成章。
  • 想要“每请求级”的绑定粒度。 mTLS 绑的是连接,DPoP 绑的是每一个请求(htm/htu/ath 都在 proof 里)。如果你的威胁模型里,连接建立后单个请求被篡改/挪用也是要防的,DPoP 的粒度更细。
  • 统一栈,减少两套机制。 如果南北向已经全量铺了 DPoP,团队又不想再维护一套 mTLS 证书体系,东西向复用 DPoP 能降低认知和运维成本——这是工程权衡,不是安全最优。
flowchart TD
    Q{服务间调用} --> A{能稳定持有<br/>客户端证书?}
    A -- 能 --> B{中间有 TLS 终止<br/>导致证书身份丢失?}
    A -- 不能<br/>类浏览器角色 --> D[用 DPoP]
    B -- 否 --> C[首选 mTLS + 证书绑定]
    B -- 是 --> D
    C --> E{需要每请求级绑定?}
    E -- 需要 --> F[mTLS 之上再叠 DPoP]
    E -- 不需要 --> C

如果你在云原生环境里,除了 mTLS 和 DPoP,还有第三条路——JWT-SVID,下一节专门说它。先记住结论:

服务能握住证书,就让 mTLS 在 TLS 层把认证和加密一起办了;一旦证书身份在中间层断掉、或调用方本就拿不到证书,DPoP(或 JWT-SVID)这种应用层的持有证明才真正派上用场。


9. 顺带说说 JWT-SVID

上面几次提到 JWT-SVID,这里单独讲清楚,因为它和 DPoP 的思路很近,但定位不同,容易混。

它是什么

先补两个 SPIFFE 的概念(SPIFFE 是一套给工作负载发身份的开放标准,workload 可以理解成"一个跑着的服务实例"):

  • SPIFFE ID:工作负载的身份,长得像一个 URI,比如 spiffe://prod.acme.com/billing/api。它回答"我是谁"。
  • SVID(SPIFFE Verifiable Identity Document):把 SPIFFE ID 装进一个可密码学验证的凭证。有两种格式:
  • X.509-SVID:一张短生命周期证书,天生配 mTLS。
  • JWT-SVID:一个 JWT,把身份装进令牌里。

所以 JWT-SVID 就是"用 JWT 承载工作负载身份"。工作负载通过 SPIFFE Workload API(通常由 SPIRE 这类实现提供)去领它——领的时候不用自己填账号密码,SPIRE Agent 靠检查进程的内核元数据(比如是哪个 Unix 进程、哪个 K8s Pod)来确认"你确实是那个工作负载",再签发对应的 JWT-SVID。

它长什么样

JWT-SVID 没有发明新字段,就是一个受约束的普通 JWT(遵循 RFC 7519)。关键就两个 claim:

{
  "sub": "spiffe://prod.acme.com/billing/api",
  "aud": ["spiffe://prod.acme.com/reports"],
  "exp": 1705127056
}
  • sub 必须是签发对象的 SPIFFE ID——这是断言身份的主战场,"我是 billing/api 这个服务"。
  • aud(受众)必填,且验证方必须拒绝没有 audaud 不含自己的令牌。 这一条是 JWT-SVID 防滥用的命门(下面展开)。
  • exp(过期)必填,验证方必须拒收过期令牌(允许一点点时钟余量)。

验证方拿到令牌后,用信任域(trust domain)的公钥集(trust bundle)验签,再检查 aud 里有没有自己、exp 有没有过。SPIRE 还贴心地提供了 ValidateJWTSVID 接口,让你把验证逻辑甩给 Workload API,自己少写容易出错的校验代码。

和 DPoP 的关键区别:绑没绑私钥

这是最容易混、也最要紧的一点:

DPoP 是"持有证明"——绑定客户端私钥,偷走令牌没私钥就用不了。JWT-SVID 默认是"身份断言"——它本身更接近一个 Bearer 型的身份令牌,靠短生命周期 + aud 收窄来控制风险,而不是靠请求级签名。

换句话说,JWT-SVID 回答的是"我是谁"(认证),DPoP 回答的是"这令牌是不是我的"(持有证明)。JWT-SVID 一旦泄露,在有效期内、且面向匹配 aud 的那个服务,是可以被冒用的——所以 SPIFFE 反复强调两件事:

  1. aud 要收得尽量窄,scope 到具体的目标服务。用 spiffe://example.org/reports 这种精确值,别用 production 或整个信任域这种大范围值——因为 aud 越宽,一旦某个服务被攻破,拿着这个令牌就能横向冒充越多下游。
  2. 有效期要短,把泄露窗口压到最小。

X.509-SVID 因为走 mTLS,私钥不出工作负载,天然带持有证明;JWT-SVID 图的是"能穿透中间层、跨信任边界、不依赖 TLS 通道"的灵活,代价就是把安全重心放在了 audexp 上。

三者放一起对照

X.509-SVID (mTLS) JWT-SVID DPoP
载体 短命证书 JWT Bearer 令牌 + 每请求 proof
身份放在 证书 SAN(SPIFFE ID) sub(SPIFFE ID) 令牌 sub
防盗用靠 私钥不出工作负载(TLS 层) exp + 窄 aud 每请求私钥签名
能穿透 TLS 终止吗 不能
典型场景 服务网格东西向 跨边界/经网关传身份 浏览器、App、公网客户端

什么时候用 JWT-SVID

  • 流量要经过 L7 网关、消息队列、或跨信任域,mTLS 的端到端证书身份在中间断了,但你又想把"我是谁"可验证地传下去。
  • 对端只想验证一个 JWT,不想为你单独配 mTLS。
  • 想要它更安全,就在外面再套一层 mTLS(通道加密 + 传输层持有证明),或按需给它叠 PoP 语义——这也是 SPIFFE 和前面 PoP 那套能接上的地方。

一句话记住它:

JWT-SVID = 给工作负载的、以 SPIFFE ID 为身份、用 aud 收窄、靠短命兜底的身份令牌。它管"我是谁",持有证明得靠 mTLS 或 DPoP 另配。


10. 接 DPoP 时的几个坑

我把容易翻车的地方列成清单,接的时候对着看:

  • 私钥别持久化到能被 XSS 读到的地方。 浏览器里最好用 Web Crypto 生成不可导出(non-extractable)的密钥,存进 IndexedDB。私钥一旦能被 JS 读出来,DPoP 的意义就打了折扣——虽然仍比裸 Bearer 强(攻击者得实时在你的页面里签,没法把令牌带走离线用)。
  • htu 要规范化再比对。 大小写、默认端口、结尾斜杠、query/fragment……客户端和服务端对 URI 的理解要一致,否则会莫名其妙验不过。规范要求 htu 去掉 query 和 fragment。
  • 时钟别差太多。 iat 的时间窗校验依赖两端时钟接近。服务器集群记得对时(NTP),时间窗留一点余量。
  • ath 别忘了。 访问资源时缺 ath,或者 ath 算错(比如对令牌做了额外编码),都会被拒。它是对令牌原始字符串做 SHA-256 再 base64url。
  • 算法钉死。 只接受你明确支持的非对称算法(如 ES256),显式拒绝 none 和对称算法,防算法混淆攻击。
  • 别忘了刷新令牌也能绑。 DPoP 不只绑访问令牌,刷新令牌(refresh token)同样可以绑,这样刷新令牌泄露也用不了。

最后一句

回到开头那个问题:浏览器里的令牌怎么防盗?DPoP 给的答案很聪明——不去改 TLS,而是让客户端每次请求都当场“按个指纹”。

它把安全的重心从“保护一串会到处乱跑的令牌”,挪到了“保护一把待在客户端、签完就完的私钥”。令牌可以被日志、被抓包、被 XSS 看到,但只要私钥不出客户端,偷走的令牌就是一张按不出指纹的废卡。

行动清单

想在自己的公网客户端上 DPoP,按这个顺序:

  1. 客户端生成密钥:优先 ES256,浏览器用 Web Crypto 生成不可导出密钥,存 IndexedDB。
  2. 取令牌时带 proof:换令牌那步就附上 DPoP proof,让 AS 把 cnf.jkt 写进令牌。
  3. 每次请求现签 proof:填对 htm/htu/iat/jti,访问资源时别忘 ath
  4. 认证头用 DPoP 而非 Bearer:令牌放 Authorization: DPoP,proof 放 DPoP 头。
  5. 服务端七步全做:尤其别漏“算 jwk 指纹比对 cnf.jkt”这步。
  6. 加防重放:短时间窗 + jti 去重;无状态服务上 DPoP-Nonce
  7. 考虑绑刷新令牌:把 refresh token 也 DPoP 绑定,收益很大。

一句话收尾:

Bearer 是不记名的票,证书绑定是给机房服务记名,DPoP 是给浏览器里的用户记名——同一把锁,换了个能塞进浏览器的锁芯。

参考资料

全文思维导图

@startmindmap
<style>
mindmapDiagram {
  node {
    BackgroundColor #F8F9FA
    RoundCorner 10
    Padding 10
    FontSize 13
  }
  :depth(0) {
    BackgroundColor #1E3A5F
    FontColor white
    FontSize 18
    FontStyle bold
  }
  :depth(1) {
    FontSize 15
    FontStyle bold
  }
  :depth(2) {
    FontSize 13
  }
}
</style>

* DPoP 详解
** 解决什么
*** 浏览器/App 管不了 mTLS 证书
*** 把持有证明挪到应用层
*** 每请求多两个 HTTP 头
** DPoP proof
*** 客户端私钥签的小 JWT
*** typ=dpop+jwt
*** jwk 只放公钥
*** 每次请求现签
** 关键字段
*** htm/htu:钉死方法和地址
*** jti:防重放唯一ID
*** iat:时间戳
*** ath:令牌哈希,绑住令牌
*** nonce:服务端下发
** 两段流程
*** 取令牌:AS 写 cnf.jkt
*** 用令牌:Authorization: DPoP
*** 令牌+proof 两个头
** 服务端校验
*** 验签名(用 proof 的 jwk)
*** 算 jwk 指纹比对 cnf.jkt
*** 核对 htm/htu/ath
*** 查 jti/iat 防重放
** 防重放三层
*** htm+htu 限定
*** jti+时间窗去重
*** DPoP-Nonce 服务端主控
** vs 证书绑定
*** 证书绑定:x5t#S256,TLS层,服务间
*** DPoP:jkt,应用层,公网客户端
*** 大系统常并存
** 服务间要用吗
*** 能握证书:首选 mTLS
*** mTLS 顺带加密通道
*** 例外:TLS 终止致证书身份丢失
*** 例外:BFF/Serverless 类浏览器角色
*** 例外:要每请求级绑定
** JWT-SVID
*** SPIFFE 工作负载身份
*** sub=SPIFFE ID
*** aud 必填且收窄
*** exp 必填,短命兜底
*** 管"我是谁",非持有证明
*** 能穿透 TLS 终止
*** 更安全再套 mTLS/DPoP
@endmindmap

DPoP 详解 思维导图


本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。 欢迎在我的个人网站 https://www.fanyamin.com 访问原文并评论。