媒体宝项目–认证


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

使用输出命令进行调试

  1. console.log();

  2. 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(打包好内容)-浏览器

  1. process_request默认返回None,返回None,则继续执行下一个中间件的process_request;一旦返回响应体对象,则会拦截 ,直接执行process_response响应返回,不会往下执行中间件的process_request了

  2. 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

(0)
上一篇 2022年8月5日 21:07
下一篇 2022年8月6日 01:47

相关推荐

发表回复

登录后才能评论