【Python】实战记录:从零搭建 Django + Vue 全栈应用 —— 用户认证篇

引言

在现代 Web 开发中,前后端分离已成为主流架构。后端负责数据处理和业务逻辑(如 Django),前端则专注于用户界面和交互(如 Vue)。本文将带你一步步构建一个基础的全栈应用,实现用户注册和登录功能,并详细记录过程中可能遇到的典型问题及其解决方案。

我们将使用:

一、后端搭建 (Django)

1. 环境准备

首先,创建并激活 Python 虚拟环境(推荐使用 venvconda),然后安装所需包:

bash 复制代码
pip install django djangorestframework djangorestframework-simplejwt python-decouple
# python-decouple 用于管理环境变量(可选但推荐)

接着,创建 Django 项目和应用:

bash 复制代码
django-admin startproject myproject
cd myproject
python manage.py startapp accounts_api

2. Django 配置 (myproject/settings.py)

这部分是整个后端的核心配置。我们需要集成 DRF、SimpleJWT、以及处理跨域问题的 django-cors-headers

python 复制代码
import os
from pathlib import Path
from datetime import timedelta # For JWT settings

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = 'your-secret-key-here' # Use a strong secret key in production
DEBUG = True # Set to False in production
ALLOWED_HOSTS = ['*'] # Configure properly for production

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',  # 确保在这里
    'rest_framework', # Add DRF
    'rest_framework_simplejwt', # Add SimpleJWT for token handling
    'accounts_api', # Add your app
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 必须放在最前面
    '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',
]

CORS_ALLOWED_ORIGINS = [
    "http://localhost:5173",  # Vue 开发服务器地址
]

ROOT_URLCONF = 'myproject.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'myproject.wsgi.application'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3', # Using SQLite for simplicity
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True

STATIC_URL = '/static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny', # Adjust as needed
    ],
}

# JWT Settings
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
}

重要提醒: corsheaders.middleware.CorsMiddleware 必须 放在 MIDDLEWARE 列表的最前面,否则它可能无法拦截到某些请求。

3. 序列化器 (accounts_api/serializers.py)

序列化器用于定义数据的输入和输出格式。

python 复制代码
from rest_framework import serializers
from django.contrib.auth.models import User
from django.contrib.auth import authenticate

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ('username', 'password')

    def create(self, validated_data):
        # 使用 create_user 方法,它会自动处理密码哈希
        user = User.objects.create_user(
            username=validated_data['username'],
            password=validated_data['password']
        )
        return user

class LoginSerializer(serializers.Serializer):
    username = serializers.CharField()
    password = serializers.CharField()

    def validate(self, data):
        username = data.get('username')
        password = data.get('password')

        if username and password:
            user = authenticate(username=username, password=password)
            if user:
                if not user.is_active:
                    raise serializers.ValidationError("用户账户已被禁用。")
                data['user'] = user
            else:
                raise serializers.ValidationError("无法使用提供的凭据登录。")
        else:
            raise serializers.ValidationError("必须包含 'username' 和 'password'。")

        return data

4. 视图 (accounts_api/views.py)

视图处理具体的业务逻辑。

python 复制代码
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework import status
from .serializers import RegisterSerializer, LoginSerializer
from rest_framework_simplejwt.tokens import RefreshToken

@api_view(['POST'])
@permission_classes([AllowAny])
def register_view(request):
    serializer = RegisterSerializer(data=request.data)
    if serializer.is_valid():
        user = serializer.save()
        refresh = RefreshToken.for_user(user)
        return Response({
            'message': '用户注册成功!',
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

@api_view(['POST'])
@permission_classes([AllowAny])
def login_view(request):
    serializer = LoginSerializer(data=request.data)
    if serializer.is_valid():
        user = serializer.validated_data["user"]
        refresh = RefreshToken.for_user(user)

        return Response({
            'message': '登录成功!',
            'refresh': str(refresh),
            'access': str(refresh.access_token),
        }, status=status.HTTP_200_OK)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

5. URLs (accounts_api/urls.py & myproject/urls.py)

python 复制代码
# accounts_api/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('register/', views.register_view, name='register'),
    path('login/', views.login_view, name='login'),
]
python 复制代码
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/accounts/', include('accounts_api.urls')),
]

6. 数据库迁移与启动

bash 复制代码
python manage.py makemigrations
python manage.py migrate
python manage.py runserver

现在,Django 服务应在 http://127.0.0.1:8000/ 上运行,并提供 http://127.0.0.1:8000/api/accounts/register/http://127.0.0.1:8000/api/accounts/login/ 两个 API 端点。

二、前端搭建 (Vue 3)

1. 环境准备(以管理员身份运行)

bash 复制代码
npm create vue@latest my-vue-app
cd my-vue-app
npm install axios
npm install

2. 主要组件 (src/App.vue)

这里整合了登录、注册和状态管理的逻辑。

vue 复制代码
<template>
  <div id="app">
    <h1>My App</h1>

    <div v-if="!isLoggedIn">
      <h2>Register</h2>
      <form @submit.prevent="handleRegister">
        <div>
          <label for="reg_username">Username:</label>
          <input type="text" id="reg_username" v-model="registerForm.username" required />
        </div>
        <div>
          <label for="reg_password">Password:</label>
          <input type="password" id="reg_password" v-model="registerForm.password" required />
        </div>
        <button type="submit">Register</button>
      </form>
    </div>

    <div v-if="!isLoggedIn">
      <h2>Login</h2>
      <form @submit.prevent="handleLogin">
        <div>
          <label for="login_username">Username:</label>
          <input type="text" id="login_username" v-model="loginForm.username" required />
        </div>
        <div>
          <label for="login_password">Password:</label>
          <input type="password" id="login_password" v-model="loginForm.password" required />
        </div>
        <button type="submit">Login</button>
      </form>
    </div>

    <div v-if="isLoggedIn">
      <p>Welcome, {{ currentUser }}!</p>
      <button @click="handleLogout">Logout</button>
    </div>

    <div v-if="message" :class="messageType">{{ message }}</div>
  </div>
</template>

<script>
import axios from 'axios';

// 将后端 API 地址提取为常量,方便维护
const API_BASE_URL = 'http://127.0.0.1:8000/api/accounts'; 

export default {
  name: 'App',
  data() {
    return {
      registerForm: { username: '', password: '' },
      loginForm: { username: '', password: '' },
      isLoggedIn: false,
      currentUser: null,
      message: '',
      messageType: ''
    }
  },
  methods: {
    async handleRegister() {
      try {
        const response = await axios.post(`${API_BASE_URL}/register/`, this.registerForm);
        console.log(response.data);
        this.setMessage(response.data.message, 'success');
      } catch (error) {
        console.error('Registration error:', error.response?.data || error.message);
        this.setMessage(
            error.response?.data?.username?.[0] || 
            error.response?.data?.password?.[0] || 
            error.response?.data?.non_field_errors?.[0] || 
            'Registration failed.',
            'error'
        );
      }
    },
    async handleLogin() {
      try {
        const response = await axios.post(`${API_BASE_URL}/login/`, this.loginForm);
        console.log(response.data);

        localStorage.setItem('access_token', response.data.access);
        localStorage.setItem('refresh_token', response.data.refresh);

        this.currentUser = this.loginForm.username;
        this.isLoggedIn = true;
        this.setMessage(response.data.message, 'success');

        this.loginForm.username = '';
        this.loginForm.password = '';

      } catch (error) {
        console.error('Login error:', error.response?.data || error.message);
        this.setMessage(
            error.response?.data?.non_field_errors?.[0] || 
            'Login failed.',
            'error'
        );
      }
    },
    handleLogout() {
      localStorage.removeItem('access_token');
      localStorage.removeItem('refresh_token');
      this.currentUser = null;
      this.isLoggedIn = false;
      this.setMessage('Logged out successfully.', 'success');
    },
    setMessage(text, type) {
      this.message = text;
      this.messageType = type;
      setTimeout(() => { this.message = ''; }, 3000);
    },
    checkAuthStatus() {
      const token = localStorage.getItem('access_token');
      if (token) {
        this.isLoggedIn = true;
        // 可以通过解析 token 获取用户名或调用 API 获取
        // this.currentUser = ... 
      }
    }
  },
  mounted() {
    this.checkAuthStatus();
  }
}
</script>

<style scoped>
#app { max-width: 600px; margin: 0 auto; padding: 20px; }
form div { margin-bottom: 10px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="password"] { width: 90%; padding: 8px; }
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background-color: #0056b3; }
.success { color: green; }
.error { color: red; }
</style>

3. 启动 Vue 应用

bash 复制代码
npm run dev

Vue 应用将在 http://localhost:5173 上启动。

三、常见问题与解决方案

在开发过程中,我遇到了一个非常典型且令人头疼的问题,值得单独拿出来记录一下。

问题现象:CORS 错误

当我尝试从前端发起注册请求时,浏览器控制台 Network 标签页显示 register/ 请求失败,状态码不明,响应数据也无法加载。Django 后端却打印出了 OPTIONS /api/accounts/register/ HTTP/1.1" 200 0 的日志。

问题分析

这是一个经典的 跨域资源共享 (CORS) 问题。

  1. 预检请求 (Preflight Request): 当前端发起一个复杂的请求(如 POST,带有自定义头)到另一个域名时,浏览器会先自动发送一个 OPTIONS 请求,询问服务器是否允许此次跨域请求。Django 成功响应了这个 OPTIONS 请求(返回 200),表示预检通过。
  2. 实际请求 (Actual Request): 预检通过后,浏览器才会发送实际的 POST 请求。然而,如果服务器没有为这个 POST 请求的响应添加正确的 CORS 头部(如 Access-Control-Allow-Origin),浏览器就会将响应拦截,并报告错误给前端代码。

解决方案

这个问题的根本原因是 django-cors-headers 中间件没有被正确加载或配置。

  • 确保安装: pip install django-cors-headers
  • 正确配置 settings.py:
    • 'corsheaders' 添加到 INSTALLED_APPS
    • 最重要的一点:'corsheaders.middleware.CorsMiddleware' 放在 MIDDLEWARE 列表的最前面
    • 正确配置 CORS_ALLOWED_ORIGINS
  • 重启服务器: 修改配置后必须重启 Django 服务器 (python manage.py runserver)。

完成以上配置后,POST 请求也能顺利收到带有正确 CORS 头部的响应,问题得以解决。

四、总结

通过本文,我们完成了 Django + Vue 全栈应用的基础用户认证功能。这个过程涉及到了前后端的通信、数据处理、安全性(密码哈希、JWT)、以及重要的跨域问题解决。这是一个很好的起点,后续可以在此基础上扩展更多功能,如用户资料管理、权限控制等。

相关推荐
2401_841495642 小时前
【强化学习】DQN 改进算法
人工智能·python·深度学习·强化学习·dqn·double dqn·dueling dqn
利刃大大2 小时前
【Vue】Vue介绍 && 声明式渲染 && 数据响应式
前端·javascript·vue.js·前端框架
qunaa01012 小时前
基于YOLO11-CSP-EDLAN的软夹持器夹持状态检测方法研究
python
SunnyDays10112 小时前
Python 文本转 PDF 完整指南:从字符串与 TXT 文件到专业 PDF 文档
python·txt转pdf·文本转pdf·文本文件转pdf
C系语言2 小时前
安装Python版本opencv命令
开发语言·python·opencv
FJW0208142 小时前
Python排序算法
python·算法·排序算法
pulinzt2 小时前
【python】第六节anacoda+配置Jupyter notebook
人工智能·python·jupyter
逄逄不是胖胖2 小时前
《动手学深度学习》-49Style_Transfer实现
pytorch·python·深度学习