
一、关于 PoW
Proof of Work,工作量证明,原用于区块链技术。核心目的是:通过计算一道复杂的数学题来确认交易并维护网络安全的共识机制。
二、在验证码的应用
传统验证码中,人需要面临一堆扭曲图片去识别主要防护的是机器爬虫,但同时也给用户带来了一些负担。为了应对这些高速爬虫对服务器的压力,我们可以尝试 PoW 验证码,他对用户是无感的,但是在一定情况下可以有效防御高速爬虫频繁的访问对服务器的压力。
2.1 基本原理
Pow的基本准则:
- 计算成本高:每一轮PoW校验,访问者都需要付出一定的算力成本,是需要CPU资源和时间(3~2秒)的。
- 验证成本低:访问者花费3-5秒去做计算,而服务器想要验证这个答案只需要执行一次hash计算。
- 不可预测性:没有捷径去快速获得答案。
这样以来,高速爬虫想要再像之前那样每分钟执行上百次就变得很困难。因为我们可以对高风险爬虫每次都给予一个 PoW 验证,每个验证都需要花费CPU资源和时间,也就是单机爬虫,最多每分钟执行20次,并且是付出几乎所有服务器资源为代价。并且,几乎没有牺牲任何用户体验。
2.2 核心流程
客户端向服务端请求一个 PoW 挑战时,服务端生成一段 Salt,和一个 Secrt Number,之后打包发送给客户端。客户端收到后开始碰撞,发现正确答案后发送到服务端校验,通过后给予相关验证token.
mermaid
sequenceDiagram autonumber actor Client as 客户端 (Browser / App) participant Server as 服务端 participant Storage as 临时存储 / 缓存 (Redis / Memory) %% 阶段 1: 挑战获取 rect rgb(240, 248, 255) Note over Client, Server: 阶段 1: 请求与下发 PoW 挑战 Client->>Server: GET /challenge (请求生成 PoW 挑战) Server->>Server: 1. 生成 RequestID (作为 Salt)<br/>2. 随机生成 Secret Number<br/>3. 计算目标哈希 TargetHash = Argon2(SecretNum, Salt, 参数...) Server->>Storage: 暂存记录: Set(RequestID, SecretNum, TTL=60s) Server-->>Client: 返回题目 JSON (RequestID/Salt, TargetHash, 计算参数, 搜索范围 Range) end %% 阶段 2: 客户端离线碰撞 rect rgb(255, 250, 240) Note over Client: 阶段 2: 客户端本地计算与碰撞 (Wasm / JS) loop 在指定 Range 内暴力枚举猜测值 (Guess) Client->>Client: Hash = Argon2(Guess, Salt, 参数...) alt Hash == TargetHash Client->>Client: 找到匹配答案: FoundAnswer = Guess (跳出循环) end end end %% 阶段 3: 提交验证与颁发凭证 rect rgb(240, 255, 240) Note over Client, Server: 阶段 3: 答案校验与凭证发放 Client->>Server: POST /challenge-verify (RequestID, FoundAnswer) Server->>Storage: 原子读取并作废: LoadAndDelete(RequestID) alt 题目不存在或已过期 Server-->>Client: 403 Forbidden (挑战过期 / 防重放拦截) else 题目有效 Server->>Server: 校验: FoundAnswer == SecretNum ? alt 校验失败 (答案错误) Server-->>Client: 403 Forbidden (验证未通过) else 校验成功 Server->>Server: 签发业务凭据 (如 5分钟有效期的 Signed JWT / Token) Server-->>Client: 200 OK (返回 VerifyToken) end end end %% 阶段 4: 携带凭证请求业务 rect rgb(245, 245, 245) Note over Client, Server: 阶段 4: 访问目标业务接口 Client->>Server: POST /api/v1/sensitive-action<br/>Header: Authorization: Bearer <VerifyToken> Server->>Server: 验证 Token 有效性与频次 Server-->>Client: 响应业务处理结果 end2.3 可选择的算法
- SHA256 传统哈希校验:高难度下对于低端设备不友好,过于简单又无法起到防御作用
- Argon2id算法:内存密集型校验,对CPU性能差的机器稍微友好一点。
2.4 核心实现算法(Golang)
go
var ( agT uint32 = 8 agM uint32 = 16 * (1 << 10) agP uint8 = 1 ) type Challenge struct { RequestId string MaxRange int Challenge string } func (p *PoW) NewChallenge() Challenge { // Challenge 计算方式 SHA-256(RequestId + SecretNum) maxRange := 10000 c := Challenge{ RequestId: uuid.New().String(), MaxRange: maxRange, } // 生成 SecretNum secretNum := rand.Intn(maxRange) // 生成 Challenge argon := argon2.IDKey([]byte(fmt.Sprintf("%d", secretNum)), []byte(c.RequestId), agT, agM, agP, 32) c.Challenge = fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%x", agM, agT, agP, argon) // 记录 reqId 与 Challenge p.reqMap.Store(c.RequestId, c) return c } func (p *PoW) Verify(reqId string, secretNum int) bool { if v, ok := p.reqMap.Load(reqId); ok { if v, ok := v.(Challenge); ok { argon := argon2.IDKey([]byte(fmt.Sprintf("%d", secretNum)), []byte(reqId), agT, agM, agP, 32) vC := fmt.Sprintf("%x", argon) if vC == v.Challenge { return true } return false } return false } return false }三、PoW 验证码的局限性
我们一开始就说了,PoW 验证码并不是为了去校验访问者是用户还是机器人。PoW 的核心目的就是构造一种 不平衡 的关系。客户端需要付出很多算力才能得到答案,而服务端只需要进行一次校验即可达得到答案。所以我们可以从 经济性 问题上去阻碍机器人、爬虫的高频次访问。