安全 API 设计:你们是用 UUID 还是 HEX?

xiaoronglv · 2022年08月29日 · 最后由 hooopo 回复于 2022年09月12日 · 1648 次阅读

通常大家设计 Restful API 时,会直接返回资源的数字 id 给前端。比如以下一个关于账单的 API,返回当前绑定的信用卡,它的资源 id 是一个数字。

GET /v1/credit_cards.json

{
  id: 13232,
  number: "61333313333000",
 ...
}

为了安全,大家都会加上过滤条件 where(company_id: current_company.id)

CreditCardsController
  def index
    render CreditCard.credit_cards.where(company_id: current_company.id)
  end
end

这样公司 A 的员工就不能访问公司 B 的数据了。但是如果万一有程序员忘记加 where(company_id: current_company.id) 这个 filter,就会导致数据泄露。

我看到有些公司的做法是,返回十六进制的 id。

比如 checkr

https://docs.checkr.com/#operation/getCandidate

第一个问题:为什么大家不在 API 中用 uuid 呢?

创建方法

SecureRandom.uuid
  1. 如果用 uuid,黑客更不容易拼出完整 URL。
  2. 万一某个 API 有安全漏洞,也不容易导致大规模的数据泄露。

比如 API response 范例


{
  uuid: "6c8aa96e-4293-408c-baf7-01980faec5bc",
  number: "61333313333000",

}

第二个问题:大家在设计企业应用时,返回给前端(合作伙伴)的是 uuid 还是十六进制的 id?

感觉十六进制的 id 还是比较容易猜的。

SecureRandom.hex

第三个问题:大家在做集成时,uuid 和 hex,哪个更常见?

我一般情况会用 uuid,有些场景和系统会对 ID 长度有要求没办法用 uuid

以传统 MySQL 的主键索引习惯为例,数据表在文件中按主键顺序存储,如果使用 UUID 作为主键且采用了无序的设计,会导致新增数据需要重新排列。或者采用自增主键与 UUID 索引键共存的方案,对外暴露 UUID 索引键。

如果想只用一个主键,应该采用能按大小顺序生成序列的键,比如 雪花算法

(其它数据库系统或主键索引策略不了解)

Uuid 太长了 不太好看 我自己喜欢生成一个 I'd

还是比较喜欢生成纯数字的

GraphQL 里的方式是输出的时候可以选择给 ID 加密输出,接收的时候再 restore,我觉得这种方式不错,数据库也可以使用传统的 id

我现在一般用 UUID,不用数据库自增长 id 了。自增长 id 容易被猜到临近的资源和数据库实际规模。后期改分布式比较麻烦。优点就是简单和短。UUID v4 方便,但在实际使用中通常觉得太长,尤其是 URL 中使用,而且有些人会有 id 按时间排序的要求。这点应该只有 UUID v5 可以做得到,但它本质上需要开发者自己保证唯一性。

现在互联网应用也很少看到自增长 id,而是用各种字符 id。实际项目中对 ID 一般考虑几点:

  1. 是否需要前后端都可以生成
  2. 是否需要异构的微服务都方便生成
  3. 是否需要中心化的 ID 生成服务,还是每个服务可以自己生成(只要遵循统一的算法)

如果考虑 1,那么只能考虑 JS 也能生成的方式,比如 UUID v4 / NanoID / Hashids 都行。如果考虑 2,那么需要一个有多种主流语言实现的方式,或者雪花算法。这个思路其实很值得学习,基本上是分布式 ID 的通用套路了。另外 MongoDB 的 ID 生成方式也可以考虑。

贴一个 Instagram 的分布式 ID 生成思路。能做到 Postgres 里去,保持数据库生成 ID 的同时效率不低。

  1. UUID + MySQL 的话,数据迁移是灾难,虽然数据迁移的概率很低,但估计要比迁移到分布式数据库发生的概率高很多
  2. UUID + InnoDB 索引膨胀,因为 InnoDB 的二级索引是指向主键的,就是多一条二级索引就膨胀一些,当然还有 buffer pool 和磁盘之类一定也膨胀

所以选不选 uuid 是一个技术层面的需求,和你使用具体的 DB 直接相关。而业务相关的需求完全可以通过其他手段解决,比如 https://github.com/namick/obfuscate_id 你完全可以使用 bigint,然后暴露给外部 string,再反向解析回来。退一步讲,真的有分布式需求或者客户端同步需求,从 bigint 往 UUID 迁移也是容易的。

hooopo 回复

问一下,Postgres 应该没有 UUID 主键和索引的问题吧?

另外个人感觉迁移主键没有容易的,因为涉及到外键引用,大量迁移关联数据是个挺麻烦的事儿。

darkbaby123 回复

  • pg 的主键和二级索引是没有区别的,都直接指向磁盘的一个位置
  • mysql innodb 的主键和二级索引是有区别的,通过二级索引查询先找到主键,主键再找磁盘位置;二级索引上是会冗余主键值的。

所以 mysql 在主键查询场景优于 pg;pg 在二级索引查询场景优于 mysql

自己写一个编解码就好了

HDJ 回复

hashid

都多做大的系统嘛,snowflake 还不够用?

uuid 这类没法排序,推荐推特的 snowflake,能排序还不是顺序生成的,有开源的 gem,也有基于 pg 的实现,可以参看 mastodon

hooopo 回复

既然 pg 的主键跟 mysql 的主键都指向磁盘文件位置,没看懂 mysql 主键查询场景优于 pg 的结论怎么来的

acaby 回复

优的地方在于顺序,mysql 的聚集索引是按主键顺序排好序的,而 pg 的是堆表,无序,在范围查询会有差异 比如 where id > 1000

有时间我可以详细写篇博客

需要 登录 后方可回复, 如果你还没有账号请 注册新账号