ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微服务你的API ID还在用自增ID就out了。。。

微服务你的API ID还在用自增ID就out了。。。 我见过太多 API 的响应体里id字段长得像数据库里的自增值{id:1000,status:已发货,customerId:521}前端拿到这个 1000存下来下次请求直接拼到 URL 里GET /api/orders/1000。一切正常——直到某天它不正常了。业务决定把单库拆成微服务订单表被分片了。自增 ID 在不同分片里重复了。A系统的 521 号用户和B系统的 521 号用户根本不是同一个人。两个数据集合并的时候彻底乱了。而且隔壁公司每天中午爬一下你的订单数据按 ID 号推一下日订单量你的业务规模直接被猜了个七七八八。下面这五个模式能让你的接口不靠“运气”活着。模式一用 UUIDv7 或 ULID 当对外 ID❌新手做法直接把数据库自增 ID 当公共资源 ID。typeOrderstruct{IDint64gorm:primaryKey;autoIncrementStatusstring}// 直接就把 ID 抛出去了✅进阶做法给每个实体配一个独立的publicId在创建时生成一次——用 UUIDv7 或 ULID。它们是时间有序的 128 位标识符插入时保持顺序不像随机 UUIDv4 那样让索引碎片化。typeOrderstruct{IDint64gorm:primaryKey;autoIncrement// 内部用对外隐藏PublicIDstringgorm:uniqueIndex;not null// UUIDv7Statusstring}funcNewOrder(req CreateOrderRequest)*Order{returnOrder{PublicID:uuid.Must(uuid.NewV7()).String(),// 创建时生成一次Status:PENDING,}}// 对外接口只用 PublicIDfuncGetOrder(publicIDstring)(*Order,error){returnrepo.FindByPublicID(publicID)}为什么有用索引友好UUIDv7 里的时间戳让插入保持顺序不会像 UUIDv4 那样乱跳不可猜测/orders/1043不会是/orders/1042的下一个缺点两个 ID 能看出谁先谁后。如果不想暴露顺序信息用 UUIDv4 或完全不透明的 token模式二把链接嵌入响应别让客户端自己拼❌新手做法只返回 ID让每个客户端自己去拼 URL{id:1000,status:已发货}url:/api/orders/strconv.Itoa(order.ID)// 每个客户端都写一遍✅进阶做法在响应里嵌入links字段让客户端直接跟着 URL 走而不是自己拼typeOrderResponsestruct{IDstringjson:idStatusstringjson:statusLinksmap[string]stringjson:links}funcToOrderResponse(order*Order)OrderResponse{base:/api/orders/order.PublicIDreturnOrderResponse{ID:order.PublicID,Status:order.Status,Links:map[string]string{self:base,cancel:base/cancel,},}}为什么有用客户端只管跟着self走路径变了只改服务端一处新集成方从响应里就能看到可用操作不用翻文档原始 ID 还在方便存储引用模式三把内部主键和对外身份彻底分开❌新手做法一个字段既当数据库主键又当 REST 标识符typeCustomerstruct{IDint64gorm:primaryKey// 这个 ID 既干数据库的活又干 API 的活}✅进阶做法两个字段两套职责typeCustomerstruct{IDint64gorm:primaryKey// 数据库内部做 JOIN 用PublicIDstringgorm:uniqueIndex;not null// 对外所有 HTTP/事件交互用Namestring}func(r*CustomerRepository)FindByPublicID(publicIDstring)(*Customer,error){// 对外查询用 PublicID不是内部 ID}为什么有用控制器和下游服务只看到publicId换存储引擎不涉及任何对外变更事件比如OrderPlaced里传递publicId下游不会知道你用了什么存储方案内部 ID 可以纯粹按数据库性能优化模式四所有 URI 都从对外 ID 构建❌新手做法201 Created的Location头直接用内部 IDlocation:fmt.Sprintf(/api/orders/%d,order.ID)// 内部 ID 泄露了c.Header(Location,location)✅进阶做法每个 URI 都从publicId构建内部 ID 永远不会出现在头、响应体或对外日志里location:fmt.Sprintf(/api/orders/%s,order.PublicID)// 只用 PublicIDc.Header(Location,location)为什么有用书签或存储的 URL 不会因为存储层变化而失效API 版本升级只改前缀标识符本身不变模式五不可猜测 ≠ 已授权❌新手做法即使换成了 UUID只检查这个publicId存不存在不检查它是不是属于当前用户。一个 UUID 一旦泄露截图、分享链接、浏览器历史、日志就能被拿来访问任何订单。funcGetOrder(publicIDstring)(*Order,error){returnrepo.FindByPublicID(publicID)// 谁都能拿任何订单}✅进阶做法把“所有权检查”直接放进查询条件里让“不存在”和“不属于你”返回同一个结果funcGetOrder(publicIDstring,customerIDint64)(*Order,error){returnrepo.FindByPublicIDAndCustomerID(publicID,customerID)// 查不到就是查不到不给任何额外信息}为什么有用“不存在”和“不属于你”返回相同的 404攻击者得不到任何额外信息所有权判断在查询层完成不属于你的记录根本不会加载到内存里把这一套用上创建publicId在工厂方法里生成一次内部id永远不对外暴露嵌入links让客户端跟着走在查询里检查所有权查不到就是查不到一个 API 能活过多少次重构、分库、合并——往往不是因为它用了多先进的架构而是因为它从一开始就没把内部实现细节暴露给外部。
返回列表