登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

用 uuid 包校验外部请求中的标识符

来源:17golang原创

时间:2026-10-09 05:49:34 290浏览 收藏

外部请求中的 UUID 应该在传输层只解析一次,再以 uuid.UUID 传给业务层。一个可靠的入口至少要处理五件事:空值与长度限制、uuid.Parse 错误、接口是否只接受规范文本、Nil UUID 是否允许,以及对客户端与内部日志分别输出什么。

我见过最常见的写法是把路径参数一路当 string 传到数据库层,直到查询为空才发现调用方传了坏 ID。另一种极端是对不可信请求使用 MustParse,结果一个格式错误就把普通的 400 响应变成 panic。把校验前移并不复杂,关键是先定义清楚“语法合法”和“业务允许”不是同一件事。

先划清格式校验与业务校验

Go 标准库 uuid.Parse 负责把文本解析为 uuid.UUID。官方文档说明,它接受常见的带连字符形式,也接受花括号、urn:uuid: 前缀、无连字符形式,十六进制字母大小写均可。解析成功只证明文本能够表示一个 UUID,不代表这个值在你的接口里一定有效。

检查层典型规则放置位置
传输限制必填、长度上限、请求体上限HTTP 入口
UUID 语法uuid.Parse 成功统一解析函数
表示策略接受多种形式或仅接受规范小写形式接口契约
业务规则是否允许 Nil、是否属于当前租户传输层与业务层分工
资源权限调用方能否读取或修改该 ID 对应对象业务授权层

UUID 校验不能替代授权。一个语法完全正确的 ID 仍可能属于其他用户,也可能根本不存在。传输层只负责把不可信文本变成明确的类型和错误;资源归属、状态与权限继续由业务层判断。

统一入口限制输入并调用 Parse

下面的函数适合路径参数、查询参数和 JSON 字符串字段复用。长度上限先挡住无意义的超长输入,Parse 负责语法,Nil 则由业务策略明确拒绝。

package requestid

import (
	"errors"
	"fmt"
	"strings"
	"uuid"
)

var (
	ErrMissingID = errors.New("missing identifier")
	ErrInvalidID = errors.New("invalid identifier")
	ErrNilID     = errors.New("nil identifier is not allowed")
)

func ParseExternalID(raw string) (uuid.UUID, error) {
	// 去除传输层常见的首尾空白,但不改写 UUID 内部字符
	value := strings.TrimSpace(raw)
	if value == "" {
		return uuid.UUID{}, ErrMissingID
	}

	// 所有官方支持的文本形式都远小于该上限,先拒绝异常长输入
	if len(value) > 64 {
		return uuid.UUID{}, ErrInvalidID
	}

	// 对外部输入使用 Parse 返回错误,不能使用会 panic 的 MustParse
	id, err := uuid.Parse(value)
	if err != nil {
		return uuid.UUID{}, fmt.Errorf("%w: %v", ErrInvalidID, err)
	}

	// Nil 是合法 UUID 值,但本接口把它定义为业务无效标识符
	if id == uuid.Nil() {
		return uuid.UUID{}, ErrNilID
	}

	return id, nil
}

MustParse 适合源码中的固定常量或启动期配置,因为失败时 panic 能暴露程序员错误;它不适合用户可控的请求值。外部输入失败应成为可预期的客户端错误,而不是进程级异常。

外部 UUID 标识符经过长度限制 Parse Nil 策略后进入业务层的静态调用关系图
图1:路径、查询参数和 JSON 字段共用一个校验边界;只有通过长度、Parse 与 Nil 策略的类型化 UUID 才进入业务层。这是原创静态结构图。

选择宽松接受还是严格规范文本

Parse 默认接受多种合法表示,这对兼容旧系统很友好。但有些公开 API 希望所有调用方都提交固定的 36 字符、小写、带连字符形式。此时不能只看 Parse 是否成功,还要把输入和 id.String() 的结果比较。

package requestid

import (
	"errors"
	"strings"
	"uuid"
)

var ErrNonCanonicalID = errors.New("identifier must use canonical form")

func ParseCanonicalID(raw string) (uuid.UUID, error) {
	// 严格接口可以允许首尾空白,也可以直接拒绝;这里选择先裁剪
	value := strings.TrimSpace(raw)
	id, err := uuid.Parse(value)
	if err != nil {
		return uuid.UUID{}, ErrInvalidID
	}

	// String 返回小写连字符形式,不完全相等就说明输入不是规范文本
	if value != id.String() {
		return uuid.UUID{}, ErrNonCanonicalID
	}

	if id == uuid.Nil() {
		return uuid.UUID{}, ErrNilID
	}

	return id, nil
}

两种策略都合理,但必须写进接口契约:

  • 兼容模式:接受官方 Parse 支持的所有形式,响应与日志统一输出 id.String()。
  • 严格模式:只接受 String() 能产生的规范形式,其他合法表示也返回 400。

不要在同一个系统里让不同 handler 随意选择。有的入口接受大写,有的入口拒绝 URN,会让客户端排错非常困难。最好由公共包提供一个固定策略,或通过清楚命名的两个函数区分。

在 HTTP handler 里返回稳定错误

客户端不需要知道解析器内部错误文本。对外保持稳定错误码和简短提示,对内日志再记录可定位的信息。路径参数的处理可以这样写:

package api

import (
	"encoding/json"
	"errors"
	"net/http"
	"uuid"
)

type problem struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func writeProblem(w http.ResponseWriter, status int, code, message string) {
	// 客户端只接收稳定错误码,不暴露底层解析器细节
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(problem{Code: code, Message: message})
}

func getOrder(w http.ResponseWriter, r *http.Request) {
	// PathValue 取得路由变量,随后立即跨过统一 UUID 校验边界
	id, err := requestid.ParseExternalID(r.PathValue("id"))
	if err != nil {
		code := "invalid_identifier"
		if errors.Is(err, requestid.ErrMissingID) {
			code = "missing_identifier"
		}
		writeProblem(w, http.StatusBadRequest, code, "order id is invalid")
		return
	}

	// 业务层接收 uuid.UUID,不再重复处理原始字符串
	handleOrderLookup(w, r, id)
}

func handleOrderLookup(w http.ResponseWriter, r *http.Request, id uuid.UUID) {
	// 示例只强调类型边界;真实实现应在这里做资源查询与授权
	w.WriteHeader(http.StatusNoContent)
}

示例中的 requestid 是项目内部公共包。真正部署时,建议把“字段缺失”“格式错误”“非规范表示”和“Nil 被禁用”映射为少量稳定错误码,不把 err.Error() 原样回显给客户端。

JSON 请求要限制请求体并显式校验字段

如果直接把 JSON 字段声明为 uuid.UUID,文本解码接口可以帮助解析,但字段级错误、Nil 策略和原始值日志通常不够直观。对公开 API,我更倾向先用 string DTO 接收,再调用统一函数。

package api

import (
	"encoding/json"
	"net/http"
)

type createChildRequest struct {
	ParentID string `json:"parent_id"`
}

func createChild(w http.ResponseWriter, r *http.Request) {
	// 限制请求体大小,避免用一个很小的字段入口接收无限数据
	r.Body = http.MaxBytesReader(w, r.Body, 1

如果接口允许批量 ID,还要限制数组长度,逐项返回索引化错误,避免同一请求触发大量解析或数据库查询。输入校验应在昂贵操作之前完成。

日志只记录需要的信息

无效 UUID 不是秘密,但它仍是用户可控文本。直接把完整原始值拼进日志,可能造成换行污染、超长日志或高基数字段膨胀。推荐把日志拆成固定原因和受限原文:

  • 固定字段:reason=invalid_uuid、路由名、字段名、请求追踪 ID。
  • 原始值:按长度截断,使用结构化日志字段,不参与日志消息模板拼接。
  • 解析成功:记录 id.String() 的规范值,便于跨服务检索。
  • 客户端响应:只返回稳定错误码,不回显内部堆栈或解析器细节。
UUID 原始输入 客户端错误响应 内部日志 类型化值与存储边界的静态关系图
图2:错误响应与内部审计是不同边界;客户端得到稳定错误码,日志只保留受限上下文,成功请求则使用规范 UUID 进入数据库与缓存。这是原创静态结构图。

业务层和存储层只接收 uuid.UUID

解析完成后继续传 string,会让下游再次猜测输入是否校验过。把方法签名改成 uuid.UUID,可以在编译期表达“这里已经过 UUID 语法校验”。

package order

import (
	"context"
	"uuid"
)

type Repository interface {
	// 仓储只接收类型化 UUID,避免每个实现重复解析字符串
	FindByID(ctx context.Context, id uuid.UUID) (Order, error)
}

type Cache interface {
	// 缓存层需要字符串键时,在边界统一使用规范化 String
	Get(ctx context.Context, key string) ([]byte, error)
}

func cacheKey(id uuid.UUID) string {
	// 同一个 UUID 值始终生成相同的小写连字符键
	return "order:" + id.String()
}

类型化参数不能证明资源存在或调用者有权限,但它能消除重复的文本解析和大小写歧义。数据库使用 16 字节列还是规范字符串列,可以按引擎和现有架构选择;无论哪种,都应避免同一 UUID 因文本形式不同产生重复记录。

发布检查清单

  • 确认项目导入的是 Go 标准库 uuid,不是接口相似的第三方同名包。
  • 所有外部入口共用一个解析策略,不在 handler 中散落正则表达式。
  • 不对用户输入调用 MustParse。
  • 明确接口是兼容多种 UUID 表示,还是只接受规范小写文本。
  • 明确 Nil UUID 在每个业务接口里是否允许。
  • 限制字符串、请求体和批量数组长度。
  • 客户端响应使用稳定错误码,日志记录固定原因和截断原文。
  • 业务、仓储和缓存边界优先接收 uuid.UUID。
  • 资源存在性、租户归属和操作权限继续在业务授权层检查。

对外部 UUID 做好校验的目标,不是写一个更复杂的正则,而是把不可信字符串尽早收敛为类型化值,并让格式、业务、错误响应和审计各自承担清楚的职责。

常见问题

Parse 成功是否说明数据库里一定有这个对象?

不是。Parse 只证明文本能表示 UUID。对象是否存在、是否属于当前租户、调用者是否有权限,都需要后续业务检查。

为什么不直接用正则校验 UUID?

正则容易与包实际接受的形式发生偏差,也不会返回类型化 UUID。使用官方 Parse 能让语法与后续值比较保持一致;严格文本策略可以在 Parse 后比较 String()。

外部请求可以用 MustParse 吗?

不建议。MustParse 遇到无效文本会 panic,更适合源码常量或启动配置。请求错误应返回正常的 400 响应。

Nil UUID 是解析错误吗?

不是。全零 UUID 是 RFC 9562 定义的合法特殊值。业务是否允许它,需要在 Parse 成功后单独判断。

官方资料

Go 标准库 uuid 文档:https://pkg.go.dev/uuid

RFC 9562:https://www.rfc-editor.org/rfc/rfc9562.html

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>