简介
本文件面向 DouPHP 的 API 端,提供“请求签名验证”的完整说明与落地方案。内容覆盖:
- HMAC-SHA256 签名生成与校验流程
- 时间戳与防重放的时间窗口控制
- 参数排序、空值处理、特殊字符转义规则
- 中间件集成方式与配置方法
- 客户端签名示例(步骤化)与常见错误调试方法
注意:当前仓库未内置统一的通用签名中间件;本文基于现有鉴权中间件与第三方插件中的 HMAC 实现,给出可落地的扩展方案。
项目结构
DouPHP 的 API 入口位于 api 目录,鉴权通过中间件管道执行。API 鉴权模式由配置文件集中声明,具体解析与拒绝策略由基类与子类协作完成。第三方插件中提供了成熟的 HMAC-SHA256 验签实现,可作为参考与复用。
graph TB
Client["客户端"] --> MW["API 中间件管道"]
MW --> AuthMW["用户认证中间件<br/>api/middleware/UserAuthMiddleware.php"]
AuthMW --> BaseMW["抽象鉴权基类<br/>core/foundation/middleware/AbstractUserAuthMiddleware.php"]
BaseMW --> Config["鉴权模式配置<br/>api/init/middleware.php"]
AuthMW --> Controller["业务控制器"]
Controller --> PluginHMAC["HMAC 验签参考实现<br/>plugin/square/SquareService.php"]
核心组件
- 鉴权模式配置:在 api/init/middleware.php 中以键值对形式声明各模块/动作的鉴权级别(public/optional/required),并支持 work_required 子策略。
- 用户认证中间件:api/middleware/UserAuthMiddleware.php 负责从请求头提取 Bearer Token,调用 auth('api') 解析上下文,并在失败时返回 JSON 错误。
- 抽象鉴权基类:core/foundation/middleware/AbstractUserAuthMiddleware.php 提供模板方法,统一路由段解析、模式决策、身份注入与拒绝响应。
- HMAC 验签参考:plugin/square/SquareService.php 实现了 HMAC-SHA256 + Base64 的 Webhook 签名校验,使用 hash_equals 进行时序安全比较。
架构总览
下图展示“请求签名验证”在 API 请求链路中的位置:客户端携带签名与时间戳发起请求,进入中间件管道后先做鉴权模式判定,再执行签名校验(建议新增签名中间件),最后进入控制器。
sequenceDiagram
participant C as "客户端"
participant P as "中间件管道"
participant A as "用户认证中间件"
participant S as "签名校验(建议新增)"
participant Ctrl as "业务控制器"
C->>P : HTTP 请求(含 timestamp, sign, params...)
P->>A : 鉴权模式判定(public/optional/required)
A-->>P : 通过或拒绝(JSON 401/403)
P->>S : 执行签名校验(HMAC-SHA256 + 时间窗口)
S-->>P : 通过或拒绝(400/403)
P->>Ctrl : 进入控制器处理
Ctrl-->>C : 业务响应
详细组件分析
签名算法与参数规范
- 算法:HMAC-SHA256,输出为十六进制字符串(也可选择 Base64,需前后端一致)。
- 参与签名的参数集合:除 sign 本身外的所有业务参数,以及可选的 timestamp、nonce 等。
- 参数排序:按参数名 ASCII 升序排列(ksort),确保拼接串稳定。
- 空值处理:值为空字符串或 null 的参数不参与签名(过滤空值)。
- 特殊字符:保持原始字节序列参与计算,不进行额外编码;如需 URL 传输,请在网络层做标准编码,但签名前必须还原为原始值。
- 拼接串:将排序后的参数以 key=value 形式用 & 连接成字符串。
- 密钥:服务端与客户端共享的 secret,严禁硬编码,建议使用环境变量或配置中心管理。
时间戳与防重放
- 时间戳:请求携带 timestamp(秒级 Unix 时间),服务端校验其是否在允许的时间窗口内(例如 ±5 分钟)。
- 时间窗口:超过窗口的请求直接拒绝,防止重放攻击。
- 时钟同步:建议 NTP 同步服务器时间;若跨机房/多实例,需保证时间一致性。
- 可选增强:结合 nonce(一次性随机数)+ 缓存去重,进一步抵御重放。
中间件集成与配置
- 鉴权模式:在 api/init/middleware.php 中为需要签名的模块/动作设置 required,确保进入鉴权流程。
- 用户认证中间件:api/middleware/UserAuthMiddleware.php 已具备 JSON 拒绝响应能力,便于扩展签名校验逻辑。
- 抽象基类:core/foundation/middleware/AbstractUserAuthMiddleware.php 提供 handle 模板方法,可在其中插入签名校验步骤。
- 推荐做法:新建一个 SignatureMiddleware,继承或参照 UserAuthMiddleware 的模式,在 handle 中优先执行签名校验,再通过鉴权流程。
后端校验流程(建议新增签名中间件)
flowchart TD
Start(["接收请求"]) --> Extract["提取参数<br/>timestamp, sign, params..."]
Extract --> Filter["过滤空值参数"]
Filter --> Sort["按 key 升序排序"]
Sort --> Build["构建待签名字符串"]
Build --> TS{"时间戳有效?"}
TS -- 否 --> RejectTS["拒绝: 时间戳无效"]
TS -- 是 --> Calc["计算期望签名<br/>HMAC-SHA256(secret, 待签名字符串)"]
Calc --> Compare["时序安全比较<br/>hash_equals(期望, 传入sign)"]
Compare -- 否 --> RejectSig["拒绝: 签名不匹配"]
Compare -- 是 --> Next["放行至后续中间件/控制器"]
RejectTS --> End(["结束"])
RejectSig --> End
Next --> End
第三方参考实现(Square Webhook 验签)
- 该实现采用 HMAC-SHA256 + Base64,并使用 hash_equals 进行常量时间比较,避免侧信道泄露。
- 适用于理解“如何正确构造消息体、如何比较签名”的最佳实践。
与现有安全机制的关系
- CSRF:后台存在独立的 CSRF 中间件,用于表单提交防护,与 API 签名不同域,互不影响。
- 验证码:核心验证码模块使用 HMAC 存储与一次性消费,体现系统对 HMAC 的安全使用习惯。
依赖关系分析
- 中间件依赖:
- api/middleware/UserAuthMiddleware.php 依赖 core/foundation/middleware/AbstractUserAuthMiddleware.php 提供的模板方法与配置加载。
- 鉴权模式来源于 api/init/middleware.php。
- 签名实现参考:
- plugin/square/SquareService.php 展示了 HMAC-SHA256 的正确用法,可作为签名中间件的实现参考。
classDiagram
class AbstractUserAuthMiddleware {
+handle(next)
-loadConfig()
-resolveContext()
-inject(context)
-hasWorkIdentity()
-rejectUnauthenticated()
-rejectForbidden()
}
class UserAuthMiddleware {
+configFile()
+resolveContext()
+inject(context)
+hasWorkIdentity()
+rejectUnauthenticated()
+rejectForbidden()
}
class SquareService {
+verifyWebhookSignature(payload, signature, notificationUrl, signatureKey) bool
}
UserAuthMiddleware --|> AbstractUserAuthMiddleware : "继承"
UserAuthMiddleware ..> SquareService : "参考HMAC实现"
性能考虑
- 签名计算复杂度:HMAC-SHA256 为 O(n),n 为待签名字符串长度;通常开销可忽略。
- 时间窗口检查:O(1) 时间比较,极低成本。
- 建议:
- 将签名校验置于鉴权之前,尽早拒绝非法请求,减少后续资源消耗。
- 对高频接口启用限流中间件,降低被暴力破解风险。
- 避免在签名中包含大体积字段(如 base64 图片),必要时在服务端拆分或改用分片上传。
故障排查指南
- 签名不匹配
- 检查参数是否包含空值且未按规则过滤。
- 确认参数排序是否为 ASCII 升序。
- 确认密钥一致且未发生换行/空格差异。
- 对比待签名字符串是否与客户端完全一致(包括大小写、顺序)。
- 时间戳无效
- 检查客户端与服务端时间是否同步。
- 调整时间窗口范围,避免跨时区或网络延迟导致误判。
- 请求被拒绝
- 确认 api/init/middleware.php 中对应模块/动作的鉴权模式是否正确。
- 检查中间件是否按顺序执行,签名校验应在鉴权之后或与之并列。
- 日志定位
- 在签名校验失败分支记录关键信息:timestamp、sign、params 摘要(脱敏)、期望签名摘要(脱敏)。
- 结合限流与访问日志,快速定位异常来源。
结论
DouPHP 当前未内置通用 API 签名中间件,但已具备完善的鉴权中间件框架与 HMAC 安全实践参考。通过新增签名中间件,结合时间戳与可选 nonce,即可实现完整的请求签名验证与防重放保护。建议在鉴权模式配置中为敏感接口开启 required,并将签名校验纳入中间件管道,确保统一治理与安全基线。
附录
客户端签名步骤(示例)
- 准备参数:收集所有业务参数,剔除空值;加入 timestamp、nonce(可选)。
- 排序:按参数名 ASCII 升序排序。
- 拼接:将 key=value 用 & 连接成待签名字符串。
- 计算:使用 HMAC-SHA256(secret, 待签名字符串) 得到签名,转为十六进制或 Base64(与约定一致)。
- 发送:将 sign、timestamp、nonce(如有)与业务参数一并发送至服务端。
服务端签名校验要点(建议新增中间件)
- 读取请求参数:timestamp、sign、业务参数。
- 过滤空值、排序、拼接。
- 校验时间戳是否在窗口内。
- 计算期望签名并与传入签名进行常量时间比较。
- 通过则放行,否则返回 400/403。