这里是Akanyi~ 最近完成了一个个人项目——LansnAccountCenter,一个基于 FastAPI 的身份认证中心。这里是项目结业以后的碎碎念。
最初,我只是想做一个简单的用户中心,但写着写着,我意识到把它做成一个功能更完整的身份和访问管理IAM轻量化解决方案似乎更好。于是,项目的核心定位便改为支持 OAuth 2.0 和 OpenID Connect-OIDC的身份提供商。
这意味着,LansnAccountCenter 不仅能管理自己的用户,还能为我未来的其他个人项目提供统一的账号登录功能,何乐而不为呢?
技术选型
在技术选型上,我主要考虑的是开发效率、性能和社区生态。最终我的技术栈如下:
- Web 框架: 我选择了 FastAPI + Uvicorn。它的高性能、原生的异步支持和无敌的自动文档生成深得我心。
- 数据库: SQLAlchemy (异步) + aiosqlite。我希望整个项目是异步的,所以选择了异步 ORM。配合 aiosqlite,即便只是用 SQLite,也能保证 I/O 不会成为瓶颈,对于个人项目来说绰绰有余。
- 安全:
- python-jose[cryptography]: 实现 OIDC/OAuth2 的核心,用它来处理 JWT 签名和验证。
- bcrypt: 密码安全是重中之重,用它来对用户密码进行哈希存储。
- pyotp & qrcode: 用于实现 2FA 功能,提供基于时间的一次性密码(TOTP)。
- 其他: Jinja2 用于模板渲染、pydantic-settings 优雅地管理配置、fastapi-mail 负责发送邮件、redis 用于 Session 和缓存。可以说,开发体验非常舒畅。
架构清晰、方便拓展
在项目结构上,我遵循了关注点分离的原则。
路由模块化
根据功能将路由拆分到 auth, oauth, users, two_factor 等不同模块中。这样做的好处是每个模块的职责都非常单一,后期维护和扩展都会轻松很多。
代码如下:
# main.py
app.include_router(auth.router)
app.include_router(oauth.router)
app.include_router(users.router)
app.include_router(two_factor.router)
app.include_router(captcha.router)
数据模型
在数据模型设计上,我主要围绕 OIDC 的核心概念构建了四张表:
- User: 核心用户表,我设计它支持用户名、手机、邮箱、GitHub ID 等多种登录方式,提供最大的灵活性。
- Client: 代表接入的第三方应用,存储了
client_id和client_secret等 OAuth 客户端凭证。 - UserGrant: 这是一张联结表,用于记录用户对应用的授权关系,解决了用户与应用之间多对多的授权问题。
- AuthorizationCode: 用于存储 OAuth 授权码流程中产生的临时
code。
最好玩的设计,PoW+验证码
在开发过程中,我花心思最多的地方之一就是人机验证。国内验证码成本太高了,为了有效保护钱包,我只能在验证码上下点功夫。
常规 Session Token 验证
这是基础,确保请求来自一个合法的、由服务器初始化的验证会话,防止跨站伪造请求。
工作量证明(PoW)
这是我最喜欢的部分!在用户请求发送验证码前,我要求前端必须先解决一个基于 SHA256 的哈希难题。
# routers/auth.py - 部分
def verify_pow(prefix: str, nonce: int, difficulty: int, dom_value: str) -> bool:
attempt = f"{prefix}:{nonce}:{dom_value}"
hash_result = hashlib.sha256(attempt.encode()).hexdigest()
return hash_result.startswith('0' * difficulty)
前端必须通过不断尝试 nonce 值,来计算出一个符合特定难度(比如前 N 位为 114514)的哈希值。这个过程会消耗攻击者的 CPU 资源,对于正常用户来说几乎无感,但对批量攻击的机器人来说,成本是指数级增加的。
“验证前端算出来的答案对不对。算不对?那肯定是脚本小子,直接打回去。”
后端验证
人机验证的交互是点击图片中不动的图形。 - 前端只负责记录点击的坐标 (x, y) 并传给后端。 - 后端从 session 中取出在生成挑战时就已确定的、正确的图形位置,然后在后端进行碰撞检测,不信任前端的任何判断。
# routers/auth.py - 部分
# 用session里存的真实坐标和大小来判断用户
correct_shape = captcha_data["shapes"][captcha_data["correct_index"]]
is_hit = check_hit(correct_shape, captcha_x, captcha_y)
---END---
开发这个Project很有意思。我在安全设计上有很多的自由发挥。特别是那个基于 PoW 的人机验证机制,算是我在安全与用户体验之间找到的一个比较满意的平衡点。
另外,Syncer准备上线了,这是一个跨网同步系统,有兴趣的可以关注一下~
:wq