引言
在现代 Web 开发中,前后端分离已成为主流架构。后端负责数据处理和业务逻辑(如 Django),前端则专注于用户界面和交互(如 Vue)。本文将带你一步步构建一个基础的全栈应用,实现用户注册和登录功能,并详细记录过程中可能遇到的典型问题及其解决方案。
我们将使用:
本文介绍了基于 Django 和 Vue 3 构建全栈应用的流程,重点实现了用户注册与登录功能。通过集成 Django REST Framework 和 SimpleJWT 完成后端认证逻辑,配置 Vue 3 前端组件进行交互,并详细解决了跨域(CORS)等常见问题。
在现代 Web 开发中,前后端分离已成为主流架构。后端负责数据处理和业务逻辑(如 Django),前端则专注于用户界面和交互(如 Vue)。本文将带你一步步构建一个基础的全栈应用,实现用户注册和登录功能,并详细记录过程中可能遇到的典型问题及其解决方案。
我们将使用:
首先,创建并激活 Python 虚拟环境(推荐使用 venv 或 conda),然后安装所需包:
pip install django djangorestframework djangorestframework-simplejwt python-decouple # python-decouple 用于管理环境变量(可选但推荐)
接着,创建 Django 项目和应用:
django-admin startproject myproject
cd myproject
python manage.py startapp accounts_api
myproject/settings.py)这部分是整个后端的核心配置。我们需要集成 DRF、SimpleJWT、以及处理跨域问题的 django-cors-headers。
import os
from pathlib import Path
from datetime import timedelta
# 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.NumericValidator'},
]
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 列表的最前面,否则它可能无法拦截到某些请求。
accounts_api/serializers.py)序列化器用于定义数据的输入和输出格式。
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
accounts_api/views.py)视图处理具体的业务逻辑。
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)
accounts_api/urls.py & myproject/urls.py)# 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'),
]
# 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')),
]
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 端点。
npm create vue@latest my-vue-app
cd my-vue-app
npm install axios
src/App.vue)这里整合了登录、注册和状态管理的逻辑。
<template>
<div>
<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" v-model="registerForm.username" required />
</div>
<div>
<label for="reg_password">Password:</label>
<input type="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" v-model="loginForm.username" required />
</div>
<div>
<label for="login_password">Password:</label>
<input type="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>
npm run dev
Vue 应用将在 http://localhost:5173 上启动。
在开发过程中,我遇到了一个非常典型且令人头疼的问题,值得单独拿出来记录一下。
当我尝试从前端发起注册请求时,浏览器控制台 Network 标签页显示 register/ 请求失败,状态码不明,响应数据也无法加载。Django 后端却打印出了 OPTIONS /api/accounts/register/ HTTP/1.1" 200 0 的日志。
这是一个经典的 跨域资源共享 (CORS) 问题。
OPTIONS 请求,询问服务器是否允许此次跨域请求。Django 成功响应了这个 OPTIONS 请求(返回 200),表示预检通过。POST 请求。然而,如果服务器没有为这个 POST 请求的响应添加正确的 CORS 头部(如 Access-Control-Allow-Origin),浏览器就会将响应拦截,并报告错误给前端代码。这个问题的根本原因是 django-cors-headers 中间件没有被正确加载或配置。
pip install django-cors-headerssettings.py:
'corsheaders' 添加到 INSTALLED_APPS。'corsheaders.middleware.CorsMiddleware' 放在 MIDDLEWARE 列表的最前面。CORS_ALLOWED_ORIGINS。python manage.py runserver)。完成以上配置后,POST 请求也能顺利收到带有正确 CORS 头部的响应,问题得以解决。
通过本文,我们完成了 Django + Vue 全栈应用的基础用户认证功能。这个过程涉及到了前后端的通信、数据处理、安全性(密码哈希、JWT)、以及重要的跨域问题解决。这是一个很好的起点,后续可以在此基础上扩展更多功能,如用户资料管理、权限控制等。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online