播放器加载中...

项目复盘:登录注册与接口联动

作者:admin

复盘这个博客里登录注册真正打通的关键链路:验证码、JWT、/me、前端本地存储、自动续期,以及页面和接口怎么联动起来。

文章图库(点击可放大)

登录注册与接口联动流程图

项目复盘:登录注册与接口联动

这篇不是讲“怎么从零写一个认证系统”,而是复盘这个博客里最关键的一条链路:注册、登录、获取当前用户、前端保存状态、带 token 请求接口、过期后自动续期,最后把页面和接口真正串起来。

很多项目一开始登录页能写出来,但真正卡住的往往不是表单,而是下面这些细节:

  • 注册接口通了,但登录后页面不知道你是谁
  • 能拿到 token,但刷新页面后状态丢了
  • 前端能显示登录成功,但发留言、发文章还是 401
  • access token 过期后,必须手动退出再登录
  • 后端有用户表,但前端拿不到头像、昵称这些资料

我这个项目复盘的重点,就是把这些“前后端真正联动”的细节讲透。

这篇文章适合谁

如果你也在做 Next.js + Django REST Framework 这种前后端分离项目,尤其正在处理登录注册、JWT、个人资料、权限接口,这篇会比较对口。

这次真正打通的是什么

不是单独做了一个登录页,而是打通了下面这条完整链路:

  1. 前端先拉验证码
  2. 注册或登录提交时,一起带上验证码
  3. 后端校验验证码
  4. 登录成功后返回 access + refresh
  5. 前端把 token 写进 localStorage
  6. 前端立刻请求 /api/auth/me/
  7. 后端返回当前用户资料,包括 username / email / is_staff / avatar_url / nickname
  8. 前端把当前用户资料缓存起来,Navbar、个人中心、留言厅、发文页都基于这份状态工作
  9. 后续受保护接口统一带 Authorization: Bearer <access>
  10. access 过期后,前端自动用 refresh 去换新 token,再自动重试原请求

这条链路通了,登录注册才不再只是“表单成功提交”。

后端这边是怎么拆的

1. 验证码接口单独提供

后端给了一个公开接口:

text
GET /api/auth/captcha/

它返回两样东西:

  • captcha_key
  • captcha_image_url

前端加载登录页和注册页时,先请求一次这个接口,把图片显示出来,并把 captcha_key 暂存起来。

这个设计的好处是清晰:

  • 图形验证码由后端生成
  • 前端只负责显示和回填
  • 登录、注册都复用同一套验证码逻辑

2. 注册接口不是裸写用户,而是先过序列化器

注册接口走的是:

text
POST /api/auth/register/

注册序列化器做了两件关键事:

  1. 校验 captcha_key + captcha_value
  2. 校验通过后再 create_user

项目里注册用的字段是:

json
{
  "username": "your_name",
  "email": "you@example.com",
  "password": "12345678",
  "captcha_key": "xxx",
  "captcha_value": "abcd"
}

这里一个重要细节是:验证码验证成功后会立即删除

也就是说,这个验证码是一次性的,防止重复利用。

3. 登录不是直接用默认 JWT,而是扩展了验证码校验

登录接口走的是:

text
POST /api/auth/token/

底层用的是 rest_framework_simplejwt,但不是直接拿默认 TokenObtainPairView 就完事了,而是自己扩展了一个带验证码的序列化器。

也就是说,登录不只是用户名密码:

json
{
  "username": "your_name",
  "password": "12345678",
  "captcha_key": "xxx",
  "captcha_value": "abcd"
}

后端顺序是:

  1. 先校验验证码
  2. 验证通过后销毁验证码
  3. 再走 JWT 登录逻辑
  4. 返回 accessrefresh

这样做的意义很直接:

把验证码挡在账号密码校验前面,减少撞库和脚本爆破风险。

4. /auth/me/ 是前后端联动的关键接口

如果只有登录接口返回 token,没有 /me,前端其实还是不知道“当前这个 token 对应的是谁”。

这个项目里专门做了:

text
GET /api/auth/me/

这个接口必须登录后访问,返回的是当前用户资料。

而且返回的不是一个干巴巴的用户名,而是直接序列化 Profile

  • username
  • email
  • is_staff
  • avatar_url
  • nickname
  • signature
  • bio

这一步很关键,因为前端顶部头像、个人中心、留言头像、管理员权限判断,都是围绕这份数据展开的。

5. 用户 Profile 用信号自动补

这个项目里用户资料不是散在用户表里的,而是单独有一个 Profile 模型。

为了避免“注册后没有 profile,前端 /me 报错”,后端还加了信号:

python
@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)
        return
    Profile.objects.get_or_create(user=instance)

这段逻辑很朴素,但很有用:

  • 新用户创建时自动补 profile
  • 老用户更新时兜底补齐 profile

这样前端拿 /me 时,不用担心 profile 缺失。

前端这边真正做了什么

1. token 没放 cookie,而是放 localStorage

这个项目前端做法是:

  • anime_blog_access
  • anime_blog_refresh
  • anime_blog_user

都存在 localStorage 里。

对应的工具函数大致是:

ts
export function setTokens(access: string, refresh?: string) {
  localStorage.setItem("anime_blog_access", access);
  if (refresh) {
    localStorage.setItem("anime_blog_refresh", refresh);
  }
}

这样做的优点是简单直接:

  • 前端页面跳转时读写方便
  • 调试时直观
  • 对这种个人博客项目足够实用

缺点也很明确:

  • 需要自己处理 XSS 风险
  • 不是最严格的安全方案

但从这个项目的阶段来看,这个取舍是合理的,重点是先把整条链路跑通。

2. 登录成功后不是直接跳首页,而是先补当前用户信息

登录页的关键流程不是“拿到 token 就结束”,而是:

ts
const data = await login(username, password, captchaKey, captchaValue);
setTokens(data.access, data.refresh);
const me = await fetchMe(data.access);
setCurrentUser(me);
router.push("/");

这一步非常重要。

因为如果你只保存 token,不立刻请求 /me

  • Navbar 头像拿不到
  • 是否管理员不知道
  • 留言、发文这种页面没法立刻切到已登录状态

换句话说,token 是“凭证”,`/me` 才是“当前用户状态”

3. 注册成功后没有自动登录,而是回到登录页

注册页流程相对保守:

ts
await register(username, email, password, captchaKey, captchaValue);
router.push("/login");

这意味着当前实现是:

  • 注册成功
  • 跳转到登录页
  • 用户再手动登录一次

这不是最省步骤的做法,但逻辑很清楚,也方便后期在注册后增加:

  • 邮箱验证
  • 审核开关
  • 注册成功提示页

对当前项目来说,这个选择是稳的。

接口联动真正难的地方,不是“能请求”,而是“状态一致”

很多前后端分离项目一开始最容易忽略的是:

登录状态不是只在登录页用一次,而是会影响整个站点。

这个项目里,登录状态实际上联动了这些功能:

  • Navbar 用户区域显示昵称和头像
  • 个人中心 /profile
  • 留言厅发留言 / 回留言
  • 文章评论
  • 点赞
  • 管理员发文章 /write
  • 后台管理 /dashboard
  • 相册上传和管理

所以真正要解决的是:

前端任何一个需要登录的请求,都得拿到可用 token。

为什么后来还补了“自动续期”

这个项目前面最实际的一个坑就是:

access token 过期后,很多操作会直接 401,导致每次改完代码或者测试久一点,就得重新退出、重新登录。

这个体验很差,也会影响调试。

后面补的方案是:

  1. 所有需要登录的请求走统一封装
  2. 请求先带 access token
  3. 如果返回 401
  4. 前端自动请求:
text
POST /api/auth/token/refresh/
  1. 拿新 access token
  2. 自动重试刚才失败的请求

这套逻辑做完之后,像留言、评论、发文、资料更新这些功能就顺多了。

大致结构是:

ts
if (res.status === 401) {
  const refreshedAccessToken = await tryRefreshAccessToken();
  // 拿到新 token 后自动重试
}

这一步是“能用”和“好用”的分水岭。

这个项目里,自动续期解决了哪些具体问题

补完自动续期以后,下面这些接口都不再需要你频繁重登:

  • 留言发布
  • 留言回复
  • 文章评论
  • 点赞
  • 个人资料读取和更新
  • 相册上传、编辑、删除
  • 仪表盘请求
  • 发文章
  • 正文图片上传

也就是说,前端后面不是各页面自己瞎处理 401,而是统一通过 API 层兜住了。

这比把刷新 token 的逻辑散在每个页面里强很多。

这次复盘下来,真正起作用的设计点有哪些

1. 验证码前置

登录、注册都先过验证码,而不是只做表单校验。

2. token 和用户资料分开处理

  • token 负责鉴权
  • /me 负责当前用户展示状态

这两个职责分开以后,前端逻辑会清楚很多。

3. Profile 独立建模

头像、昵称、签名这些扩展信息没有硬塞进用户基础表,而是放在 Profile 里,更利于后面继续扩展。

4. 登录态不只服务登录页,而是服务整站

一旦涉及留言、评论、发文、后台,登录状态就必须是“全站状态”。

5. 统一封装带 token 请求

把 token 续期、401 重试放在 api.ts,而不是散落在各个页面组件里。

这套实现还不算完美,后面还能继续补什么

虽然现在这套已经能稳定用,但如果以后要继续打磨,还可以往下做:

1. 把 token 存储策略再升级

现在是 localStorage

如果以后更强调安全性,可以考虑:

  • HttpOnly Cookie
  • CSRF 配套处理
  • 更严格的登录会话策略

2. 给登录态做全局上下文

现在主要是本地存储 + 页面初始化读取。

后面可以进一步抽成:

  • React Context
  • Zustand
  • 或者统一 auth provider

这样页面状态更新会更自然。

3. 增加更细的错误提示

比如把下面几类错误拆开:

  • 验证码过期
  • 验证码错误
  • 用户名已存在
  • 密码太短
  • refresh token 失效

这样用户体验会更完整。

4. 补登录后重定向

比如访问 /write 被打回登录页后,登录成功自动跳回原页面,而不是统一回首页。

这会更像成熟后台系统。

这次项目复盘我最明确的结论

登录注册这件事,真正难的从来不是“写两个表单”。

真正难的是把下面这些环节接起来:

  • 验证码
  • JWT
  • 当前用户资料
  • 前端本地状态
  • 权限接口
  • token 过期续期
  • 页面显示联动

把它们串起来以后,登录注册才不是一个孤立功能,而是整个站点的基础设施。

如果只做到了“能登录”,那还只是第一步。

做到“登录后整站行为一致、token 过期还能平滑续期”,前后端联动这部分才算真的跑通。

这篇复盘对应到这个项目的几个关键接口

text
GET  /api/auth/captcha/
POST /api/auth/register/
POST /api/auth/token/
POST /api/auth/token/refresh/
GET  /api/auth/me/
PATCH /api/auth/profile/

如果你也在做类似博客、后台管理或者社区型站点,这几个接口基本就是登录注册链路里最核心的一组。

互动

发表评论

已通过的评论

还没有评论。

🐱 目录