2.登录模块

登录模块与前后端认证体系

一、目标

在项目基础结构上实现完整的用户认证流程,包括:

  • 用户注册;
  • 用户登录;
  • 用户退出;
  • Access Token 与 Refresh Token 双 Token 机制;
  • 刷新页面后的登录状态恢复;
  • Axios 自动携带 Token;
  • 401 自动刷新 Token;
  • Vue Router 路由守卫;
  • 导航栏根据登录状态动态显示。

++登录模块是后续创建角色、编辑资料、聊天、语音等所有需要用户身份的功能基础。++


二、认证模块总体架构

2.1 数据流概览

text 复制代码
用户输入账号密码
    ↓
Vue 登录/注册页面
    ↓
Axios 调用 Django API
    ↓
Django authenticate / create_user
    ↓
生成 Refresh Token 和 Access Token
    ↓
Access Token 返回给前端内存保存
Refresh Token 写入 HttpOnly Cookie
    ↓
前端 Pinia 更新用户状态
    ↓
导航栏、路由守卫、页面权限随状态变化

2.2 双 Token 设计

Token 保存位置 生命周期 作用
Access Token 前端内存 / Pinia 较短,例如 2 小时 每次 API 请求认证
Refresh Token HttpOnly Cookie 较长,例如 7 天 Access Token 过期后刷新
为什么不用一个 Token?

如果只用长期 Token,一旦泄露风险较大。双 Token 的好处是:

  • Access Token 短期有效,即使泄露影响有限;
  • Refresh Token 放在 HttpOnly Cookie,JavaScript 不能读取,降低 XSS 风险;
  • 页面刷新后 Pinia 状态丢失,但 Cookie 还在,可以自动恢复登录状态。

三、后端用户数据模型

3.1 Django 内置 User

Django 自带 User 模型,包含:

  • username;
  • password;
  • email;
  • is_staff;
  • is_superuser 等。

本项目使用内置 User 处理登录认证。

3.2 UserProfile 扩展模型

为了保存头像、简介等业务字段,需要创建 UserProfile。

python 复制代码
import uuid

from django.contrib.auth.models import User
from django.db import models
from django.utils.timezone import now, localtime


def photo_upload_to(instance, filename):
    ext = filename.split('.')[-1]
    filename = f'{uuid.uuid4().hex[:10]}.{ext}'
    return f'user/photos/{instance.user_id}/{filename}'


class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    photo = models.ImageField(default='user/photos/default.png', upload_to=photo_upload_to)
    profile = models.TextField(default='谢谢你的关注', max_length=500)
    create_time = models.DateTimeField(default=now)
    update_time = models.DateTimeField(default=now)

    def __str__(self):
        return f"{self.user.username} - {localtime(self.create_time).strftime('%Y-%m-%d %H:%M:%S')}"

3.3 OneToOneField 的作用

UserProfile 与 User 是一对一关系:

text 复制代码
User 1  ─────  1 UserProfile

这样既能复用 Django 自带认证系统,又能扩展项目业务字段。

3.4 注册到 Admin

python 复制代码
from django.contrib import admin
from web.models.user import UserProfile


@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
    raw_id_fields = ('user',)

raw_id_fields 的作用:

  • 外键或一对一字段显示为 ID 输入框;
  • 避免用户数量大时后台下拉框加载过慢;
  • 提升 Django Admin 管理性能。

四、数据库迁移

修改模型后执行:

shell 复制代码
cd backend
python manage.py makemigrations
python manage.py migrate

4.1 makemigrations

生成迁移文件,不直接修改数据库。

4.2 migrate

将迁移文件真正应用到数据库,创建或修改表结构。

4.3 标准流程

text 复制代码
修改 models.py
    ↓
python manage.py makemigrations
    ↓
检查 migrations 文件
    ↓
python manage.py migrate
    ↓
数据库结构更新完成

五、后端账号 API

5.1 注册接口

功能:

  1. 获取用户名、密码;
  2. 校验非空;
  3. 检查用户名是否已存在;
  4. 创建 User;
  5. 创建 UserProfile;
  6. 生成 JWT;
  7. 返回用户信息。

关键逻辑:

python 复制代码
from django.contrib.auth.models import User
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken

from web.models.user import UserProfile


class RegisterView(APIView):
    def post(self, request):
        username = request.data.get('username', '').strip()
        password = request.data.get('password', '').strip()

        if not username or not password:
            return Response({'result': '用户名和密码不能为空'})

        if User.objects.filter(username=username).exists():
            return Response({'result': '用户名已存在'})

        user = User.objects.create_user(username=username, password=password)
        user_profile = UserProfile.objects.create(user=user)
        refresh = RefreshToken.for_user(user)

        response = Response({
            'result': 'success',
            'access': str(refresh.access_token),
            'user_id': user.id,
            'username': user.username,
            'photo': user_profile.photo.url,
            'profile': user_profile.profile,
        })
        response.set_cookie(
            key='refresh_token',
            value=str(refresh),
            httponly=True,
            samesite='Lax',
            max_age=86400 * 7,
        )
        return response

5.2 用户名重复检查易错点

错误写法:

python 复制代码
User.objects.get(username=username).exists()

原因:

  • get() 返回一个对象,或者抛异常;
  • 对象没有 .exists() 方法。

正确写法:

python 复制代码
User.objects.filter(username=username).exists()

5.3 登录接口

功能:

  1. 获取用户名和密码;
  2. 使用 authenticate 校验;
  3. 查询 UserProfile;
  4. 生成 Token;
  5. 返回用户信息。
python 复制代码
from django.contrib.auth import authenticate
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken

from web.models.user import UserProfile


class LoginView(APIView):
    def post(self, request):
        username = request.data.get('username', '').strip()
        password = request.data.get('password', '').strip()

        if not username or not password:
            return Response({'result': '用户名和密码不能为空'})

        user = authenticate(username=username, password=password)
        if not user:
            return Response({'result': '用户名或密码错误'})

        user_profile = UserProfile.objects.get(user=user)
        refresh = RefreshToken.for_user(user)

        response = Response({
            'result': 'success',
            'access': str(refresh.access_token),
            'user_id': user.id,
            'username': user.username,
            'photo': user_profile.photo.url,
            'profile': user_profile.profile,
        })
        response.set_cookie(
            key='refresh_token',
            value=str(refresh),
            httponly=True,
            samesite='Lax',
            max_age=86400 * 7,
        )
        return response

5.4 退出登录接口

python 复制代码
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated


class LogoutView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        response = Response({'result': 'success'})
        response.delete_cookie('refresh_token')
        return response

5.5 刷新 Token 接口

python 复制代码
from django.conf import settings
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken


class RefreshTokenView(APIView):
    def post(self, request):
        try:
            refresh_token = request.COOKIES.get('refresh_token')
            if not refresh_token:
                return Response({'result': 'refresh token 不存在'}, status=status.HTTP_401_UNAUTHORIZED)

            refresh = RefreshToken(refresh_token)
            response = Response({
                'result': 'success',
                'access': str(refresh.access_token),
            })

            if settings.SIMPLE_JWT.get('ROTATE_REFRESH_TOKENS'):
                refresh.set_jti()
                refresh.set_exp()
                response.set_cookie(
                    key='refresh_token',
                    value=str(refresh),
                    httponly=True,
                    samesite='Lax',
                    max_age=86400 * 7,
                )
            return response
        except Exception:
            return Response({'result': 'refresh token 已过期'}, status=status.HTTP_401_UNAUTHORIZED)

5.6 获取用户信息接口

python 复制代码
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

from web.models.user import UserProfile


class GetUserInfoView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        user = request.user
        user_profile = UserProfile.objects.get(user=user)
        return Response({
            'result': 'success',
            'user_id': user.id,
            'username': user.username,
            'photo': user_profile.photo.url,
            'profile': user_profile.profile,
        })

六、后端路由配置

python 复制代码
from django.urls import path, re_path

from web.views.index import index
from web.views.user.account.login import LoginView
from web.views.user.account.logout import LogoutView
from web.views.user.account.register import RegisterView
from web.views.user.account.refresh_token import RefreshTokenView
from web.views.user.account.get_user_info import GetUserInfoView

urlpatterns = [
    path('api/user/account/login/', LoginView.as_view()),
    path('api/user/account/logout/', LogoutView.as_view()),
    path('api/user/account/register/', RegisterView.as_view()),
    path('api/user/account/refresh_token/', RefreshTokenView.as_view()),
    path('api/user/account/get_user_info/', GetUserInfoView.as_view()),
    path('', index),
    re_path(r'^(?!media/|static/|assets/).*$', index),
]

七、前端 Pinia 用户状态

frontend/src/stores/user.js:

javascript 复制代码
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const id = ref(0)
  const username = ref('')
  const photo = ref('')
  const profile = ref('')
  const accessToken = ref('')
  const hasPulledUserInfo = ref(false)

  function isLogin() {
    return !!accessToken.value
  }

  function setAccessToken(token) {
    accessToken.value = token
  }

  function setUserInfo(data) {
    id.value = data.user_id
    username.value = data.username
    photo.value = data.photo
    profile.value = data.profile
  }

  function logout() {
    id.value = 0
    username.value = ''
    photo.value = ''
    profile.value = ''
    accessToken.value = ''
    hasPulledUserInfo.value = false
  }

  function setHasPulledUserInfo(value) {
    hasPulledUserInfo.value = value
  }

  return {
    id,
    username,
    photo,
    profile,
    accessToken,
    hasPulledUserInfo,
    isLogin,
    setAccessToken,
    setUserInfo,
    logout,
    setHasPulledUserInfo,
  }
})

7.1 为什么需要 hasPulledUserInfo?

页面刷新后:

  • Pinia 中 accessToken 会变成空;
  • 但 Cookie 中的 refresh_token 仍在;
  • 前端还没请求完用户信息时,不能立刻判断用户未登录。

hasPulledUserInfo 表示:前端已经尝试过从后端恢复登录状态。


八、Axios 请求封装

frontend/src/js/http/api.js:

javascript 复制代码
import axios from 'axios'
import { useUserStore } from '@/stores/user.js'

const BASE_URL = 'http://127.0.0.1:8000'

const api = axios.create({
  baseURL: BASE_URL,
  withCredentials: true,
})

api.interceptors.request.use(config => {
  const user = useUserStore()
  if (user.accessToken) {
    config.headers.Authorization = `Bearer ${user.accessToken}`
  }
  return config
})

let isRefreshing = false
let refreshSubscribers = []

function subscribeTokenRefresh(callback) {
  refreshSubscribers.push(callback)
}

function onRefreshed(token) {
  refreshSubscribers.forEach(callback => callback(token))
  refreshSubscribers = []
}

function onRefreshFailed(error) {
  refreshSubscribers.forEach(callback => callback(null, error))
  refreshSubscribers = []
}

api.interceptors.response.use(
  response => response,
  async error => {
    const user = useUserStore()
    const originalRequest = error?.config

    if (!originalRequest) return Promise.reject(error)

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true

      return new Promise((resolve, reject) => {
        subscribeTokenRefresh((token, err) => {
          if (err) {
            reject(err)
          } else {
            originalRequest.headers.Authorization = `Bearer ${token}`
            resolve(api(originalRequest))
          }
        })

        if (!isRefreshing) {
          isRefreshing = true
          axios.post(`${BASE_URL}/api/user/account/refresh_token/`, {}, { withCredentials: true })
            .then(res => {
              user.setAccessToken(res.data.access)
              onRefreshed(res.data.access)
            })
            .catch(err => {
              user.logout()
              onRefreshFailed(err)
              reject(err)
            })
            .finally(() => {
              isRefreshing = false
            })
        }
      })
    }

    return Promise.reject(error)
  }
)

export default api

8.1 拦截器流程

text 复制代码
发送请求
    ↓
请求拦截器附加 Authorization: Bearer accessToken
    ↓
后端校验 Token
    ↓
如果返回 401
    ↓
响应拦截器调用 refresh_token
    ↓
刷新成功:重发原请求
刷新失败:清空用户状态

8.2 多个请求同时 401 的处理

isRefreshing + refreshSubscribers 用于避免并发刷新多次 Token。

流程:

  1. 第一个 401 请求触发刷新;
  2. 其他 401 请求加入等待队列;
  3. 新 Token 返回后,统一重发队列中的请求。

九、登录页面

vue 复制代码
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user.js'
import api from '@/js/http/api.js'

const username = ref('')
const password = ref('')
const errorMessage = ref('')
const router = useRouter()
const user = useUserStore()

async function handleLogin() {
  errorMessage.value = ''

  if (!username.value.trim()) {
    errorMessage.value = '用户名不能为空'
    return
  }
  if (!password.value.trim()) {
    errorMessage.value = '密码不能为空'
    return
  }

  const res = await api.post('/api/user/account/login/', {
    username: username.value,
    password: password.value,
  })

  const data = res.data
  if (data.result === 'success') {
    user.setAccessToken(data.access)
    user.setUserInfo(data)
    await router.push({ name: 'homepage-index' })
  } else {
    errorMessage.value = data.result
  }
}
</script>

<template>
  <div class="flex justify-center mt-30">
    <form @submit.prevent="handleLogin" class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
      <label class="label">用户名</label>
      <input v-model="username" type="text" class="input" placeholder="用户名" />

      <label class="label">密码</label>
      <input v-model="password" type="password" class="input" placeholder="密码" />

      <p v-if="errorMessage" class="font-bold text-red-500 text-base">{{ errorMessage }}</p>

      <button class="btn btn-neutral mt-4">登录</button>
      <div class="flex justify-end">
        <RouterLink :to="{ name: 'register-index' }" class="btn btn-sm btn-ghost text-gray-500 mt-2">
          注册
        </RouterLink>
      </div>
    </form>
  </div>
</template>

十、注册页面

注册页比登录页多一个确认密码字段。

校验:

  • 用户名不能为空;
  • 密码不能为空;
  • 两次密码一致;
  • 密码长度建议不少于 6 位。

成功后与登录相同:保存 Token 和用户信息,然后跳转首页。


十一、导航栏登录状态

导航栏根据用户状态动态渲染:

vue 复制代码
<RouterLink
  v-if="user.hasPulledUserInfo && !user.isLogin()"
  :to="{ name: 'login-index' }"
  class="btn btn-ghost text-lg"
>
  登录
</RouterLink>

<UserMenu v-else-if="user.isLogin()" />

11.1 避免刷新闪烁

如果不判断 hasPulledUserInfo,页面刷新瞬间可能先显示"登录",随后又变成头像,视觉上会闪烁。


十二、用户菜单与退出登录

vue 复制代码
<script setup>
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user.js'
import api from '@/js/http/api.js'

const user = useUserStore()
const router = useRouter()

function closeMenu() {
  const element = document.activeElement
  if (element && element instanceof HTMLElement) element.blur()
}

async function handleLogout() {
  const res = await api.post('/api/user/account/logout/')
  if (res.data.result === 'success') {
    user.logout()
    await router.push({ name: 'homepage-index' })
  }
}
</script>

重点:

  • 点击退出时调用后端删除 Cookie;
  • 前端调用 user.logout() 清空状态;
  • 下拉菜单点击后通过 blur() 自动关闭。

十三、路由守卫

13.1 路由 meta

javascript 复制代码
{
  path: '/friend',
  component: FriendIndex,
  name: 'friend-index',
  meta: { needLogin: true },
}

13.2 beforeEach 守卫

javascript 复制代码
router.beforeEach((to, from, next) => {
  const user = useUserStore()

  if (!user.hasPulledUserInfo) {
    return next()
  }

  if (to.meta.needLogin && !user.isLogin()) {
    return next({ name: 'login-index', query: { redirect: to.fullPath } })
  }

  if (user.isLogin() && (to.name === 'login-index' || to.name === 'register-index')) {
    return next({ name: 'homepage-index' })
  }

  return next()
})

13.3 为什么先判断 hasPulledUserInfo?

页面刚刷新时,用户信息还没恢复,如果马上根据 isLogin() 判断,会把本来已登录的用户错误踢到登录页。


十四、App.vue 中恢复登录状态

vue 复制代码
<script setup>
import { onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user.js'
import api from '@/js/http/api.js'
import NavBar from '@/components/navbar/NavBar.vue'

const user = useUserStore()
const route = useRoute()
const router = useRouter()

onMounted(async () => {
  try {
    const res = await api.get('/api/user/account/get_user_info/')
    const data = res.data
    if (data.result === 'success') {
      user.setUserInfo(data)
    }
  } catch (error) {
    console.error(error)
  } finally {
    user.setHasPulledUserInfo(true)

    if (route.meta.needLogin && !user.isLogin()) {
      await router.replace({ name: 'login-index' })
    }
  }
})
</script>

<template>
  <NavBar>
    <RouterView />
  </NavBar>
</template>

14.1 页面刷新恢复流程

text 复制代码
刷新页面
    ↓
Pinia accessToken 丢失
    ↓
App.vue 请求 get_user_info
    ↓
请求无 accessToken,后端返回 401
    ↓
Axios 响应拦截器调用 refresh_token
    ↓
Cookie 中 refresh_token 有效
    ↓
获得新 accessToken
    ↓
重发 get_user_info
    ↓
恢复用户信息

十五、前端路由命名建议

建议统一命名规则,避免混乱:

text 复制代码
homepage-index
friend-index
create-index
login-index
register-index
user-space-index
user-profile-index
not-found

++如果项目中历史代码出现 home-index、user-account-login-index 等不同命名,复习时要注意以实际路由表为准。++


十六、常见问题与易错点

开发环境一般不是 HTTPS:

python 复制代码
secure=False

生产环境必须使用 HTTPS 时:

python 复制代码
secure=True

16.2 withCredentials 必须开启

如果前端要携带 Cookie:

javascript 复制代码
withCredentials: true

后端也要:

python 复制代码
CORS_ALLOW_CREDENTIALS = True

16.3 Authorization 格式

必须是:

text 复制代码
Authorization: Bearer <access_token>

16.4 refresh_token 拼写

接口路径要统一,例如:

text 复制代码
/api/user/account/refresh_token/

不要写成 refersh_token。

16.5 图片字段返回 URL

后端返回图片时要使用:

python 复制代码
user_profile.photo.url

不要直接返回 user_profile.photo。

16.6 捕获异常不要吞掉问题

开发阶段不建议写裸 except: 后完全不打印错误,否则难以调试。更好的方式是记录日志。


十七、小结

本节建立了完整认证闭环:

  1. 后端使用 Django User + UserProfile 管理账号和资料;
  2. 登录 / 注册后生成 JWT;
  3. Access Token 放前端内存,Refresh Token 放 HttpOnly Cookie;
  4. Axios 自动携带 Token,并在 401 时自动刷新;
  5. Pinia 保存用户信息;
  6. App.vue 刷新后自动恢复用户状态;
  7. Router 守卫保护需要登录的页面;
  8. 导航栏根据登录状态显示登录按钮或用户菜单。

十八、新手版:登录到底发生了什么

18.1 用生活例子理解 Token

可以把网站想象成一栋办公楼:

  • 用户名和密码:第一次进楼时出示身份证;
  • Access Token:临时门禁卡,有效期短;
  • Refresh Token:续卡凭证,放在更安全的地方;
  • 后端 API:不同办公室;
  • IsAuthenticated:门口保安,检查你有没有门禁卡。

登录成功后,前端每次访问需要权限的接口,都要带上门禁卡:

text 复制代码
Authorization: Bearer <access_token>

后端验证通过后,request.user 就会变成当前登录用户。

18.2 为什么刷新页面后还能保持登录

刷新页面会导致 JavaScript 内存中的 Pinia 状态丢失。

但是 Refresh Token 保存在 HttpOnly Cookie 中,浏览器会自动带给后端。

所以恢复登录的过程是:

text 复制代码
刷新页面
  ↓
前端没有 access token
  ↓
请求用户信息失败,返回 401
  ↓
Axios 自动请求 refresh_token
  ↓
后端从 Cookie 里拿到 refresh token
  ↓
生成新的 access token
  ↓
前端重新请求用户信息
  ↓
登录状态恢复

十九、跟做步骤:如何验证登录模块

19.1 验证注册

  1. 启动后端;
  2. 启动前端;
  3. 打开注册页面;
  4. 输入用户名和密码;
  5. 点击注册;
  6. 注册成功后进入首页或登录状态页面。

验证数据库中是否创建用户:

shell 复制代码
cd backend
python manage.py shell
python 复制代码
from django.contrib.auth.models import User
User.objects.all()

19.2 验证登录

打开浏览器开发者工具 Network,点击登录后观察:

text 复制代码
POST /api/user/account/login/

如果成功,响应中应包含:

json 复制代码
{
  "result": "success",
  "access": "...",
  "username": "..."
}

同时在 Application -> Cookies 中可以看到 refresh_token。

19.3 验证自动刷新 Token

可以把 Access Token 有效期调短,例如 1 分钟,然后:

  1. 登录;
  2. 等 Access Token 过期;
  3. 再请求需要登录的页面;
  4. 观察 Network 中是否先出现 401,再出现 refresh_token 请求,再重发原请求。

二十、登录模块常见报错排查

20.1 登录后仍然显示未登录

检查:

  1. 后端是否返回了 access;
  2. 前端是否调用 user.setAccessToken(data.access);
  3. 前端是否调用 user.setUserInfo(data);
  4. 导航栏是否使用了正确的 store;
  5. hasPulledUserInfo 是否被正确设置。

检查:

  1. 后端 set_cookie 是否执行;
  2. 前端 Axios 是否设置 withCredentials: true;
  3. 后端是否设置 CORS_ALLOW_CREDENTIALS = True;
  4. 浏览器是否拦截第三方 Cookie;
  5. 开发环境下 secure=True 会导致 HTTP 无法写入 Cookie。

20.3 一直 401

检查:

  1. 请求头是否有 Authorization;
  2. 格式是否是 Bearer token;
  3. Token 是否过期;
  4. 后端 JWT 配置是否正确;
  5. 访问的接口是否加了 IsAuthenticated。

20.4 注册时报用户名已存在

检查数据库:

python 复制代码
from django.contrib.auth.models import User
User.objects.filter(username='你的用户名').exists()

如果返回 True,说明这个用户名已经被注册。


二十一、本节复习问题

  1. 为什么项目要有 UserProfile,而不是只用 Django 自带 User?
  2. Access Token 和 Refresh Token 分别保存在什么地方?
  3. 为什么 Refresh Token 要放在 HttpOnly Cookie 中?
  4. Axios 请求拦截器做了什么?
  5. Axios 响应拦截器如何处理 401?
  6. IsAuthenticated 的作用是什么?
  7. 页面刷新后,前端如何恢复用户状态?
  8. 为什么需要 hasPulledUserInfo?

二十二、核心接口完整请求 / 响应示例

22.1 注册接口

http 复制代码
POST /api/user/account/register/
Content-Type: application/json

请求体:

json 复制代码
{
  "username": "alice",
  "password": "123456",
  "password_confirm": "123456"
}

成功响应:

json 复制代码
{
  "result": "success"
}

失败响应示例:

json 复制代码
{
  "result": "用户名已存在"
}

22.2 登录接口

http 复制代码
POST /api/user/account/login/
Content-Type: application/json

请求体:

json 复制代码
{
  "username": "alice",
  "password": "123456"
}

成功响应:

json 复制代码
{
  "result": "success",
  "access": "eyJhbGciOi...",
  "id": 1,
  "username": "alice",
  "photo": "/media/user/photo/default.png"
}

同时后端会通过响应头设置 Cookie:

http 复制代码
Set-Cookie: refresh_token=eyJhbGciOi...; HttpOnly; Path=/

22.3 获取当前用户信息接口

http 复制代码
GET /api/user/account/info/
Authorization: Bearer <access_token>

成功响应:

json 复制代码
{
  "result": "success",
  "id": 1,
  "username": "alice",
  "photo": "/media/user/photo/alice.png"
}

未登录响应:

json 复制代码
{
  "detail": "Authentication credentials were not provided."
}

22.4 刷新 Token 接口

http 复制代码
POST /api/user/account/token/refresh/
Cookie: refresh_token=<refresh_token>

成功响应:

json 复制代码
{
  "result": "success",
  "access": "新的access_token"
}

失败响应:

json 复制代码
{
  "result": "refresh token无效或已过期"
}

二十三、关键代码逐行解释示例

以后看到类似代码时,可以这样拆开理解:

python 复制代码
class InfoView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        user = request.user
        profile = user.userprofile
        return Response({
            "result": "success",
            "id": user.id,
            "username": user.username,
            "photo": profile.photo.url,
        })

逐行解释:

代码 新手解释
class InfoView(APIView): 定义一个 DRF 接口视图类
permission_classes = [IsAuthenticated] 这个接口必须登录后才能访问
def get(self, request): 当前端发送 GET 请求时执行这个函数
user = request.user DRF 根据 Token 找到当前用户
profile = user.userprofile 通过一对一关系找到用户扩展资料
return Response(...) 把用户信息以 JSON 格式返回给前端

二十四、Vue 登录状态数据流图

text 复制代码
LoginView.vue
  │ 用户输入账号密码
  ↓
loginApi.js
  │ POST /login/
  ↓
后端 LoginView
  │ 校验成功,返回 access,写入 refresh cookie
  ↓
Pinia user store
  │ 保存 access、id、username、photo
  ↓
NavBar.vue
  │ 根据 store 显示头像或登录按钮
  ↓
Axios 请求拦截器
  │ 后续请求自动带 Authorization

二十五、后端登录视图处理流程

text 复制代码
收到 POST /login/
  ↓
读取 username 和 password
  ↓
调用 authenticate 校验账号密码
  ↓
如果失败,返回用户名或密码错误
  ↓
如果成功,生成 refresh 和 access
  ↓
把 refresh 写入 HttpOnly Cookie
  ↓
把 access 和用户信息放入 JSON 返回

二十六、小练习与自测答案

练习 1

问题:如果前端请求用户信息接口时没有携带 Authorization 请求头,会发生什么?

答案:后端的 IsAuthenticated 会拦截请求,返回 401,前端应尝试使用 refresh token 刷新 access token。

练习 2

问题:为什么 Refresh Token 不建议直接放在 LocalStorage?

答案:LocalStorage 可以被 JavaScript 读取,如果页面出现 XSS 漏洞,攻击者可能偷走 Refresh Token。HttpOnly Cookie 不能被 JS 直接读取,相对更安全。

练习 3

问题:为什么需要 Axios 响应拦截器?

答案:因为 access token 会过期。响应拦截器可以统一处理 401,自动刷新 token 并重发原请求,避免每个接口都写重复逻辑。

相关推荐
梦幻精灵_cq20 分钟前
sumdir——一个伪python.built-in的“术法”打造
python
weixin_440730502 小时前
函数return、参数、参数组、递归函数、高阶函数等小结
开发语言·python
天赐范式2 小时前
天赐范式第181天:让视图开始切换——PBFT视图切换与从safety到liveness的跨越
python·pbft·数字生命·天赐范式·动态运行时·拜占庭容错
sugarzhangnotes2 小时前
【无标题】
开发语言·人工智能·python
weixin_447195293 小时前
【无标题】
pytorch·python
迅猛龙办公室3 小时前
Python实现简单的人名对话
python
阿坨3 小时前
firestart:一行命令启动你的日常应用和网页
python·pypi·cli·click
老歌老听老掉牙3 小时前
麻花钻切屑形态演变的力学机制与临界条件分析
python·算法·钻头
happylifetree4 小时前
Python18(补充):练习
python
weixin_416667964 小时前
银河麒麟V10看门狗试验-1
网络·chrome·python