引言
在现代 Web 开发中,前后端分离已成为主流架构。后端负责数据处理和业务逻辑(如 Django),前端则专注于用户界面和交互(如 Vue)。本文将带你一步步构建一个基础的全栈应用,实现用户注册和登录功能,并详细记录过程中可能遇到的典型问题及其解决方案。
我们将使用:
- 后端 : Django + Django REST Framework (DRF) + SimpleJWT
- 前端 : Vue 3
一、后端搭建 (Django)
1. 环境准备
首先,创建并激活 Python 虚拟环境(推荐使用 venv 或 conda),然后安装所需包:
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) 问题。
- 预检请求 (Preflight Request): 当前端发起一个复杂的请求(如 POST,带有自定义头)到另一个域名时,浏览器会先自动发送一个
OPTIONS请求,询问服务器是否允许此次跨域请求。Django 成功响应了这个OPTIONS请求(返回 200),表示预检通过。 - 实际请求 (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)、以及重要的跨域问题解决。这是一个很好的起点,后续可以在此基础上扩展更多功能,如用户资料管理、权限控制等。