JWT鉴权的实现:从原理到 Django + Vue3

在做前后端分离项目时,第一个绕不开的问题就是:用户登录后,服务器怎么知道后续请求是来自这个用户的?

传统的做法是用 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(主题)
公共声明 可以自由使用,但要避免冲突 nameemail
私有声明 双方约定的私有信息 user_idrole

一个典型的 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 的优势

  1. 无状态:服务器不需要存储 Token,减轻服务器压力

  2. 跨域友好:通过 HTTP Header 传递,天然支持跨域

  3. 移动端友好:移动端存储 Token 比管理 Cookie 简单

  4. 信息丰富:Token 本身包含用户信息,减少数据库查询

3 JWT 的劣势

当然,JWT 也不是完美的,也有一些需要注意的地方:

  1. 无法主动失效:Token 一旦签发,在过期前无法主动撤销(除非用黑名单)

  2. Token 过大:如果 Payload 里信息太多,Token 会很长

  3. 安全风险:如果密钥泄露,攻击者可以伪造任意 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': '登录成功!'
        })

代码解析

  1. permission_classes = [AllowAny]:登录接口不需要认证,任何人都可以访问

  2. check_password(password, user.password):Django 提供的密码验证函数,会自动处理密码哈希

  3. RefreshToken.for_user(user):为用户生成 Refresh Token

  4. str(refresh.access_token):从 Refresh Token 中提取 Access Token

  5. 返回的 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)
);

代码解析

  1. localStorage 获取 Token

  2. 如果 Token 存在,添加到请求头的 Authorization 字段

  3. 格式必须是 Bearer <token>,注意 Bearer 后面有个空格

  4. 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 刷新机制提升用户体验

相关推荐
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于Springboot的智慧养老系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Funny_AI_LAB2 小时前
Zcode:智谱AI推出的轻量级 AI IDE 编程利器
人工智能·python·算法·编辑器
2501_944452232 小时前
活动记录 Cordova 与 OpenHarmony 混合开发实战
python
子夜江寒2 小时前
基于 Python 使用 SVM、K-means与DBSCAN
python·支持向量机·kmeans
最贪吃的虎2 小时前
什么是开源?小白如何快速学会开源协作流程并参与项目
java·前端·后端·开源
Blossom.1182 小时前
GPTQ量化实战:从零手写大模型权重量化与反量化引擎
人工智能·python·算法·chatgpt·ai作画·自动化·transformer
Elaine3363 小时前
实战教学:使用 Scrapy 爬取 CSDN 文章与用户头像
python·scrapy·网络爬虫
Thomas游戏开发3 小时前
Unity3D IL2CPP如何调用Burst
前端·后端·架构
货拉拉技术3 小时前
货拉拉离线大数据迁移-验数篇
后端·架构