登录模块与前后端认证体系 一、目标 在项目基础结构上实现完整的用户认证流程,包括:
用户注册;
用户登录;
用户退出;
Access Token 与 Refresh Token 双 Token 机制;
刷新页面后的登录状态恢复;
Axios 自动携带 Token;
401 自动刷新 Token;
Vue Router 路由守卫;
导航栏根据登录状态动态显示。
登录模块是后续创建角色、编辑资料、聊天、语音等所有需要用户身份的功能基础。
二、认证模块总体架构 2.1 数据流概览 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 用户输入账号密码 ↓ 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import uuidfrom django.contrib.auth.models import Userfrom django.db import modelsfrom django.utils.timezone import now, localtimedef 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 是一对一关系:
1 User 1 ───── 1 UserProfile
这样既能复用 Django 自带认证系统,又能扩展项目业务字段。
3.4 注册到 Admin 1 2 3 4 5 6 7 from django.contrib import adminfrom web.models.user import UserProfile@admin.register(UserProfile ) class UserProfileAdmin (admin.ModelAdmin): raw_id_fields = ('user' ,)
raw_id_fields 的作用:
外键或一对一字段显示为 ID 输入框;
避免用户数量大时后台下拉框加载过慢;
提升 Django Admin 管理性能。
四、数据库迁移 修改模型后执行:
1 2 3 cd backend python manage.py makemigrations python manage.py migrate
4.1 makemigrations 生成迁移文件,不直接修改数据库。
4.2 migrate 将迁移文件真正应用到数据库,创建或修改表结构。
4.3 标准流程 1 2 3 4 5 6 7 8 9 修改 models.py ↓ python manage.py makemigrations ↓ 检查 migrations 文件 ↓ python manage.py migrate ↓ 数据库结构更新完成
五、后端账号 API 5.1 注册接口 功能:
获取用户名、密码;
校验非空;
检查用户名是否已存在;
创建 User;
创建 UserProfile;
生成 JWT;
返回用户信息。
关键逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from django.contrib.auth.models import Userfrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework_simplejwt.tokens import RefreshTokenfrom web.models.user import UserProfileclass 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 用户名重复检查易错点 错误写法:
1 User.objects.get(username=username).exists()
原因:
get() 返回一个对象,或者抛异常;
对象没有 .exists() 方法。
正确写法:
1 User.objects.filter (username=username).exists()
5.3 登录接口 功能:
获取用户名和密码;
使用 authenticate 校验;
查询 UserProfile;
生成 Token;
返回用户信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from django.contrib.auth import authenticatefrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework_simplejwt.tokens import RefreshTokenfrom web.models.user import UserProfileclass 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 退出登录接口 1 2 3 4 5 6 7 8 9 10 11 12 from rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework.permissions import IsAuthenticatedclass LogoutView (APIView ): permission_classes = [IsAuthenticated] def post (self, request ): response = Response({'result' : 'success' }) response.delete_cookie('refresh_token' ) return response
5.5 刷新 Token 接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from django.conf import settingsfrom rest_framework import statusfrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework_simplejwt.tokens import RefreshTokenclass 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 获取用户信息接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework.permissions import IsAuthenticatedfrom web.models.user import UserProfileclass 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, })
六、后端路由配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from django.urls import path, re_pathfrom web.views.index import indexfrom web.views.user.account.login import LoginViewfrom web.views.user.account.logout import LogoutViewfrom web.views.user.account.register import RegisterViewfrom web.views.user.account.refresh_token import RefreshTokenViewfrom web.views.user.account.get_user_info import GetUserInfoViewurlpatterns = [ 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 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 拦截器流程 1 2 3 4 5 6 7 8 9 10 11 12 发送请求 ↓ 请求拦截器附加 Authorization: Bearer accessToken ↓ 后端校验 Token ↓ 如果返回 401 ↓ 响应拦截器调用 refresh_token ↓ 刷新成功:重发原请求 刷新失败:清空用户状态
8.2 多个请求同时 401 的处理 isRefreshing + refreshSubscribers 用于避免并发刷新多次 Token。
流程:
第一个 401 请求触发刷新;
其他 401 请求加入等待队列;
新 Token 返回后,统一重发队列中的请求。
九、登录页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <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 和用户信息,然后跳转首页。
十一、导航栏登录状态 导航栏根据用户状态动态渲染:
1 2 3 4 5 6 7 8 9 <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,页面刷新瞬间可能先显示“登录”,随后又变成头像,视觉上会闪烁。
十二、用户菜单与退出登录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <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() 自动关闭。
十三、路由守卫 1 2 3 4 5 6 { path : '/friend' , component : FriendIndex , name : 'friend-index' , meta : { needLogin : true }, }
13.2 beforeEach 守卫 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 中恢复登录状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <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 页面刷新恢复流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 刷新页面 ↓ Pinia accessToken 丢失 ↓ App.vue 请求 get_user_info ↓ 请求无 accessToken,后端返回 401 ↓ Axios 响应拦截器调用 refresh_token ↓ Cookie 中 refresh_token 有效 ↓ 获得新 accessToken ↓ 重发 get_user_info ↓ 恢复用户信息
十五、前端路由命名建议 建议统一命名规则,避免混乱:
1 2 3 4 5 6 7 8 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:
生产环境必须使用 HTTPS 时:
16.2 withCredentials 必须开启 如果前端要携带 Cookie:
后端也要:
1 CORS_ALLOW_CREDENTIALS = True
16.3 Authorization 格式 必须是:
1 Authorization: Bearer <access_token>
16.4 refresh_token 拼写 接口路径要统一,例如:
1 /api/user/account/refresh_token/
不要写成 refersh_token。
16.5 图片字段返回 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:门口保安,检查你有没有门禁卡。
登录成功后,前端每次访问需要权限的接口,都要带上门禁卡:
1 Authorization: Bearer <access_token>
后端验证通过后,request.user 就会变成当前登录用户。
18.2 为什么刷新页面后还能保持登录 刷新页面会导致 JavaScript 内存中的 Pinia 状态丢失。
但是 Refresh Token 保存在 HttpOnly Cookie 中,浏览器会自动带给后端。
所以恢复登录的过程是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 刷新页面 ↓ 前端没有 access token ↓ 请求用户信息失败,返回 401 ↓ Axios 自动请求 refresh_token ↓ 后端从 Cookie 里拿到 refresh token ↓ 生成新的 access token ↓ 前端重新请求用户信息 ↓ 登录状态恢复
十九、跟做步骤:如何验证登录模块 19.1 验证注册
启动后端;
启动前端;
打开注册页面;
输入用户名和密码;
点击注册;
注册成功后进入首页或登录状态页面。
验证数据库中是否创建用户:
1 2 cd backend python manage.py shell
1 2 from django.contrib.auth.models import UserUser.objects.all ()
19.2 验证登录 打开浏览器开发者工具 Network,点击登录后观察:
1 POST /api/user/account/login/
如果成功,响应中应包含:
1 2 3 4 5 { "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 注册时报用户名已存在 检查数据库:
1 2 from django.contrib.auth.models import UserUser.objects.filter (username='你的用户名' ).exists()
如果返回 True,说明这个用户名已经被注册。
二十一、本节复习问题
为什么项目要有 UserProfile,而不是只用 Django 自带 User?
Access Token 和 Refresh Token 分别保存在什么地方?
为什么 Refresh Token 要放在 HttpOnly Cookie 中?
Axios 请求拦截器做了什么?
Axios 响应拦截器如何处理 401?
IsAuthenticated 的作用是什么?
页面刷新后,前端如何恢复用户状态?
为什么需要 hasPulledUserInfo?
二十二、核心接口完整请求 / 响应示例 22.1 注册接口 1 2 POST /api/user/account/register/ Content-Type : application/json
请求体:
1 2 3 4 5 { "username" : "alice" , "password" : "123456" , "password_confirm" : "123456" }
成功响应:
1 2 3 { "result" : "success" }
失败响应示例:
22.2 登录接口 1 2 POST /api/user/account/login/ Content-Type : application/json
请求体:
1 2 3 4 { "username" : "alice" , "password" : "123456" }
成功响应:
1 2 3 4 5 6 7 { "result" : "success" , "access" : "eyJhbGciOi..." , "id" : 1 , "username" : "alice" , "photo" : "/media/user/photo/default.png" }
同时后端会通过响应头设置 Cookie:
1 Set-Cookie : refresh_token=eyJhbGciOi...; HttpOnly; Path=/
22.3 获取当前用户信息接口 1 2 GET /api/user/account/info/ Authorization : Bearer <access_token>
成功响应:
1 2 3 4 5 6 { "result" : "success" , "id" : 1 , "username" : "alice" , "photo" : "/media/user/photo/alice.png" }
未登录响应:
1 2 3 { "detail" : "Authentication credentials were not provided." }
22.4 刷新 Token 接口 1 2 POST /api/user/account/token/refresh/ Cookie : refresh_token=<refresh_token>
成功响应:
1 2 3 4 { "result" : "success" , "access" : "新的access_token" }
失败响应:
1 2 3 { "result" : "refresh token无效或已过期" }
二十三、关键代码逐行解释示例 以后看到类似代码时,可以这样拆开理解:
1 2 3 4 5 6 7 8 9 10 11 12 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 登录状态数据流图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 LoginView.vue │ 用户输入账号密码 ↓ loginApi.js │ POST /login/ ↓ 后端 LoginView │ 校验成功,返回 access,写入 refresh cookie ↓ Pinia user store │ 保存 access、id、username、photo ↓ NavBar.vue │ 根据 store 显示头像或登录按钮 ↓ Axios 请求拦截器 │ 后续请求自动带 Authorization
二十五、后端登录视图处理流程 1 2 3 4 5 6 7 8 9 10 11 12 13 收到 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 并重发原请求,避免每个接口都写重复逻辑。