可下载本文项目,gitee地址如下

https://gitee.com/longjing-with-sugar/vue3--django

git clone https://gitee.com/longjing-with-sugar/vue3--django.git

Django设置

1、安装 django-cors-headers库

pip3 install django-cors-headers -i https://pypi.mirrors.ustc.edu.cn/simple/

2、setting.py设置

DEBUG = True

ALLOWED_HOSTS = []  # 默认则允许所ip访问

# 1、配置白名单与安全设置
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",  # Vue开发服务器地址
    "http://127.0.0.1:5173",
]
# 2、允许携带Cookie
CORS_ALLOW_CREDENTIALS = True  

# 3、CSRF 可信源配置 - Django 4.x + 必需
CSRF_TRUSTED_ORIGINS = [
    "http://localhost:5173",
    "http://127.0.0.1:5173",
]
# 自定义Cookie名称,默认值给是 'csrftoken' 可选定义
# CSRF_COOKIE_NAME = 'my_project_csrftoken'

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'login.apps.LoginConfig',
    'corsheaders',    # 4、添加跨域 app
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # 5、跨域中间件,确保在CommonMiddleware之间,现在放在首行
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
...

注:本人后端文件夹路径如下,大家可能跟我的不一样,注意路径的配置即可

3. (同项目文件夹)backend/urls.py设置

此处创建了两个方法 获取 csrf 和 登陆前的注册

from django.contrib import admin
from django.urls import path, include
# from login import views as login_views

urlpatterns = [
    path('admin/', admin.site.urls),

    # 引入Login应用下的urls.py,匹配Login/开头的URL
    path('login/', include('login.urls')),

    # 不使用 include 则可以使用原 from login import views as login_views的方式
    # 并注释掉 app/login/urls.py 中的内容
    # path('login/csrf/', login_views.get_csrf_token),
    # path('login/register/', login_views.register),
]

app/login/urls.py

from django.urls import path
from .views import *

urlpatterns = [

    path('csrf/', get_csrf_token),

    path('register/', get_register),
]

4. app/login/views.py

说明:@ensure_csrf_cookie 是 Django 提供的一个视图装饰器,它的核心作用是:强制让 Django 在响应中设置 CSRF 令牌的 Cookie(即使视图本身没有用到 CSRF 验证)。Django 为了防御这种攻击,默认开启了CSRF 中间件,要求 POST、PUT、DELETE 等非 GET 请求携带CSRF 令牌(csrf_token) 才能通过验证。

PS: 当然你也可以使用@csrf_exempt,用于跳过CSRF保护的,如果您在视图函数上添加了 @csrf_exempt 那么之前的settting.py的设置都不需要了

import json

from backend import settings
from .models import User
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from django.middleware.csrf import get_token, _unmask_cipher_token

# @ensure_csrf_cookie 的作用是检查请求的 Cookie 中是否已有 csrftoken
# 如果没有,生成一个新的 CSRF 令牌,并通过响应头的 Set-Cookie写入客户端 Cookie
# 如果已有,直接复用这个已有的令牌,不会重新生成
@ensure_csrf_cookie
def get_csrf_token(request):
    print("获取CSRF令牌")
    csrfToken = get_token(request)
    print("csrfToken:", csrfToken)
    """获取CSRF令牌的API"""
    return JsonResponse({
        'success': True,
        'csrfToken': csrfToken
    })
# 只返回  return JsonResponse({ 'success': True }) 也行, @ensure_csrf_cookie 已帮助在响应头中
# 返回了CSRF令牌,所以这里可以不需要再返回CSRF令牌,返回只是方便接收打印测试

# @ensure_csrf_cookie
# def get_csrf_token(request):
#     # 1. 从Cookie获取原始秘钥(32位),在settings.py中设置了 CSRF_COOKIE_NAME = 'xxx'
#     cookie_secret = request.COOKIES.get(settings.CSRF_COOKIE_NAME)
#     # 2. 获取掩码后的令牌(64位)
#     masked_token = get_token(request)
#     # 3. 解掩码得到原始秘钥
#     unmasked_secret = _unmask_cipher_token(masked_token)
# 
#     # 打印对比(结果是True)
#     print(f"Cookie原始秘钥:{cookie_secret}")
#     print(f"解掩码后的秘钥:{unmasked_secret}")
#     print(f"是否一致:{cookie_secret == unmasked_secret}")
# 
#     return JsonResponse({
#         'success': True,
#         'my_project_csrftoken': masked_token
#     })

def get_register(request):
    if request.method == 'POST':
        print("注册post请求")
        print("-------", request.body)
        data = json.loads(request.body) if request.body else {}
        username = data.get('username')
        password = data.get('password')
        if username and password:
            user = User.objects.create(username=username, password=password)
            # 返回JSON格式数据,而非直接返回User对象
            res = json.dumps({'id': user.id, 'username': user.username})
            print("res:", res)
            return HttpResponse(res, content_type="application/json", status=201)
        else:
            # 返回JSON格式的错误提示
            res = json.dumps({"error": "Username and password are required"})
            print("res:", res)
            return HttpResponse(res, content_type="application/json", status=400)
    else:
        print("注册get请求")
        # 返回JSON格式的错误提示
        res = json.dumps({"error": "Invalid request method"})
        return HttpResponse(res, content_type="application/json", status=405)

Vue3 设置

1、vite.config.js 代理设置

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    },
  },
  //定义代理
  server: {
    proxy: {
      //(/api)表示获取路径中包含api的请求
      '/api': {
        //后台服务所在的源
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        // 把(/api)替换陈空字符串
        rewrite: (path) => path.replace(/^\/api/, '')
      },
    }
  }
  
})

2、axios 封装 utils/request.js修改

需要先安装: js-cookie,并在 request中 引入

npm install js-cookie --registry=https://registry.npmmirror.com

// axios基础的封装
import axios from 'axios'
import Cookies from 'js-cookie'  // 需要先安装: npm install js-cookie

const request = axios.create({
    baseURL: '/api',       //开发环境
    timeout: 50000,
    withCredentials: true,  // 允许携带cookie
})

// 获取CSRF令牌的函数
function getCSRFToken() {
    // 先从cookie中获取,'csrftoken'是默认值,如果django后端 settings.py 设置了自定义Cookie名称,
    // 如 CSRF_COOKIE_NAME = 'my_project_csrftoken',那么Cookies.get('xxx') 中的xxx也要跟着改
    let token = Cookies.get('my_project_csrftoken')
    // 如果没有,尝试从DOM获取
    if (!token && document.querySelector('[name=csrfmiddlewaretoken]')) {
        token = document.querySelector('[name=csrfmiddlewaretoken]').value
    }
    return token
}

// axios请求拦截器
request.interceptors.request.use(
    config => {
        // 对于非GET请求,添加CSRF令牌
        if (config.method !== 'get' && config.method !== 'GET') {
            const csrfToken = getCSRFToken()
            if (csrfToken) {
                config.headers['X-CSRFToken'] = csrfToken
            }
        }
        // 设置内容类型
        if (!config.headers['Content-Type']) {
            config.headers['Content-Type'] = 'application/json'
        }
        return config
    },
    e => Promise.reject(e)
)

// axios响应式拦截器
request.interceptors.response.use(res => res.data,
    e => {
        // 处理CSRF错误
        if (e.response && e.response.status === 403) {
            const data = e.response.data
            if (data.detail && data.detail.includes('CSRF')) {
                // CSRF验证失败,重新获取令牌
                console.error('CSRF验证失败,请刷新页面重试')
                // 可以在这里跳转到登录页面或刷新页面
                window.location.reload()
            }
        }
        return Promise.reject(e)
    }
)

export default request

3. main.js 加载时获取CSRF令牌

import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'
import request from '@/utils/request';

const app = createApp(App)

// 应用启动时获取CSRF令牌
const initCSRFToken = async () => {
  try {
    await request.get('login/csrf/')
  } catch (error) {
    console.log('初始化CSRF令牌失败,但应用继续运行')
  }
}
// 执行初始化
initCSRFToken()


app.use(createPinia())
app.use(router)

app.mount('#app')

4. 调用 举例 login.js

import request from '@/utils/request'

export const register = (queryParams) => {
    return request({
        url: '/login/register/',
        method: 'post',
        data: queryParams
    })
}

5. App.vue 调用注册 API

<template>
    ...
</template>
<script setup>
import { register } from '@/api/login';
import { ref } from 'vue'
const username = ref('')
const password = ref('')

const login = async () => {
  try {
    const response = await register({
      username: username.value,
      password: password.value
    })
    console.log(JSON.stringify(response));
    alert(JSON.stringify(response))
  } catch (error) {
    console.log(error.response || error);
  }
}

</script>

效果图

对比浏览器 cookie 中的 csrftoken 值和 django 后台打印

可以看到名称一样,但值不一样:

为什么看起来 “不一样”?

  • get_token(request)会调用_mask_cipher_secret(csrf_secret),把原始秘钥(Cookie 里的csrftoken)和一个随机生成的 32 位掩码结合,生成 64 位的掩码令牌。
  • 验证时,Django 会通过_unmask_cipher_token把掩码令牌还原为原始秘钥,再和 Cookie 里的csrftoken对比(这一步是自动的)。

验证两者的一致性

@ensure_csrf_cookie
def get_csrf_token(request):
    # 1. 获取掩码后的令牌
    masked_token = get_token(request)
    # 2. 解掩码(验证一致性)
    unmasked_secret = _unmask_cipher_token(masked_token)
    # 3. 验证是否一致
    print(f" 获取掩码后的令牌:{masked_token}")
    print(f"解掩码后的秘钥:{unmasked_secret}")
    return JsonResponse({
        'success': True,
        'csrfToken': masked_token,
    })

可看到解掩码后的秘钥与浏览器的cookie csrftoken值一致:

情况2、

settings.py 设置

CSRF_COOKIE_NAME = 'my_project_csrftoken'

修改app/login/views.py

@ensure_csrf_cookie
def get_csrf_token(request):
    # 1. 从Cookie获取原始秘钥(32位),在settings.py中设置了 CSRF_COOKIE_NAME = 'xxx'
    cookie_secret = request.COOKIES.get(settings.CSRF_COOKIE_NAME)
    # 2. 获取掩码后的令牌(64位)
    masked_token = get_token(request)
    # 3. 解掩码得到原始秘钥
    unmasked_secret = _unmask_cipher_token(masked_token)

    # 打印对比(结果是True)
    print(f"Cookie原始秘钥:{cookie_secret}")
    print(f"解掩码后的秘钥:{unmasked_secret}")
    print(f"是否一致:{cookie_secret == unmasked_secret}")

    return JsonResponse({
        'success': True,
        'my_project_csrftoken': masked_token
    })

以及 vue中request.js Cookies.get('csrftoken') => Cookies.get('my_project_csrftoken')

function getCSRFToken() {
    ...
    let token = Cookies.get('my_project_csrftoken')
    ...
}

效果图

对比浏览器 cookie 中的 csrftoken 值和 django 后台打印

关于设置自定义的 settings.py 属性 CSRF_COOKIE_NAME = 'xxx' 的作用?

举个例子:当你启动了多个后端,不同的后端的 CSRF_COOKIE_NAME 属性是相同设置,单个网页就可以方便的访问多个后端了。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐