1、功能需求
通常中小型前后端项目,对安全要求不高,也可以采用密码认证方案。如果只用django来实现非常简单。采用 Vue3 前后端分离架构,实现起来稍繁琐一点,好处是可以利用各种前端技术栈,如element-plus UI库来渲染页面。
演示项目需求为:
- Vue3 前端提供登录页面
- 输入用户名与密码后,发送POST登录请求至服务器,后者验证通过后,用json格式返回认证结果.
- 前端收到响应后,如果认证通过,更新用户登录状态,保存响应消息中传来的 cookie
- 后续请求中,携带cookie,服务端根据请求消息中的cookie验证,通过后,以json格式返回数据。
2、前后端技术栈环境
前端技术栈:
- vue3
- element-plus UI 库
- pinia 状态管理库
- axios 库
准备Vue3环境
进入保存项目的目录,如d:/workplace/projects/, 运行命令:
npm create vue@latest
这个命令会安装create-vue 工具,并执行创建项目,其过程会显示许多配置选项
新项目的路径为项目名称,即vue02/ , 生成的项目结构如下。
项目默认采用组合式API
D:\workplace\web\vue02>tree /A /F
卷 软件 的文件夹 PATH 列表
卷序列号为 0DC5-179B
D:.
| .gitignore
| index.html
| package.json
| README.md
| vite.config.js
+---.vscode
| extensions.json
+---public
| favicon.ico
\---src
| App.vue
| main.js
|
+---assets
| base.css
| logo.svg
| main.css
|
+---components
| | HelloWorld.vue
| | TheWelcome.vue
| | WelcomeItem.vue
|
+---router
| index.js
|
\---views
AboutView.vue
HomeView.vue
修改App.vue,清空项目。
导入依赖库
安装element-plus, axios, pinia
npm install element-plus --save-dev
npm install @element-plus/icons-vue --save-dev
npm install axios --save-dev
npm install pinia --save-dev
在main.js 全局导入依赖库
import { createApp } from 'vue'
import "./assets/main.css"
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router'
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import formCreate from '@form-create/element-ui'
const app = createApp(App)
app.use(createPinia()) // 导入pinia 库
app.use(router)
app.use(ElementPlus)
app.use(formCreate)
//导入所有elementplus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
创建路由文件 src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue')
},
{
path: '/order',
name: 'order',
component: () => import("../views/FormOrder.vue")
},
{
path: '/login',
name: 'login',
component: () => import("../views/Login.vue")
},
]
})
export default router
修改 App.vue, 添加布局与导航菜单
<template>
<div class="common-layout">
<el-container>
<el-header class="el-header">Vue3 测试项目</el-header>
<el-container>
<el-aside id="demo-aside" :width="isCollapse ? '64px':'180px'">
<div class="toggle-button" @click="toggleCollapse" style="color: #ffffff;"><el-icon size="15" color="#fff" style="margin-top: 5px;"><Menu /></el-icon></div>
<el-menu background-color="#222222" active-text-color="#8ef" text-color="#fff" default-active="2"
class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose" :router="true" :collapse="isCollapse" :collapse-transition="false">
<el-menu-item index="/logina">
<el-icon size="15" color="#fff"><User /></el-icon>
<span>密码登录</span>
</el-menu-item>
<el-menu-item index="/loginjwt">
<el-icon size="15" color="#fff"><User /></el-icon>
<span>JWT登录</span>
</el-menu-item>
<el-menu-item index="/">
<el-icon :size="15" color="#fff"> <Flag /></el-icon>
<span>演示</span>
</el-menu-item>
<el-menu-item index="/listdata">
<el-icon :size='15' color='#fff'><Collection /></el-icon>
<span>显示Blog</span>
</el-menu-item>
<el-menu-item index="/about">
<el-icon :size="15" color="#fff"> <Plus /></el-icon>
<span>新建Blog</span>
</el-menu-item>
<el-menu-item index="/order">
<el-icon size="15" color="#fff"><Sell /></el-icon>
<span>订单管理</span>
</el-menu-item>
<el-sub-menu >
<template #title >
<el-icon :size="15" color="#fff"> <Setting /></el-icon>
<span>选项</span>
</template>
<el-menu-item index="3-1">item one</el-menu-item>
<el-menu-item index="3-2">item two</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-container>
<el-main class="el-main">
<router-view></router-view>
</el-main>
<el-footer>Footer</el-footer>
</el-container>
</el-container>
</el-container>
</div>
</template>
<script>
import { RouterLink, RouterView } from 'vue-router'
export default {
data(){
return {
isCollapse: false
}
},
components: {
},
methods: {
handleOpen(key, keyPath){
console.log(key, keyPath)
},
handleClose(key, keyPath){
console.log(key, keyPath)
},
toggleCollapse(){
this.isCollapse = !this.isCollapse
}
},
}
</script>
<style lang="scss" scoped>
html, body, .common-layout {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
.el-container {
height: 100%;
}
.el-header {
margin: 0px;
padding-top: 5px;
padding-bottom: 5px;
height: 30px;
text-align: center;
background-color: darkblue;
color: #ffffff;
}
.el-aside {
background-color: #222;
text-align: center;
}
.el-main {
height: 600px;
color: black;
}
.el-footer {
background-color: rgb(6, 15, 103);
color: #fff;
height: 25px;
}
.sub-hide * {
color: #222;
}
.sub-show {
color: #ffffff;
}
</style>
Django后端环境准备
请参考作者另一篇 [博文] (https://blog.csdn.net/captain5339/article/details/131572762) 准备django环境
3、实现流程分析
Login登录的时序图如下
说明:
- response 消息的header:中,django服务器通过set-cookie发送sessionid 以及csrftoken。
Browser会自动保存set-cookie的值,对于后续请求,自动将cookie添加到头部,通常无须处理。
Set-Cookie: csrftoken=stUBZaZO26cKbf6RidHmmgiwHAFmY31jFpUbFuMqa8gJycz8WB4DNc6jmNexsqn6; expires=Wed, 19 Mar 2025 10:45:44 GMT; Max-Age=31449600; Path=/; SameSite=Lax
Set-Cookie: sessionid=anv6tzhtws4mzdl5hprjcucre1feynyk; expires=Wed, 03 Apr 2024 10:45:44 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax
- api 登录接口与网页登录页面是有区别的,server端应该分别实现页面登陆与api login 视图, api login 应该用json格式发送登录结果。
Vue3 + Pinia 实现技术要点
思路:
- 通过 pinia 的store 来保存用户信息及登录状态,userinfo, 通过axios 发送login 请求,登陆成功后,将用户全局状态改为loginStatus=true,
技术要点:
- 使用pinia 保存username, loginStatus,并且将登录 api 方法也放在pinia store中。 可以采用base64或des对密码进行必要的加密后再发送。
- 在store api方法中axios发送请求时使用 async await 语法, 组件的事件处理方法也采用async await 方式调用api, 这样可以避免不同步现象。
- 对于响应返回的cookie,浏览器可以自行处理( 问题:读 set-cookie失败)
4、具体步骤
(1) 创建userStore
主要包含
state:- username, password,loginStatus等数据。
actions:
- login() ,通过axios 发送登录请求。
- logout()
创建 store 文件: src/stores/userStore.js
import { ref, computed } from 'vue' import { defineStore } from 'pinia' import axios from 'axios'; export const useUserStore = defineStore('user', () => { const username = ref('') const password = ref('') const loginStatus = ref(false) const ax = axios.create({ baseURL: 'http://localhost:8000', //请求后端数据的基本地址,自定义 timeout: 2000 //请求超时设置,单位ms }) ax.defaults.withCredentials = true const Login = async (userName, pass) => { try { const res = await ax({ url: '/v1/api-auth/login/', method: 'post', headers: { 'Content-Type': 'multipart/form-data', }, data: { username: userName, password: pass, }, }) console.log(res.data) console.log(res.headers) if (res.data.result == 'success') { username.value = userName loginStatus.value = true } else { loginStatus.value = false } } catch (error) { console.log(error) } } //清空state const clearUserStore = () => { username.value = '' password.value = '' loginStatus.value = false } return { username, password, loginStatus, Login, clearUserStore } })
(2)创建登陆组件
a) 提供username, password 输入表单
b) 将login表单数据传入 userStore的login()方法。
c) 处理response数据
- 登陆成功:更新loginStatus, 重定向至下一页
- 登陆失败,显示失败信息,继续重试。组件名称 src/views/Login.vue
<template> <el-form ref="form" style="max-width: 500px" :model="userinfo" label-width="80px" label-position="left" > <el-form-item label="登陆名"> <el-input v-model="userinfo.username" /> </el-form-item> <el-form-item label="密码"> <el-input v-model="userinfo.password" /> </el-form-item> <el-form-item> <el-button type="primary" @click="onLogin">登录</el-button> </el-form-item> </el-form> </template> <script setup> import { ref,reactive } from 'vue' import axios from 'axios' import { useUserStore } from "../stores/userStore.js" import { ElMessage } from 'element-plus' const userinfo = reactive({ username: '', password: '', }) const store = useUserStore() const onLogin = async ()=> { await store.Login(userinfo.username, userinfo.password) console.log(store.loginStatus) if(store.loginStatus == true ){ console.log("登录成功") ElMessage({ message: '登录成功', type: 'success', }) } else { ElMessage({ message: '登录失败', type: 'warning', }) } } </script> <style> </style>
(3) 修改路由数据以及父组件
a) 修改src/router/router.js
import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ … { path: '/login', name: 'login', component: () => import("../views/Login.vue") }, ] }) export default router
b) 添加菜单项,指向新建路由
src/app.vue<el-menu-item index="/logina"> <el-icon size="15" color="#fff"><User /></el-icon> <span>密码登录</span> </el-menu-item>
(4) Django 实现登录API
注意,django应提供基于api的view ,而非基于页面视图的login view.
from rest_framework import status from rest_framework.decorators import api_view, authentication_classes from rest_framework.response import Response from rest_framework.authentication import ( SessionAuthentication, BasicAuthentication ) from django.contrib.auth import authenticate, login, logout from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .models import * from .serializers import ArticleSerializer, UserSerializer @csrf_exempt def api_login(request): if request.method == "POST": print(list(request.POST.items())) username = request.POST['username'] password = request.POST['password'] user = authenticate(request, username=username, password=password) if user is not None: login(request, user) # Redirect to a success page. return JsonResponse({"result": "success"}) else: # Return an 'invalid login' error message. return JsonResponse({'result': 'failed' ,'reason': "用户名与密码不正确"}) else: return JsonResponse({"result": "rejected", "reason": "request method must be post"}, status=403) @csrf_exempt def api_logout(request): logout(request) return JsonResponse({"result": "success"}) @api_view(['GET','POST']) @authentication_classes([SessionAuthentication, BasicAuthentication]) def article_list(request, format=None): """ List all articles, or create a new article. """ if request.method == 'GET': qs = Article.objects.all() qs = qs.select_related('author') serializer = ArticleSerializer(qs, many=True) return Response(serializer.data) elif request.method == 'POST': serializer = ArticleSerializer(data=request.data) if serializer.is_valid(): # Very important. Associate request.user with author serializer.save(author=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @api_view(['GET', 'PUT', 'DELETE']) def article_detail(request, pk,format=None): """ Retrieve,update or delete an article instance。""" try: qs = Article.objects.select_related('author') article = qs.get(pk=pk) except Article.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) if request.method == 'GET': serializer = ArticleSerializer(article) return Response(serializer.data) elif request.method == 'PUT': serializer = ArticleSerializer(article, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) elif request.method == 'DELETE': article.delete() return Response(status=status.HTTP_204_NO_CONTENT)
修改app.urls , 添加path
urlpatterns = [ ... path('api-auth/login/', api_login, name='login'), path('api-auth/logout/',api_logout,name='logout'), path('articles/', article_list), ... ]
5、运行与测试
进入django 文件夹,启动server
python manage.py runserver 0.0.0.0:8000
默认服务器端口为 http://127.0.0.1:8000
登录 api url: http://127.0.0.1:8000/v1/api-auth/login/进入vue3项目文件夹,启动项目
npm run dev
默认前端访问地址:
http://localhost:5173/
通过菜单进入登录表单页,打开浏览器的开发者工具,点击网络选项
输入用户名与密码后,点击提交按钮,axio发送请求至服务器,
服务器端发送响应,vue3组件收到后,弹出登录成功的 message。接口消息可以从开发者工具的网络视图中查看。
后续请求消息处理
如访问 http://127.0.0.1:8000/v1/articles/ 时,可以看到vue3在自动将 sessionid, csrftoken 放进request 的cookie中了。 django服务器根据sessionid 确定该user是否已通过登录验证。如果通过允许访问 /v1/articles/ 接口。否则将拒绝。