登录模块与前后端认证体系
一、目标
在项目基础结构上实现完整的用户认证流程,包括:
- 用户注册;
- 用户登录;
- 用户退出;
- 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 注册接口
功能:
- 获取用户名、密码;
- 校验非空;
- 检查用户名是否已存在;
- 创建
User; - 创建
UserProfile; - 生成 JWT;
- 返回用户信息。
关键逻辑:
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 登录接口
功能:
- 获取用户名和密码;
- 使用
authenticate校验; - 查询
UserProfile; - 生成 Token;
- 返回用户信息。
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。
流程:
- 第一个 401 请求触发刷新;
- 其他 401 请求加入等待队列;
- 新 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 等不同命名,复习时要注意以实际路由表为准。++
十六、常见问题与易错点
16.1 Cookie 设置 secure
开发环境一般不是 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: 后完全不打印错误,否则难以调试。更好的方式是记录日志。
十七、小结
本节建立了完整认证闭环:
- 后端使用 Django
User+UserProfile管理账号和资料; - 登录 / 注册后生成 JWT;
- Access Token 放前端内存,Refresh Token 放 HttpOnly Cookie;
- Axios 自动携带 Token,并在 401 时自动刷新;
- Pinia 保存用户信息;
- App.vue 刷新后自动恢复用户状态;
- Router 守卫保护需要登录的页面;
- 导航栏根据登录状态显示登录按钮或用户菜单。
十八、新手版:登录到底发生了什么
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 验证注册
- 启动后端;
- 启动前端;
- 打开注册页面;
- 输入用户名和密码;
- 点击注册;
- 注册成功后进入首页或登录状态页面。
验证数据库中是否创建用户:
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 分钟,然后:
- 登录;
- 等 Access Token 过期;
- 再请求需要登录的页面;
- 观察 Network 中是否先出现 401,再出现 refresh_token 请求,再重发原请求。
二十、登录模块常见报错排查
20.1 登录后仍然显示未登录
检查:
- 后端是否返回了
access; - 前端是否调用
user.setAccessToken(data.access); - 前端是否调用
user.setUserInfo(data); - 导航栏是否使用了正确的 store;
hasPulledUserInfo是否被正确设置。
20.2 Cookie 没有保存
检查:
- 后端
set_cookie是否执行; - 前端 Axios 是否设置
withCredentials: true; - 后端是否设置
CORS_ALLOW_CREDENTIALS = True; - 浏览器是否拦截第三方 Cookie;
- 开发环境下
secure=True会导致 HTTP 无法写入 Cookie。
20.3 一直 401
检查:
- 请求头是否有
Authorization; - 格式是否是
Bearer token; - Token 是否过期;
- 后端 JWT 配置是否正确;
- 访问的接口是否加了
IsAuthenticated。
20.4 注册时报用户名已存在
检查数据库:
python
from django.contrib.auth.models import User
User.objects.filter(username='你的用户名').exists()
如果返回 True,说明这个用户名已经被注册。
二十一、本节复习问题
- 为什么项目要有
UserProfile,而不是只用 Django 自带User? Access Token和Refresh Token分别保存在什么地方?- 为什么 Refresh Token 要放在 HttpOnly Cookie 中?
- Axios 请求拦截器做了什么?
- Axios 响应拦截器如何处理 401?
IsAuthenticated的作用是什么?- 页面刷新后,前端如何恢复用户状态?
- 为什么需要
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 并重发原请求,避免每个接口都写重复逻辑。