在做前后端分离项目时,第一个绕不开的问题就是:用户登录后,服务器怎么知道后续请求是来自这个用户的?
传统的做法是用 Session,但前后端分离后,Session 就显得不太合适了。于是我们选择了 JWT(JSON Web Token)作为鉴权方案。
一、什么是 JWT?
1 JWT 的基本概念
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。它由三部分组成,用点号(.)分隔:
Header.Payload.Signature
一个典型的 JWT 长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InRlc3R1c2VyIiwiZXhwIjoxNzAwMDAwMDAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
2 JWT 的三部分详解
Header(头部)
Header 通常包含两个信息:算法类型和令牌类型。
{
"alg": "HS256",
"typ": "JWT"
}
-
alg:签名算法,常用 HS256、RS256 等 -
typ:令牌类型,固定为 "JWT"
这部分经过 Base64Url 编码后形成第一段。
Payload(负载)
Payload 是实际要传递的数据,也叫声明(Claims)。JWT 有三种类型的声明:
| 类型 | 说明 | 示例 |
|---|---|---|
| 标准声明 | JWT 标准预定义的声明 | iss(签发者)、exp(过期时间)、sub(主题) |
| 公共声明 | 可以自由使用,但要避免冲突 | name、email |
| 私有声明 | 双方约定的私有信息 | user_id、role |
一个典型的 Payload:
{
"user_id": 1,
"username": "testuser",
"exp": 1700000000,
"iat": 1699913600
}
注意:Payload 只是 Base64Url 编码,不是加密,所以不要把敏感信息(如密码)放在这里。
Signature(签名)
Signature 是对 Header 和 Payload 的签名,用于验证 JWT 是否被篡改。
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
-
前两部分拼接后,用密钥(secret)进行签名
-
服务器验证时,用同样的密钥重新计算签名,对比是否一致
-
如果 JWT 被篡改,签名就会对不上
二、为什么选择 JWT 而不是 Session?
1 传统 Session 认证的问题
在传统的 Web 应用中,用户登录后,服务器会在内存或数据库中创建一个 Session,然后把 Session ID 通过 Cookie 返回给客户端。客户端后续请求会自动带上这个 Cookie,服务器通过 Session ID 找到对应的 Session 数据。
这种方式在单体应用中没问题,但在前后端分离架构下会遇到几个问题:
| 问题 | Session 方案 | JWT 方案 |
|---|---|---|
| 跨域问题 | Cookie 无法跨域,需要额外配置 CORS | 通过 Authorization Header 传递,天然支持跨域 |
| 服务器压力 | 每次请求都要查询 Session,服务器压力大 | 无状态,服务器不需要存储 Session |
| 水平扩展 | 多台服务器需要共享 Session(Redis) | 每台服务器都能独立验证 Token |
| 移动端支持 | 移动端 Cookie 管理复杂 | Token 存储灵活,LocalStorage、内存都行 |
2 JWT 的优势
-
无状态:服务器不需要存储 Token,减轻服务器压力
-
跨域友好:通过 HTTP Header 传递,天然支持跨域
-
移动端友好:移动端存储 Token 比管理 Cookie 简单
-
信息丰富:Token 本身包含用户信息,减少数据库查询
3 JWT 的劣势
当然,JWT 也不是完美的,也有一些需要注意的地方:
-
无法主动失效:Token 一旦签发,在过期前无法主动撤销(除非用黑名单)
-
Token 过大:如果 Payload 里信息太多,Token 会很长
-
安全风险:如果密钥泄露,攻击者可以伪造任意 Token
三、Django 后端实现
1 安装依赖
首先安装 djangorestframework-simplejwt:
pip install djangorestframework-simplejwt
2 配置 Django Settings
在 settings.py 中添加配置:
# 注册应用
INSTALLED_APPS = [
# ... 其他应用
'rest_framework_simplejwt',
]
# DRF 配置鉴权方式
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
# JWT 配置
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': datetime.timedelta(days=15), # 访问令牌有效期
'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=15), # 刷新令牌有效期
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
}
配置说明:
-
ACCESS_TOKEN_LIFETIME:Access Token 的有效期,我设置的是 15 天 -
REFRESH_TOKEN_LIFETIME:Refresh Token 的有效期,用于刷新 Access Token -
USER_ID_FIELD:用户模型的 ID 字段 -
USER_ID_CLAIM:Token 中存储用户 ID 的字段名
3 实现登录接口
在 user/views.py 中实现登录视图:
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from django.contrib.auth.hashers import check_password
from django.http import JsonResponse
import json
class LoginView(APIView):
permission_classes = [AllowAny] # 允许任何人登录
def post(self, request):
try:
data = json.loads(request.body)
username = data.get('username')
password = data.get('password')
# 查询用户
user = SysUser.objects.get(username=username)
# 验证密码
if not check_password(password, user.password):
return JsonResponse({'code': 500, 'info': '用户名或者密码错误!'})
# 使用 simplejwt 生成 Token
refresh = RefreshToken.for_user(user)
token = str(refresh.access_token)
except SysUser.DoesNotExist:
return JsonResponse({'code': 500, 'info': '用户名或者密码错误!'})
except Exception as e:
print(e)
return JsonResponse({'code': 500, 'info': '用户名或者密码错误!'})
# 返回 Token
return JsonResponse({
'code': 200,
'token': token,
'info': '登录成功!'
})
代码解析:
-
permission_classes = [AllowAny]:登录接口不需要认证,任何人都可以访问 -
check_password(password, user.password):Django 提供的密码验证函数,会自动处理密码哈希 -
RefreshToken.for_user(user):为用户生成 Refresh Token -
str(refresh.access_token):从 Refresh Token 中提取 Access Token -
返回的 Token 格式:
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
4 保护需要认证的接口
对于需要登录才能访问的接口,使用 IsAuthenticated 权限:
from rest_framework.permissions import IsAuthenticated
class UserInfoView(APIView):
permission_classes = [IsAuthenticated] # 需要认证
def get(self, request):
user = request.user # JWT 认证后,request.user 就是当前用户
return JsonResponse({
'code': 200,
'data': {
'username': user.username,
'email': user.email,
}
})
关键点:
-
permission_classes = [IsAuthenticated]:只有携带有效 Token 的请求才能访问 -
request.user:JWT 认证中间件会自动解析 Token,把用户信息注入到 request.user
四、Vue3 前端实现
1 Axios 请求拦截器
在 src/unit/request.ts 中配置 Axios 拦截器,自动添加 Token:
import axios, { type AxiosError, type AxiosResponse } from 'axios'
import router from "@/router";
import { ElMessage } from "element-plus";
const httpServer = axios.create({
baseURL: 'http://localhost:8000/',
timeout: 300000
})
// 请求拦截器:自动添加 Token
httpServer.interceptors.request.use(
(config) => {
const token = window.localStorage.getItem('token');
if (token) {
if (!config.headers) {
config.headers = config.headers || {}
}
// 去除 token 中可能存在的空白字符
const cleanToken = token.replace(/\s+/g, '');
// 添加 Authorization Header
config.headers['Authorization'] = `Bearer ${cleanToken}`;
}
return config;
},
(error) => Promise.reject(error)
);
代码解析:
-
从
localStorage获取 Token -
如果 Token 存在,添加到请求头的
Authorization字段 -
格式必须是
Bearer <token>,注意Bearer后面有个空格 -
replace(/\s+/g, ''):去除 Token 中的空白字符,防止格式错误
2 响应拦截器:处理 Token 过期
// 响应拦截器:处理错误
httpServer.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error: AxiosError) => {
if (error.response) {
const status = error.response.status;
switch (status) {
case 401:
// Token 过期或无效
localStorage.removeItem('token') // 先删除无效 Token
router.push('/login') // 跳转到登录页
window.location.reload() // 刷新页面
ElMessage.error('登录已过期,请重新登录')
break;
case 403:
// 权限不足
ElMessage.error('权限不足')
break;
case 500:
// 服务器错误
ElMessage.error('服务器错误')
break;
}
}
return Promise.reject(error);
}
);
关键点:
-
401状态码表示 Token 无效或过期 -
收到 401 时,先删除本地 Token,然后跳转登录页
-
window.location.reload():刷新页面,清除所有状态
3 登录流程实现
在登录页面中,调用登录接口并保存 Token:
const handleLogin = async () => {
try {
const res = await post('/api/user/login', {
username: loginForm.username,
password: loginForm.password,
})
if (res.code === 200) {
// 保存 Token 到 localStorage
localStorage.setItem('token', res.token)
ElMessage.success('登录成功')
// 跳转到首页
router.push('/')
} else {
ElMessage.error(res.info || '登录失败')
}
} catch (error) {
ElMessage.error('登录失败,请检查网络')
}
}
4 退出登录
const handleLogout = () => {
// 删除 Token
localStorage.removeItem('token')
// 跳转到登录页
router.push('/login')
ElMessage.success('已退出登录')
}
五、完整的认证流程
1 流程图
2 认证流程详解
①登录阶段:
-
用户在前端输入用户名和密码
-
前端调用登录接口,发送用户名密码
-
后端验证用户名密码,生成 JWT Token
-
前端接收 Token,存储到 localStorage
②请求阶段:
-
前端发起 API 请求
-
Axios 请求拦截器自动从 localStorage 获取 Token
-
在请求头中添加
Authorization: Bearer {token} -
后端接收请求,验证 Token 签名
-
验证通过后,从 Token 中解析用户信息
-
后端返回数据
③Token 过期处理:
-
如果 Token 过期,后端返回 401
-
前端响应拦截器捕获 401
-
删除本地 Token,跳转登录页
六总结
JWT 是一种非常适合前后端分离架构的鉴权方案,它具有无状态、跨域友好、易于扩展等优点。
核心要点:
-
JWT 由 Header、Payload、Signature 三部分组成
-
使用
Bearer {token}格式在请求头中传递 Token -
Token 只是 Base64 编码,不要放敏感信息
-
生产环境必须使用 HTTPS 和强密钥
-
实现 Token 刷新机制提升用户体验