第4章 RESTful审计接口设计原则:资源命名、HTTP方法、状态码
说到RESTful接口设计,很多新手容易掉进一个坑里——把接口设计成了RPC调用。我个人刚开始接触审计接口时也是这么干的,觉得审计嘛,就是记录操作日志,写几个/addAuditLog、/queryAuditByUserId就完事了。后来被一位架构师一顿批,才真正理解REST的精髓。
其实RESTful设计的核心就一句话:把业务操作映射到资源上,用HTTP的语义去表达动作。而不是你的接口名里包含动词。审计接口尤其如此,因为我们处理的审计记录本质上就是一组资源。
4.1 资源命名:用名词,别用动词
这是所有RESTful设计的共识,但也是最容易被违背的。
核心原则:资源用名词复数形式,路径层级反映资源之间的从属关系。
那审计接口的资源怎么命名?我建议围绕以下几个核心资源来设计:
- 审计日志 (audit-logs):最基础也是最主要的数据实体
- 审计策略 (audit-policies):定义审计规则、范围、级别
- 审计用户/主体 (subjects):谁在做操作
- 审计事件 (events):审计事件分类
看看错误和正确的对比:
| 错误命名 | 正确命名 | 说明 |
|---|---|---|
/api/addAuditLog |
POST /api/audit-logs |
动词在HTTP方法里,不在URL里 |
/api/getAuditLogById |
GET /api/audit-logs/{id} |
查询操作就用GET |
/api/deleteAuditLog |
DELETE /api/audit-logs/{id} |
删除操作对应DELETE方法 |
/api/searchAuditByUser |
GET /api/audit-logs?subjectId={userId} |
筛选条件用查询参数 |
我曾经在一个项目中看到有人把接口路径写成/api/addSystemLogForUser,这实际上是个RPC风格的远程调用。如果按REST来设计,这就是POST /api/audit-logs,然后在请求体里带上subject信息就行。
资源命名还有其他几个要点:
- 一致的命名分隔符:推荐连字符
-,不要下划线或驼峰。例如audit-policies而不是auditPolicies或audit_policies - 版本号放在路径最前面:如
/v1/audit-logs - 子资源用层级路径:如
/v1/audit-logs/{id}/comments - 复数形式更常见:用
audit-logs而不是audit-log
注意:不要为了“方便”把查询参数里该出现的内容放到路径里。比如/v1/audit-logs/user/123这种,应该写成/v1/audit-logs?subjectId=123。
路径层级只用来表示资源的从属关系,不是用来传参的。
4.2 HTTP方法:动作交给协议层
资源命名之后,HTTP方法决定了你对资源做什么。在审计接口中,5个标准方法各有用途:
| HTTP方法 | 审计接口典型场景 | 幂等性 | 安全(非幂等) |
|---|---|---|---|
| GET | 查询审计日志、审计策略列表或详情 | 是 | 是 |
| POST | 创建一条审计日志、批量写入审计流 | 否 | 否 |
| PUT | 全量更新审计策略、替换审计配置 | 是 | 否 |
| PATCH | 部分更新审计日志状态、修改审计等级 | 否 | 否 |
| DELETE | 删除过期的审计日志或审计策略 | 是 | 否 |
这里有个容易混淆的点——审计日志的“写入”。你会发现审计日志一旦生成就不太会改了,那么用PUT还是POST?我个人习惯是:如果日志ID由客户端生成,用PUT;如果ID由服务端生成,用POST。多数审计系统是由服务端生成ID,所以POST更常见。
经验之谈:审计接口中GET请求占了八成以上。你想想看,审计系统主要就是查查查。所以GET的查询设计直接影响接口可用性。
再说说GET查询的几种常见模式:
- 分页查询:
GET /v1/audit-logs?page=1&size=50 - 时间范围:
GET /v1/audit-logs?startTime=2025-01-01T00:00:00Z&endTime=2025-01-31T23:59:59Z - 条件筛选:
GET /v1/audit-logs?subjectType=USER&action=LOGIN - 排序:
GET /v1/audit-logs?sort=createdAt,desc
嗯,这里要注意一个设计陷阱——不要把操作用自定义的HTTP头来表示。我见过有人用X-Action: query头来实现不同的“动作”,这其实是把HTTP方法给扭曲了。
4.3 状态码:精准表达请求结果
状态码是RESTful接口的“呼吸灯”——它告诉调用方这次请求到底发生了什么。好多接口设计得不错,但状态码乱用,导致调用方要么处理不了错误,要么被迫去解析响应体里的错误码。
在审计接口里,我用得最多的是这几个:
| 状态码 | 含义 | 审计接口场景 |
|---|---|---|
| 200 OK | 请求成功,响应体包含数据 | GET查询成功、PUT/PATCH更新成功 |
| 201 Created | 资源创建成功 | POST创建审计日志成功 |
| 204 No Content | 请求成功,响应体为空 | DELETE删除成功、某些批量操作 |
| 400 Bad Request | 客户端请求参数有误 | 必填字段缺失、时间格式错误、参数类型不对 |
| 401 Unauthorized | 身份验证失败 | 过期或无效 |
| 403 Forbidden | 权限不足 | 用户无审计日志查看权限 |
| 404 Not Found | 资源不存在 | 查询不存在的审计日志ID |
| 409 Conflict | 资源状态冲突 | 审计策略已存在,重复创建 |
| 422 Unprocessable Entity | 语义错误,业务规则不满足 | 审计日志内容不符合规则、数据完整性校验失败 |
| 500 Internal Server Error | 服务端意外错误 | 数据库连接失败、未捕获的异常 |
| 503 Service Unavailable | 服务暂时不可用 | 审计服务熔断、限流命中 |
个人建议:不要在状态码上“耍花招”。比如用200来表示业务异常,然后在响应体里放个code字段说明“实际上失败”。这种做法是最糟糕的——调用方必须解析响应体才能知道请求是否成功。
记住:状态码反映HTTP层面的请求结果,响应体里的错误码反映业务层面的错误原因。这是两层不同的事情。
我之前在一个审计平台上,看到接口设计把“日志写入成功”返回200,“日志写入失败”也返回200,然后响应体里放个{"success": false}。调用方得先解析json再判断。这个设计对审计这种高可靠场景来说是个大坑。
正确的做法是:比如日志内容不合法,返回422 Unprocessable Entity;如果日志写入超时,返回500或者503。调用方根据状态码就能快速判断是否要重试或报错。
4.4 RESTful审计接口设计清单
总结一下,设计一个标准的RESTful审计接口,你可以对照这个清单检查:
- 资源命名:用名词复数,路径层级合理,URL里没有动词
- HTTP方法:GET用于查询,POST用于创建,PUT/PATCH用于更新,DELETE用于删除
- 状态码:成功用2xx,客户端错误用4xx,服务端错误用5xx
- 查询参数:分页、排序、筛选都用标准查询参数
- 响应格式:统一的JSON结构,包含数据、错误信息(如果有)
- 版本管理:路径包含版本号,保证向后兼容
避坑指南:我曾经在一个项目里没有严格控制接口的幂等性——用户重复提交审计日志导致重复记录。后来我们在POST接口上加了一个X-Request-Id,用幂等键来保证同样的请求只处理一次。
审计场景对数据准确性要求极高,幂等性设计是必选项,不是可选项。
4.5 知识体系图
下面这张图概括了RESTful审计接口设计的核心逻辑:资源命名→HTTP方法映射→状态码响应。你可以把它当作设计时的“快速脑图”。
这张图从左到右展示了设计的三个关键步骤。你先明确要操作的资源(审计日志、策略等),然后选择对应的HTTP方法来表达操作语义,最后用合适的状态码反馈结果。
实际项目里,大部分设计问题都出在第一步——资源划分不合理,导致后面怎么改都像打补丁。你设计阶段花时间把资源模型理清了,后面的工作会顺畅很多。
小提示:如果你刚开始做审计接口设计,不妨先把所有操作罗列出来,然后问自己一个问题——“这个操作在表达对资源的什么动作?” 找到答案,你的设计就完成了一半。