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
npminstall element-plus --save-dev
npminstall @element-plus/icons-vue --save-dev
npminstall axios --save-dev
npminstall 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")},]})exportdefault 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'exportdefault{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 202510:45:44 GMT; Max-Age=31449600;Path=/;SameSite=Lax
Set-Cookie: sessionid=anv6tzhtws4mzdl5hprjcucre1feynyk;expires=Wed, 03 Apr 202410: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';exportconst 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 =trueconstLogin=async(userName, pass)=>{try{const res =awaitax({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 constclearUserStore=()=>{ 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()constonLogin=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.jsimport{ 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")},]})exportdefault routerb) 添加菜单项,指向新建路由 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 statusfrom rest_framework.decorators import api_view, authentication_classesfrom rest_framework.response import Responsefrom rest_framework.authentication import( SessionAuthentication, BasicAuthentication)from django.contrib.auth import authenticate, login, logoutfrom django.http import JsonResponsefrom django.views.decorators.csrf import csrf_exemptfrom.models import*from.serializers import ArticleSerializer, UserSerializer@csrf_exemptdefapi_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 isnotNone: 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_exemptdefapi_logout(request): logout(request)return JsonResponse({"result":"success"})@api_view(['GET','POST'])@authentication_classes([SessionAuthentication, BasicAuthentication])defarticle_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'])defarticle_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 , 添加pathurlpatterns =[... path('api-auth/login/', api_login, name='login'), path('api-auth/logout/',api_logout,name='logout'), path('articles/', article_list),...]## 5、运行与测试进入django 文件夹,启动serverpython 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/ 接口。否则将拒绝。
版权归原作者 __弯弓__ 所有, 如有侵权,请联系我们删除。