1.创建项目(后端)
-
前端vue.js项目:city
https://gitee.com/wupeiqi/city
-
后端django项目:mtb
https://gitee.com/wupeiqi/mtb
项目代码的git上会同步更新,大家下载下来后,可以根据提交记录来进行回滚,查看看各个版本。
1.1 虚拟环境&项目
和原来不太一样的创建django项目
(1、用原来的方式直接创建django项目会自动安装最新的django版本)
(2、pycharm老版本创建会报错)
-
在pycharm中创建项目【空Python项目 Prue Python】+【虚拟环境 Virtualenv】
-
在pycharm中进入到当前虚拟环境
-
安装django
pip install django==3.2
-
创建django项目到当前目录/Users/wupeiqi/PycharmProjects/mtb
django-admin startproject mtb /Users/wupeiqi/PycharmProjects/mtb
1.2 创建app
-
项目根目录下创建apps目录
-
在apps目录下创建文件夹(app)
apps - task - msg - base
-
执行命令
python manage.py startapp task apps/task python manage.py startapp msg apps/msg python manage.py startapp base apps/base
1.3 注册app
-
在app目录下的app.py中修改
from django.apps import AppConfig class TaskConfig(AppConfig): name = 'task' from django.apps import AppConfig class TaskConfig(AppConfig): name = 'apps.task'
-
在settings.py中注册
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', "apps.task.apps.TaskConfig" ]
1.4 基本配置
-
移除无用的app(django自带的不用 注释掉 相关表结构也不会生成)
INSTALLED_APPS = [ # 'django.contrib.admin', # 'django.contrib.auth', # 'django.contrib.contenttypes', # 'django.contrib.sessions', # 'django.contrib.messages', 'django.contrib.staticfiles', "apps.base.apps.BaseConfig" ]
-
移除无用的中间件(app和中间件对应都注掉)
MIDDLEWARE = [ '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', ]
-
数据库相关(MySQL)
-
创建数据库 create database mtb DEFAULT CHARSET utf8 COLLATE utf8_general_ci; 设置数据库连接 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mtb', 'USER': 'root', 'PASSWORD': 'root123', 'HOST': '127.0.0.1', 'PORT': 3306, } } 安装python操作MySQL模块 pip install mysqlclient
-
1.5 本地配置
-
settings.py(引入本地settings)
try: from .local_settings import * except ImportError: pass
-
local_settings.py(新建一个本地settings文件 在共享代码的时候把一些敏感数据保留不分享 把此文件留下 不分享)
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mydatabase', 'USER': 'mydatabaseuser', 'PASSWORD': 'mypassword', 'HOST': '127.0.0.1', 'PORT': '5432', } }
1.6 代码仓库
提示:如果你没学过git,请忽略这一小节,先去看我单独录的git实战课程。
如果想要代码共享给他人 或 多人协同开发,来会发文件不方便。
如果想要保留自己之前编写功能的的版本,每次都生成一个文件夹也不方便。
程序员一般都用git来解决上述问题,想要用git需要两个步骤:
-
【自己电脑上】安装git + 学习git相关的命令。
git ... .venv文件夹 local_settings.py
-
【代码仓库】创建项目,将本地项目推送上去,可共享给他人。 gitee
https://gitee.com/wupeiqi/mtb
1.7 启动&运行项目
手动配置Pycharm去运行。
2.认证(登录)
2.1 后端API
-
基于token,drf案例中的项目。(会在数据库存储)(不建议使用 对数据库压力大)
-
基于jwt【推荐】(不会在数据存储)
-
在项目开发中,一般会按照上图所示的过程进行认证,即:用户登录成功之后,服务端给用户浏览器返回一个token,以后用户浏览器要携带token再去向服务端发送请求,服务端校验token的合法性,合法则给用户看数据,否则,返回一些错误信息。
传统token方式和jwt在认证方面有什么差异?
-
传统token方式
用户登录成功后,服务端生成一个随机token给用户,并且在服务端(数据库或缓存)中保存一份token,以后用户再来访问时需携带token,服务端接收到token之后,去数据库或缓存中进行校验token的是否超时、是否合法。
-
jwt方式
用户登录成功后,服务端通过jwt生成一个随机token给用户(服务端无需保留token),以后用户再来访问时需携带token,服务端接收到token之后,通过jwt对token进行校验是否超时、是否合法。
-
jwt的原理是什么呢?
https://www.cnblogs.com/wupeiqi/p/11854573.html
jwt的生成token格式如下,即:由 .
连接的三段字符串组成。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
生成规则如下:
-
第一段HEADER部分,固定包含算法和token类型,对此json进行base64url加密,这就是token的第一段。
{ "alg": "HS256", "typ": "JWT" }
-
第二段PAYLOAD部分,包含一些数据,对此json进行base64url加密,这就是token的第二段
{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 ... }
-
第三段SIGNATURE部分,把前两段的base密文通过
.
拼接起来,然后对其进行HS256
加密,再然后对hs256
密文进行base64url加密,最终得到token的第三段。base64url( HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret (秘钥加盐) ) )
最后将三段字符串通过 .
拼接起来就生成了jwt的token。
注意:base64url加密是先做base64加密,然后再将 -
替代 +
及 _
替代 /
。
pip install pyjwt==2.3.0
-
生成jwt token(由点.链接的三段字符串)
import jwt import datetime from jwt import exceptions SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv=' def create_token(): # 第一段构造header 固定包含算法和token类型,对此josn进行base64url加密,这就是token的第一段 headers = { 'typ': 'jwt', 'alg': 'HS256' //加密 } # 第二段构造payload 包含一些数据对此json进行base64url加密 这就是token的第二段 payload = { 'user_id': 1, # 自定义用户ID 'username': 'wupeiqi', # 自定义用户名 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # 超时时间 } # 三 jwt.encode生成token result = jwt.encode(payload=payload, key=SALT, algorithm="HS256", headers=headers) return result if __name__ == '__main__': token = create_token() print(token) # eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Ind1cGVpcWkiLCJleHAiOjE2NDc2MjMzMDR9.mC409LXIl1RZu4OX5J01hvCxWEOJcK7C4P3zKzedXdU
-
校验
import jwt from jwt import exceptions SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv=' def get_payload(token): """ 根据token获取payload :param token: :return: """ try: # 从token中获取payload【校验合法性,并获取payload】 verified_payload = jwt.decode(token, SALT, ["HS256"]) # 只有校验成功了才会拿到 return verified_payload except exceptions.ExpiredSignatureError: print('token已失效') except jwt.DecodeError: print('token认证失败') except jwt.InvalidTokenError: print('非法的token') if __name__ == '__main__': token = "eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Ind1cGVpcWkiLCJleHAiOjE2NDc2MjMzMDR9.mC409LXIl1RZu4OX5J01hvCxWEOJcK7C4P3zKzedXdU" payload = get_payload(token) print(payload)
2.1.1 创建表和数据
-
创建相关表结构并录入基本数据。
from django.db import models class UserInfo(models.Model): username = models.CharField(verbose_name="用户名", max_length=32) password = models.CharField(verbose_name="密码", max_length=64)
-
离线脚本,创建用户
#!/usr/bin/env python # -*- coding:utf-8 -*- import os import sys import django base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(base_dir) # os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mtbao.settings") # 1、加载django配置 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mtb.settings") # 2、启动django django.setup() from apps.base import models # 3、录入数据 models.UserInfo.objects.create( username="wupeiqi", password="123123" )
2.1.2 登录(基于djangorestframework 和 pyjwt)
# pip install djangorestframework==3.12.4 # 用镜像 pip install djangorestframework==3.12.4 -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com pip install pyjwt==2.3.0
-
settings apps配置restframework
INSTALLED_APPS = [ # 'django.contrib.admin', # 'django.contrib.auth', # 'django.contrib.contenttypes', # 'django.contrib.sessions', # 'django.contrib.messages', 'django.contrib.staticfiles', 'apps.base.apps.BaseConfig', 'apps.msg.apps.MsgConfig', 'apps.task.apps.TaskConfig', 'rest_framework', ] REST_FRAMEWORK = { # 认证 "UNAUTHENTICATED_USER": lambda: None, "UNAUTHENTICATED_TOKEN": lambda: None, }
-
编写URL
from django.urls import path, include from apps import base urlpatterns = [ # 路由分发 path('api/base/', include('apps.base.urls')), ] from django.urls import path from rest_framework import routers from .views import account router = routers.SimpleRouter() # 评论 # router.register(r'comment', comment.CommentView) urlpatterns = [ # path('register/', account.RegisterView.as_view({"post": "create"})), path('auth/', account.AuthView.as_view()), ] urlpatterns += router.urls
-
编写视图 & 生成jwt token
import datetime import jwt from django.conf import settings from rest_framework.views import APIView from rest_framework.response import Response from utils.extension import return_code from ..serializers.account import AuthSerializer from .. import models class AuthView(APIView): """ 用户登录 """ # 如果全局设置了认证组件和权限组件 访问此视图 我不用 登录接口谁都可以访问 authentication_classes = [] permission_classes = [] def post(self, request): # 获取用户请求发送用户名和密码 # 1. 表单验证serializer系列化器,数据校验用户名密码不能为空 serializer = AuthSerializer(data=request.data) if not serializer.is_valid(): return Response({"code": return_code.VALIDATE_ERROR, 'detail': serializer.errors}) # 2.数据库查询 username = serializer.validated_data.get('username') password = serializer.validated_data.get('password') # 当前用户 user_object = models.UserInfo.objects.filter(username=username, password=password).first() if not user_object: return Response({"code": return_code.VALIDATE_ERROR, "error": "用户名或密码错误"}) # 3、登录成功,生成jwt token headers = { 'typ': 'jwt', 'alg': 'HS256' } # 构造payload payload = { 'user_id': user_object.id, 'username': user_object.username, "exp": datetime.datetime.now() + datetime.timedelta(weeks=2) } # settings.SECRET_KEY这个当做是盐了 token = jwt.encode(payload=payload, key=settings.SECRET_KEY, algorithm="HS256", headers=headers) return Response({"code": return_code.SUCCESS, "data": {"token": token, "name": user_object.username}})
-
序列化器
serializers/accoutn.py from rest_framework import serializers class AuthSerializer(serializers.Serializer): # required 是否是必填字段 username = serializers.CharField(label="用户名", required=True) password = serializers.CharField(label="密码", min_length=6, required=True) return_code.py # 成功 SUCCESS = 0 # 失败 ERROR = 100 # 用户提交数据校验失败(字段错误) FIELD_ERROR = 1000 # 用户提交数据校验失败 VALIDATE_ERROR = 1001 # 认证失败 AUTH_FAILED = 2000 # 认证过期 AUTH_OVERDUE = 2001 # 无权访问 PERMISSION_DENIED = 3000 # 无权访问 TOO_MANY_REQUESTS = 4000
2.1.3 校验
在请求其他页面时,对jwt token进行校验(认证组件)。
-
测试API
urls.py from django.urls import path from rest_framework import routers from .views import account router = routers.SimpleRouter() # 其他注册方式 router.register(r'public', wx.PublicNumberView) urlpatterns = [ path('auth/', account.AuthView.as_view()), path('test/', account.TestView.as_view()), ] urlpatterns += router.urls views/accounts.py class TestView(APIView): def get(self, request, *args, **kwargs): # 获取当前用户登录的用户名和用户id print(request.user.user_id) print(request.user.username) print(request.user.exp) return Response("test")
-
编写认证组件
exy/auth.py import jwt from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from rest_framework.exceptions import NotAuthenticated from django.conf import settings from .. import return_code from corsheaders.middleware import CorsMiddleware class CurrentUser(object): # 将这几个数据封装到一个对象里 def __init__(self, user_id, username, exp): self.user_id = user_id self.username = username self.exp = exp class MtbAuthenticationFailed(AuthenticationFailed): status_code = 200 class JwtTokenAuthentication(BaseAuthentication): def authenticate(self, request): # OPTIONS # 1、读取用户提交的jwt token # 这个是在请求地址中获取的每次有点麻烦 可以放到请求头中获取 # token = request.query_params.get("token") # 去请求头获取token Authorization: token 401f7ac837da42b97f613d789819ff93537bee6a token = request.META.get('HTTP_AUTHORIZATION') # 首先校验是否有token(AuthenticationFailed settings文件里有全局认证配置) if not token: # 状态码=>401 ,内容=>{code:2000,error:"认证失败"} raise AuthenticationFailed({"code": return_code.AUTH_FAILED, "error": "认证失败"}) # 状态码=>200 ,内容=>{code:2000,error:"认证失败"} # raise MtbAuthenticationFailed({"code": return_code.AUTH_FAILED, "error": "认证失败"}) # 2、jwt token校验 try: payload = jwt.decode(token, settings.SECRET_KEY, ["HS256"]) # {'user_id': 1, 'username': 'wupeiqi', 'exp': 1648309198} # print(payload, type(payload)) # 将payload字典直接放进去 **payload会结构放到各自属性 # TestView接口能够获取的信息返回 return CurrentUser(**payload), token except Exception as e: # 状态码=>200 ,内容=>{code:2000,error:"认证失败"} raise MtbAuthenticationFailed({"code": return_code.AUTH_FAILED, "error": "认证失败"}) def authenticate_header(self, request): # 返回响应头 return 'Bearer realm="API"'
-
认证全局配置
settings.py REST_FRAMEWORK = { # # 认证配置 "DEFAULT_AUTHENTICATION_CLASSES": ["utils.ext.auth.JwtTokenAuthentication", ], "UNAUTHENTICATED_USER": lambda: None, "UNAUTHENTICATED_TOKEN": lambda: None, # 分页配置 # "DEFAULT_PAGINATION_CLASS": "utils.ext.page.MtbLimitOffsetPagination" }
2.2 前端vue
使用输出命令进行调试
-
console.log();
-
alert();此命令为阻塞式命令,如果不点击确定,后续代码则不会执行。
-
打开页面:在本地的cookie中读取 username,写入到 state (便于后续view页面使用)
-
路由拦截:在本地的cookie中读取 jwt token,如果有则继续访问,没有就跳转登录页面。
-
登录:
-
发送请求,验证用户名密码合法性
-
写入cookie和state
-
跳转
-
-
其他API请求(每个请求都要在请求头中 携带jwt token)
2.2.1 打开页面
浏览器打开平台页面时,自动去cookie中读取之前登录写入到cookie中的用户名和token
npm install vue-cookie
plugins/cookie.js
import Vue from 'vue' import VueCookie from 'vue-cookie' Vue.use(VueCookie) // export 导出token export const getToken = () => { return Vue.cookie.get("token"); } // export 导出username export const getUserName = () => { return Vue.cookie.get("username"); } he plugin is available through this.$cookie in components or Vue.cookie // From some method in one of your Vue components this.$cookie.set('test', 'Hello world!', 1); // This will set a cookie with the name 'test' and the value 'Hello world!' that expires in one day // To get the value of a cookie use this.$cookie.get('test'); // To delete a cookie use this.$cookie.delete('test');
store/index.js
import Vue from 'vue' import Vuex from 'vuex' import {getUserName,getToken} from "@/plugins/cookie"; Vue.use(Vuex) // vuex的使用 export default new Vuex.Store({ state: { // 这种方式不好 可以把cookie相关的东西放到一个文件里@/plugins/cookie 方便操作 // username:Vue.cookie.get("username") username:getUserName(), token:getToken(), }, getters: {}, mutations: {}, actions: {}, modules: {} })
View Code
Layout.vue
// vuex的使用 // <template slot="title">{{ username }}</template> // computed:{// 计算属性 username(){ return this.$store.state.username() } <!-- ----------------------------------------------- --> <template> <!-- 顶部菜单 --> <div> <div> <!-- default属性表示激活谁 :default-active="rootActiveRouter" 激活谁谁就被选中 --> <!-- router表示可以把<el-menu-item>当作route-link用 --> <el-menu class="el-menu-demo" mode="horizontal" background-color="#545c64" text-color="#fff" :default-active="rootActiveRouter" active-text-color="#ffd04b" router> <!-- 点击切换tab 需要router-link 现在都是<el-menu-item> 所以再上个标签加入router属性就有router-link效果了 --> <!-- 通过route属性:route="{name:'ActivityList'} 可以帮我们跳转到某个路由--> <el-menu-item>媒体宝系统</el-menu-item> <!-- 点击一级菜单可以默认选中一个二级菜单跳转 将名字改成二级Activity 就行了--> <el-menu-item index="Task" :route="{name:'Activity'}">任务宝</el-menu-item> <el-menu-item index="Msg" :route="{name:'Push'}">消息宝</el-menu-item> <el-menu-item index="Auth" :route="{name:'Auth'}">授权</el-menu-item> <el-submenu index="2" style="float: right"> <template slot="title">{{ username }}</template> <el-menu-item index="2-1">个人中心</el-menu-item> <el-menu-item index="2-2">注销</el-menu-item> </el-submenu> </el-menu> </div> <div> <!-- 菜单下的内容 通过子组件来渲染内容 --> <router-view></router-view> </div> </div> </template> <script> export default { // eslint-disable-next-line vue/multi-word-component-names name: "Layout", data() { return { // 激活选中状态的变量 rootActiveRouter: "" } }, // 获取当前的所有路由 mounted() { // 获取当前的所有路由 获取当前路由组件的名称 用来默认选中tab(刷新后还是默认选中的) this.rootActiveRouter = this.$route.matched[1].name; }, computed:{// 计算属性 username(){ return this.$store.state.username() } } } </script> <style scoped> </style>
View Code
main.js
import Vue from 'vue' import App from './App.vue' import './plugins/cookie.js' import store from './store' import router from './router' import './plugins/element.js' import HighchartsVue from 'highcharts-vue' Vue.use(HighchartsVue) Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')
View Code
2.2.2 路由拦截器
如果cookie中的 token 为空,则重定向到登录页面。
router/index.js
import Vue from 'vue' import VueRouter from 'vue-router' import {getToken} from '@/plugins/cookie' Vue.use(VueRouter) ... // to: 下一个页面/即将要进入的目标 // form: 当前页面 // next:路由的控制参数,常用的有next(true)和next(false) // router.beforeEach()一般用来做一些进入页面的限制。 // 比如没有登录,就不能进入某些页面,只有登录了之后才有权限查看某些页面。。。说白了就是路由拦截。 router.beforeEach((to, from, next) => { // 先获取token let token = getToken(); // 如果有token 已登录,则可以继续访问目标地址 if (token) { next(); return; } // 未登录,访问登录页面 if (to.name === "Login") { next(); return; } // 未登录,跳转登录页面 // next(false); 保持当前所在页面,不跳转 next({name: 'Login'}); }) export default router
View Code
2.2.3 登录
需要用到发送网络请求axios
npm install axios npm install vue-axios
plugins/axios.js(把axios.js当做插件放到 plugins 再在main.js中导入进去)
import Vue from 'vue' import axios from 'axios' import VueAxios from 'vue-axios' Vue.use(VueAxios, axios)
页面中axios使用方法(看官方文档设置)
this.axios.get( "URL地址", { headers:{ .... }, params:{ ... } } ) this.axios.post( URL, {}, { headers:{ .... }, params:{ ... } } )
views/Login.vue
......... <el-form :model="userForm" :rules="rules" ref="userForm"> <el-form-item prop="username" class="row-item" :error="userFormError.username"> <el-input v-model="userForm.username" placeholder="用户名或手机号或邮箱"></el-input> </el-form-item> <el-form-item prop="password" class="row-item" :error="userFormError.password"> <el-input placeholder="请输入密码" v-model="userForm.password" show-password></el-input> </el-form-item> <el-form-item class="row-item"> <el-button type="primary" size="medium" @click="submitForm('userForm')">登 录</el-button> </el-form-item> </el-form> ........... ...... userFormError: {//表单验证的错误信息提示 username: "", password: "", }, ..... ....... submitForm(formName) { // 清除错误(在每次点击按钮 需要清除之前的错误信息提示 validateFormFailed此方法产生的提示)//submit点击执行前清空 this.clearCustomFormError() // 执行验证规则 this.$refs[formName].validate((valid) => { // 验证失败 if (!valid) { return false; } // 验证成功 通过,发送ajax请求 this.axios.post('http://127.0.0.1:8000/api/base/auth/', this.userForm).then(res => { // 发送成功后then会接收到用户登录的返回值 // res.data = {code:1000,detail:"....."} 登录失败 // res.data = {code:1000,detail:".....",username:"",token:""} 登录成功 // 字段验证 if (res.data.code === 0) { // 登录成功:将用户返回的usename和token 写入cookie(为的是页面刷新的时候可以直接写入到state中)和state(为的是其他页面可以立即拿到那个值) + 跳转首页 // 所以就得用vuex中mutations属性 this.$store.commit触发mutations // 调用了mutations里的login this.$store.commit('login', res.data.data.username, res.data.data.token); // 跳转 到“/” 重定向到首页 this.$router.push({path: "/"}) return } // code === 1000 后端对应的是字段错误 if (res.data.code === 1000) { // 字段错误 // detail = {username:['错误信息'],password:[11,22]} // 在对应输入框下展示 this.validateFormFailed(res.data.detail); } else { // 整体错误信息展示 // 验证错误 elementui里的组件 信息展示$message this.$message.error(res.data.detail); } }) }); }, // 清除错误信息 clearCustomFormError() { for (let key in this.userFormError) { this.userFormError[key] = "" } }, // 提示错误信息 validateFormFailed(errorData) { // 对res.data.detail 里的错误信息遍历 for (let fieldName in errorData) { let error = errorData[fieldName][0]; // 将错误信息复制userFormError 展示在前端 this.userFormError[fieldName] = error; } }, ........
View Code
store/index.js
import Vue from 'vue' import Vuex from 'vuex' import {getUserName,getToken,setUserToken} from "@/plugins/cookie"; Vue.use(Vuex) // vuex的使用 export default new Vuex.Store({ state: { // 这种方式不好 可以把cookie相关的东西放到一个文件里@/plugins/cookie 方便操作 // username:Vue.cookie.get("username") username:getUserName(), token:getToken(), }, getters: {}, mutations: { login:function (state,username,token){ state.username = username; state.token = token; // this.$store.commit('login', res.data.data.username, res.data.data.token);登录这里需要 // 登录成功需要将username token 写入到cookie // 需要把cookie的东西放到一个文件里方便管理 // Vue.cookie.set("username",username) // Vue.cookie.set("token",token) setUserToken(username,token) } }, actions: {}, modules: {} })
View Code
cookie.js
import Vue from 'vue' import VueCookie from 'vue-cookie' Vue.use(VueCookie) // export 导出token export const getToken = () => { return Vue.cookie.get("token"); } // export 导出username export const getUserName = () => { return Vue.cookie.get("username"); } // mutations的login需要 export const setUserToken = (username,token) => { // 设置有效期7天{expires:"70"} Vue.cookie.set("username",username); Vue.cookie.set("token",token);
View Code
ref的三种用法:
1、ref 加在普通的元素上,用this.$refs.(ref值) 获取到的是dom元素
2、ref 加在子组件上,用this.$refs.(ref值) 获取到的是组件实例,可以使用组件的所有方法。在使用方法的时候直接this.$refs.(ref值).方法() 就可以使用了。
3、如何利用 v-for 和 ref 获取一组数组或者dom 节点
浏览器的同源策略:
后端API需要解决跨域问题:可以编写
关于跨域:
- 响应头
- 复杂请求(发送2个请求)
- options预检
- post请求
- 简单请求
- post请求
https://www.cnblogs.com/wupeiqi/articles/5703697.html
BUG
mutation中的参数(只能传两个参数 第三个参数会undefind 如果想要传多个参数 就把参数打包车一个对象或字典)。
// 这种mutation获取不到token // this.$store.commit('login', res.data.data.username, res.data.data.token); this.$store.commit("login", res.data.data); import Vue from 'vue' import Vuex from 'vuex' import {getUserName, getToken, setUserToken} from "@/plugins/cookie" Vue.use(Vuex) export default new Vuex.Store({ state: { username: getUserName(), token: getToken(), }, mutations: { # {username, token}解包res.data.data login: function (state, {username, token}) { state.username = username; state.token = token; // Vue.cookie.set("username",username); // Vue.cookie.set("token",token); setUserToken(username, token); } }, actions: {}, modules: {} })
View Code
探讨2个问题:
比如访问这种界面 http://localhost:8081/task/activity/list 一定会发送ajax请求服务端获取数据,发送请求时要携带token
-
其他的页面中是不是也会需要发送请求,发送时要携带token,怎么携带?
-
1、默认值
-
2、请求拦截器
-
-
如果token过期了怎么办?【有人主动在cookie随机设置了jwt token】
-
响应拦截器,每次请求返回结果时,先执行的代码。
判断返回值的内容。
- 状态码401,内容 code==="2000",跳转到登录界面 + 清空cookie中的数据+ state中的数据
- 返回其他正确的内容,继续向后执行,正常结果的处理。
-
2.2.4 axios默认值和拦截器
ActivityCreate.vue
......... created() {// 访问组件自动加载 请求http://127.0.0.1:8000/api/base/test/ 接口 // 请求必须得携带token 要不通不过后台验证 这里axios里有两个方法(在axios里设置) // - 1、默认值 // - 2、请求拦截器 // 因为 默认值设置了 axios.defaults.baseURL = 'http://127.0.0.1:8000/api/';所以url可以只写/base/test/ // then方法请求成功逻辑 catch方法请求失败逻辑 this.axios.get("/base/test/").then(res => { console.log("请求成功", res); }).catch(reason => { console.log('请求失败', reason); // reason 看响应拦截器reject返回值是什么 return Promise.reject(response) return reason; }) } ........
View Code
-
默认值,登录成功后,以后每次发请求,都应该在请求头中携带token,每次发送请求都携带吗?
axios.js
import Vue from 'vue' import axios from 'axios' import VueAxios from 'vue-axios' import {getToken} from "@/plugins/cookie" Vue.use(VueAxios, axios) // 设置默认值 baseURL 其他页面在写url时可以省略掉'http://127.0.0.1:8000/api/' // 测试环境的ip和正式环境ip不同 通过在这里修改就可以修改请求的是测试环境还是正式环境 axios.defaults.baseURL = 'http://127.0.0.1:8000/api/'; // 设置公共common( 所有的)的请求头或单独的post put 的请求头 加上token(或者也可以把token放在请求拦截器中) // 这个token 只在页面刷新时才执行 携带token 不是每次请求都执行的 // axios.defaults.headers.common['Authorization'] = getToken(); // axios.defaults.headers.post['Content-Type'] = 'application/json'; // axios.defaults.headers.put['Content-Type'] = 'application/json';
View Code
请求拦截器,如果有人伪造token向API发送了请求,跳转到登录页面(只要认证失败,跳到登录页面)。
axios.js
-
...... // token放在默认值或请求拦截器都一样 后端都有验证 // 请求拦截器,axios发送请求时候, 都会执行以下函数内容(每次请求都会携带token) axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 const token = getToken(); if (token) { // 表示用户已登录 config就加Authorization这个头(这个是每次请求都执行的) config.headers.common['Authorization'] = token; } return config; }); ....... ...... // 响应拦截器(有两个函数 第一个是成功第二个是失败) axios.interceptors.response.use(function (response) { // API请求执行成功,响应状态码200,自动执行 return response; }, function (error) { // API请求执行失败 响应状态嘛400/500 自动执行 对响应错误做点什么 console.log(error.response); return Promise.reject(error); }); ........
View Code
axios.js
import Vue from 'vue' import axios from 'axios' import VueAxios from 'vue-axios' import {getToken} from "@/plugins/cookie" import router from '../router/index' import store from '../store/index' import {Message} from "element-ui" Vue.use(VueAxios, axios) // 设置默认值 // axios.defaults.baseURL = 'http://127.0.0.1:8000/api/'; axios.defaults.baseURL = 'http://127.0.0.1:8000/api/'; // 只在页面刷新时才执行 携带token 不是每次请求都执行的 // axios.defaults.headers.common['Authorization'] = getToken(); // axios.defaults.headers.post['Content-Type'] = 'application/json'; // axios.defaults.headers.put['Content-Type'] = 'application/json'; // 请求拦截器,axios发送请求时候,每次请求都会携带token 都会执行以下内容 axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 const token = getToken(); if (token) { // 表示用户已登录 config就加Authorization这个头(这个是每次请求都执行的) config.headers.common['Authorization'] = token; } return config; }); // 响应拦截器 axios.interceptors.response.use(function (response) { // API请求执行成功,响应状态码200,自动执行 if (response.data.code === "2000") { // store中的logout方法 store.commit("logout"); // 重定向登录页面 [Login,] // router.push({name:"Login"}); router.replace({name: "Login"}); // element-ui页面提示 Message.error("认证过期,请重新登录..."); return Promise.reject(response); // 下一个相应拦截器的第二个函数 } return response; }, function (error) { // API请求执行失败,响应状态码400/500,自动执行 if (error.response.status === 401) { // store.mutations中的logout方法 清除cookie store.commit("logout"); // 用router重定向登录页面 [Login,] // router.push({name:"Login"}); // 这个可以后退到上一步页面 router.replace({name: "Login"}); // 这个不能后退 // element-ui页面提示 Message.error("认证过期,请重新登录..."); // return // 返回值很重要 如果只写return 那就执行的是axio请求的then方法请求成功的逻辑 } return Promise.reject(error); // 下一个相应拦截器 这个是执行axio请求的错误的catch逻辑 });
View Code
小结
-
jwt是什么?与传统的token有什么不同。
-
django的中间件 vs drf的认证组件
-
vue-cookie组件:读、写、删除 + 自定义导出函数(把cookie相关功能写在一起)
-
vue-router组件:路由守卫,读取本地本地cookie => 没有就返回登录界面。
-
vuex组件:state中存储用户信息,在mutation中操作:state和cookie
-
axios组件:
-
基本发送请求
-
默认值
-
请求拦截器
-
相应拦截器
-
注意:短信登录不在此实现,可以参考我在路飞上讲的《轻量级Bug管理平台》,里面有短信登录的逻辑和处理过程。
中间件process_request
中间件顾名思义,是介于request与response处理之间的一道处理过程,相对比较轻量级,并且在全局上改变django的输入与输出。、
客户端浏览器 请求- WSGI-中间件就是类(从上到下依次执行process_request方法有就执行没有就下一个)-路由-视图(响应体返回)- 中间件(倒着从下往上依次执行process_response)-WSGI(打包好内容)-浏览器
-
process_request默认返回None,返回None,则继续执行下一个中间件的process_request;一旦返回响应体对象,则会拦截 ,直接执行process_response响应返回,不会往下执行中间件的process_request了
-
process_response必须有一个形参response,并return response;这是view函数返回的响应体,像接力棒一样传承给最后的客户端。
opptions 请求
简单请求:一次请求
非简单请求:两次请求,请求方式:OPTIONS 在发送数据之前会先发一次请求用于做“预检”,只有“预检”通过后才再发送一次请求用于数据传输。
- 请求方式:OPTIONS
- “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息
- 如何“预检”
=> 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过
Access-Control-Request-Method
=> 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过
Access-Control-Request-Headers
所以在预检的时候不要对token验证
后端:middlewares/cors.py
from django.middleware.security import SecurityMiddleware from django.utils.deprecation import MiddlewareMixin from django.shortcuts import HttpResponse # 中间件 class CorsMiddleware(MiddlewareMixin): # 有HttpResponse()会直接直接执行process_response响应返回,不会再走视图的认证组件 # 所以将此中间件按执行顺序放在settings配置文件中的最上面 就会先执行 不会再走其他中间件 def process_request(self, request): # 所以判断如果请求是复杂请求 会发送OPTIONS预检请求 直接返回HttpResponse() if request.method == "OPTIONS": return HttpResponse() # 解决跨域 def process_response(self, request, response): response["Access-Control-Allow-Origin"] = "*" response["Access-Control-Allow-Headers"] = "*" # response["Access-Control-Request-Method"] = "*" response["Access-Control-Allow-Methods"] = "*" return response
View Code
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/279120.html