Vue3 + Django 前后端分离项目实现密码认证登录
作者:mmseoamin日期:2024-04-27

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, 添加布局与导航菜单

      
      
      

      Django后端环境准备

      请参考作者另一篇 [博文] (https://blog.csdn.net/captain5339/article/details/131572762) 准备django环境

      3、实现流程分析

      Login登录的时序图如下

      Vue3 + Django 前后端分离项目实现密码认证登录,在这里插入图片描述,第1张

      说明:

      • 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

                  
                  
                  

                  (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

                  
                  	
                  	密码登录
                  
                  

                  (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 + Django 前后端分离项目实现密码认证登录,在这里插入图片描述,第2张

                  服务器端发送响应,vue3组件收到后,弹出登录成功的 message。接口消息可以从开发者工具的网络视图中查看。

                  Vue3 + Django 前后端分离项目实现密码认证登录,在这里插入图片描述,第3张

                  后续请求消息处理

                  如访问 http://127.0.0.1:8000/v1/articles/ 时,可以看到vue3在自动将 sessionid, csrftoken 放进request 的cookie中了。 django服务器根据sessionid 确定该user是否已通过登录验证。如果通过允许访问 /v1/articles/ 接口。否则将拒绝。