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 的作用

UserProfileUser 是一对一关系:

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-indexuser-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 TokenRefresh 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 并重发原请求,避免每个接口都写重复逻辑。

相关推荐
C+-C资深大佬1 小时前
Java 变量:从入门到精通
java·开发语言·python
chouchuang1 小时前
day-027-面向对象-下
开发语言·python
华研前沿标杆游学1 小时前
2026年标杆游学机构选择指南:从资质审核到效果验收全流程本指南
python
衔烛之龙12 小时前
Windows x64 构建 liboqs-java教程
java·windows·python
VIP_CQCRE3 小时前
用 Ace Data Cloud 快速接入 AI 视频生成:HappyHorse Videos API 实战指南
人工智能·python·api·ai视频生成·acedatacloud
阿豪只会阿巴4 小时前
两小时快速入门 FastAPI--第二回
windows·python·fastapi
idingzhi4 小时前
A股量化策略日报(2026年07月12日)
python
二宝哥4 小时前
07.Python运算符详解与编码基础
python
巴啦啦小魔仙变身4 小时前
SIGINT = Signal Interrupte
linux·服务器·python