公共头像服务 Gravatar 的使用及其安全隐患
所谓 Gravatar 的公共可识别头像服务(Globally Recognizable Avatar),就是用户通过自己的邮箱注册一个 Gravatar 的帐号,然后给此帐号上传一个自己的头像图片。 于是 Gravatar 就建立了一个全球性的“邮箱 ↔️ 头像”的关联库。 任何需要通过用户邮箱展示其头像的地方(比如:发表社交评论),都可以通过用户邮箱来向 Gravatar 获取。 这样就简化了用户需要在不同的站点上维护相同头像的麻烦。 Gravatar 有被大量广泛地使用于各种博客/个人站点/WordPress/GitHub 等。
Gravatar 的简单用法
非常简单,访问类似:https://secure.gravatar.com/avatar/{HASH}
的链接即可。
其中的 {HASH}
是通过标准的算法根据用户的邮箱计算得来的哈希值:md5(email)
/ sha256(email)
。
比如我的头像链接是:
https://secure.gravatar.com/avatar/09fe7907900043edaa672b7a2620543b。
还有一些其它的查询参数可以设置,比如大小、默认头像、头像分级等等。不在本文今天的讨论范围内,可参考:Image Requests – Gravatar Developer Docs。
本地化
另外,如果你有“你自己的网站可以访问,但是头像却加载不了”的问题,可以参考:本地化。 关于为什么要本地化:
自己做代理是因为早在十年前玩的 WordPress 的时候我就发现,官方的 Gravatar 在中国经常不能访问,但是我自己的网站却能访问,外加第三方的代理站也是经常不能访问/不稳定,所以从那时候开始我就萌生了自己代理的念头,毕竟可以和我自己的网站本身“共生死”,几个头像流量也不大。没有理由不这样做。
上面的参考链接差不多是🔟年前的代码了。Gravatar 的请求响应头部会包含一些可能包含哈希的字段,比如:
- Link:
link: <https://gravatar.com/avatar/09fe7907900043edaa672b7a2620543b>; rel="canonical"
- Content-Disposition:
content-disposition: inline; filename="09fe7907900043edaa672b7a2620543b.png"
请记得去掉,最好用白名单过滤一下就好了。
隐私与安全性㊙️
为了安全与隐私,链接中肯定是不能直接将用户的邮箱带上的,所以最容易想到且实际也被大量运用的方案就是“使用哈希后的结果”。 尽管 md5/sha256 名义上是摘要算法,不可逆,但是也免不了仍然被人们经常称其为“MD5 加密/解密”。 解密的过程并不是真正通过复杂的数学计算🧮,而仅仅是预先创建了一个 哈希 ↔️ 邮箱 的关联表,直接查表得出结果。这个表被称为“彩虹表🌈”。
而预先创建这个表需要的邮箱列表通常就是 爬取各种社区数据库/泄漏数据库/随机构造常用的 等方式得来。 一旦有了这个列表,md5/sha256 的结果也就有了。 可能是由于 md5 已经很容易被“解密”,所以 Gravatar 可能是后来把算法切换成了 sha256(没考证具体原因,反正我记得早期确实是只支持 md5 的)。 但是我觉得这完全💯没有任何用处。
随便在网上搜索🔍一下“MD5 解密”/“SHA256 解密”就能出现很多网站。比如这个:Hashes。
你可以试试把我的邮箱的哈希(09fe7907900043edaa672b7a2620543b
)粘贴上去看看我的邮箱📪是啥(“你能看见的都是我想让你看见的。”)。
然而,在当今 WordPress 占据半壁江山的时代,包括所有使用 Gravatar 作为用户头像的个人网站/博客,用户的邮箱就这样地泄露了。你可以随便找一个个人站点试试看评论区的头像。
我认为 Gravatar 也没有更好的办法解决这个问题。
我能做的🥵
为了实现我博客发表评论时“邮箱(不公开)”的承诺,我做了我自己的努力。
早期我也是直接用哈希(2015年)的,但是后来就改成了内部接口(2020年)。
接口从 /v2/avatar/{HASH}
换成了 /v3/comments/{ID}/avatar
。
后面的 {ID}
是评论的编号(注:因为我的博客目前没有“用户系统”,所以我只能根据评论编号来区分用户,否则我完全可以直接用“用户编号”来代表一个用户。),内部通过此编号反查出用户的真实邮箱,再内部代理转发 HTTP 请求获取头像。
前端不可能根据评论编号拿到用户的邮箱📮,这样就真正地保护了用户的隐私安全/我自己的数据安全。
你真的可以选择相信我而大胆地写你真实的邮箱地址,除了生成头像以及接收评论回复↩️外,不会有任何其它用途。
改接口后的性能/资源优化
在二狗那里 我发现我这个 /v3/comments/{ID}/avatar
请求不够好:
另外,昨天在跟你反馈这个问题后,我发现一个小细节/性能问题:多个评论可能是同一个人的头像,如果根据评论ID来生成头像,则强制把它们区别开了,实际上图片是同一张,这会导致请求重复/浪费网络流量。哈哈哈哈。所以我昨天简单改了下(上上面的那张图/API废弃了),内部根据邮箱生成头像“编号”返回给前端,类似一对一映射,这样请求会大量减少。
所以我再次把接口改了(2024年):/v3/comments/{ID}/avatar
➡️ /v3/avatar/{ephemeral}
。
内部会为每个邮箱生成一个可在服务重启后也尽量保持一致的、非常简单/值又小的“短生/临时(ephemeral)编号”,然后用此编号反查出用户邮箱。 和最开始使用 md5/sha256 类似,只不过这次是我用我自己选择的简单生成算法,越简单别人就越不能根据生成的值(ephemeral)反推出邮箱。
至此,一切都刚刚好。