因为信安协会原本的SSO改用户信息比较麻烦,协会的周报系统和论坛两个系统打通的任务从很早以前咕到现在,重新开发一版SSO的任务就这样提上日程。这也算是我第二个项目?
谨以本篇文章记录我在整个项目设计、开发、测试和部署过程中的一些经验。能将协会各位大佬传授的经验记录沉淀下来,算是我的一份荣幸。
因为这个项目的周期可能比较长,就先发,随时更新。
如何防止邮箱枚举
我们系统有一些端点需要对外暴露,既要让用户使用,也要防止邮箱枚举造成信息泄露。大概有这些接口:
- 通过用户名登录
- 通过邮箱重置密码
我们在设计的过程中,明确不允许用户自主注册账户,只能通过管理员在后台添加或导入,所以「注册用户」这个接口不列在这里。
「通过用户名登录」这个接口可以很容易想到「用户名或密码错误」的提示信息来规避明确告知用户名不存在,但「通过邮箱重置密码」这个如果提示邮箱错了就直接表明邮箱不存在,告知邮箱存在容易误导用户。直到我问了Deepseek,它说了一句「如果该邮箱已注册,验证码已发送至邮箱」。这句话点醒了我,怎么会有这么美妙的设计。
后来,为了对开启TOTP的用户进行验证,我们又设计了一个接口,输入邮箱,返回该用户是否开启TOTP。这个也容易泄露邮箱,我想到了一招:
- 当邮箱存在时,返回真实的信息;
- 当邮箱不存在时,不报错,从true和false里随机返回一个。
这样攻击者就区分不出哪些邮箱有账户,哪些没有了。当然这样设计TOTP的验证流程在大佬眼中欠佳,他给出了更好的解决方案。
如何利用jwt并设计TOTP验证流程
我之前在设计TOTP验证流程的时候注意过,腾讯云和Cloudflare的登录验证都是单独腾出一个页面来弄。尝试看看他们怎么处理这个流程的,可惜我网页逆向水平太差,看不懂,只能从网络Tab里捕风捉影。大概猜了一个这个。

但是感觉这样还要生成一个token给前端太麻烦了,于是改了一下——让前端先看看需不需要TOTP,再把TOTP、用户名、密码一块返回给后端,也就是上一节的那个接口对应的方案。
emmmm
Reverier对这份方案的评价
很有想法(
不过让我设计的话我可能先正常登录之后重新开个页面说totp的事
看来他倾向于另开一个页面的方案,为什么呢?
因为用户登录接口本身是有密码鉴权这个慢哈希接口来做限流的,而且登录接口本身风控比较严。你这么设计相当于开放了一个非登录鉴权的api出去,并且这个api还会查库,攻击者如果只是想把你网站弄垮的话,可以dos这个api给你数据库压力打满,而且因为没有登录态,你还追不到来源。
然后他分享了一个非常美的设计。

可以把totp登录态直接放在jwt字段里。这样前端拿到jwt一解,发现totp未登录,就继续要求totp验证。验证之后,服务器重新签发一个totp已登录的jwt。两套系统就合一起了,不用维护两拨token。服务端解码jwt发现totp开启并且未登录的时候直接视为整个未登录就好了。
这个设计实在优雅,把两套token合在一起,还让totp的验证流程更简洁了。