标签 vue 下的文章

作用

  1. 后端提供restful api接口
  2. 前端提供页面展示


而前端日常开发时,大致有以下几种开发模式(vue2.0版本)

  1. 使用npm run dev模式,且通过配置proxyTable,将axios的前端请求转发给实际的后端地址(常用


    1. 项目根下的config目录


      • index.js

      'use strict'
      // Template version: 1.3.1
      // see http://vuejs-templates.github.io/webpack for documentation.
      
      const path = require('path')
      
      module.exports = {
        dev: {
          // Paths
          assetsSubDirectory: 'static',
          assetsPublicPath: '/',
          proxyTable: {
            '/api': {
              target: 'http://127.0.0.1:8080', // 后端接口地址
              changeOrigin: true, // 是否允许跨域
              pathRewrite: {
                '^/api': '' // 重写
              }
            }
          },
      
          // Various Dev Server settings
          host: 'localhost', // can be overwritten by process.env.HOST
          port: 80, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
          autoOpenBrowser: true,
          errorOverlay: true,
          notifyOnErrors: true,
          poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
      
          // Use Eslint Loader?
          // If true, your code will be linted during bundling and
          // linting errors and warnings will be shown in the console.
          useEslint: true,
          // If true, eslint errors and warnings will also be shown in the error overlay
          // in the browser.
          showEslintErrorsInOverlay: false,
      
          /**
           * Source Maps
           */
      
          // https://webpack.js.org/configuration/devtool/#development
          devtool: 'cheap-module-eval-source-map',
      
          // If you have problems debugging vue-files in devtools,
          // set this to false - it *may* help
          // https://vue-loader.vuejs.org/en/options.html#cachebusting
          cacheBusting: true,
      
          cssSourceMap: true
        },
      
        build: {
          // Template for index.html
          index: path.resolve(__dirname, '../dist/index.html'),
      
          // Paths
          assetsRoot: path.resolve(__dirname, '../dist'),
          assetsSubDirectory: 'static',
          assetsPublicPath: '/',
      
          /**
           * Source Maps
           */
      
          productionSourceMap: false,
          // https://webpack.js.org/configuration/devtool/#production
          devtool: '#source-map',
      
          // Gzip off by default as many popular static hosts such as
          // Surge or Netlify already gzip all static assets for you.
          // Before setting to true, make sure to:
          // npm install --save-dev compression-webpack-plugin
          productionGzip: true,
          productionGzipExtensions: ['js', 'css'],
      
          // Run the build command with an extra argument to
          // View the bundle analyzer report after build finishes:
          // npm run build --report
          // Set to true or false to always turn it on or off
          bundleAnalyzerReport: process.env.npm_config_report
        }
      }
      

      • dev.env.js

      'use strict'
      const merge = require('webpack-merge')
      const prodEnv = require('./prod.env')
      
      module.exports = merge(prodEnv, {
        NODE_ENV: '"development"',
        BASE_API: '"/api"'
      })
      

      • prod.env.js

      'use strict'
      module.exports = {
        NODE_ENV: '"production"',
        // BASE_API: '"http://georege.slhk.com/"'
      }
      
    2. 项目根下的src目录下自己封装api目录


      • axios-filter.js

      import axios from 'axios'
      import { Message } from 'element-ui'
      import router from '@/router'
      
      // 根据不同环境自动读取不同的baseurl
      if (process.env.NODE_ENV === 'development') {
        axios.defaults.baseURL = process.env.BASE_API
      }
      // 读取cookie中的csrftoken
      axios.defaults.xsrfCookieName = 'csrftoken'
      // axios header设置X-CSRFTOKEN
      axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'
      // 设置Content-Type
      axios.defaults.headers = {
        'Content-Type': 'application/json;charset=UTF-8'
      }
      // 设置超时时间
      axios.defaults.timeout = 50000
      
      // request拦截器
      axios.interceptors.request.use(
        config => {
          // sessionStorage 如果存在jwt token
          if (sessionStorage.token) {
            // 则JWT token认证格式,写入header
            config.headers.Authorization = 'JWt ' + sessionStorage.token
            // console.log(sessionStorage.token)
          }
          return config
        },
        error => {
          // console.log(error)
          return Promise.reject(error)
        }
      )
      // reponse自定义拦截器 start
      function customAxiosResponseInterceptor () {
        const interceptor = axios.interceptors.response.use(
          response => response,
          error => {
            // console.log(error)
            var config = error.config
            if (error.response.status === 401) {
              axios.interceptors.response.eject(interceptor)
              const v = sessionStorage.getItem('refresh')
      
              if (typeof v === 'undefined' || v === null || v === '') {
                Message({
                  showClose: true,
                  message: error.response.data.msg,
                  type: 'error'
                })
              } else {
                let data = { 'refresh': v }
                return axios.post('/api-token-refresh/', data).then(response => {
                  sessionStorage.setItem('token', response.data.access)
                  console.log('刷新 access token 成功')
                  // 重新发送请求
                  return axios(config)
                }).catch(error => {
                  Message({
                    showClose: true,
                    message: '登录超时,请重新登录!!!',
                    type: 'error'
                  })
                  redirectLoginWithQuery()
                  console.log(error)
                  return Promise.reject(error)
                }).finally(customAxiosResponseInterceptor)
              }
            } else {
              console.log(error.response)
              // Message({
              //   showClose: true,
              //   message: error.response.data.msg,
              //   type: 'error'
              // })
              return Promise.reject(error)
            }
          }
        )
      }
      customAxiosResponseInterceptor()
      // reponse自定义拦截器 end
      // login with redirect
      function redirectLoginWithQuery () {
        router.push({
          path: '/login',
          query: {
            redirect: router.currentRoute.fullPath
          }
        })
      }
      export default axios
      
    3. 项目目录结构

  2. 使用npm run build模式,直接dist下打包编译出生产所需静态文件

  3. 使用npm run watch模式(需要安装watch组件,且需要配置package.json文件中的scripts的内容)


    • package.json

      "scripts": {
          "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
          "start": "npm run dev",
          "lint": "eslint --ext .js,.vue src",
          "build": "node build/build.js",
          "watch": "nodemon --watch src -e html,vue,js,less build/build.js"
        },

simplejwt token前后端验证详解

  1. 前后端分离场景:

    django

    djangorestframework

    djangorestframework-simplejwt

    element-ui

  2. 范例说明:

    后端部分代码:

    • serializers.py

      from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
      
      # Custom Token ObtainPairSerializer
      class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
          '''
          自定义jwt认证返回值
          '''
          def validate(self, attrs):
              data = super().validate(attrs)
              refresh = self.get_token(self.user)
              data['refresh'] = str(refresh)
              data['access'] = str(refresh.access_token)
              data['user_id'] = self.user.id
              data['user_name'] = self.user.username
              return data
      
    • views.py

      from backend.serializers import *
      from rest_framework_simplejwt.views import TokenObtainPairView
      
      # custom token view
      class CustomTokenObtainPairView(TokenObtainPairView):
          serializer_class = CustomTokenObtainPairSerializer
      
    • urls.py

      from django.contrib import admin
      from django.views.generic import TemplateView
      from django.urls import path, include
      from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView
      from rest_framework.routers import DefaultRouter
      from backend import views
      
      router = DefaultRouter()
      router.register(r'users', views.UserViewSet)
      router.register(r'groups', views.GroupViewSet)
      
      # 使用自动url路由连接我们的API
      # 另外,我们还包括支持浏览器浏览API的登录URL
      app_name = 'backend'
      urlpatterns = [
          path('', TemplateView.as_view(template_name='index.html'), name='index'),
          path('admin/', admin.site.urls),  # django 管理员视图
          path('alterpassword/', views.AlterPassword.as_view()),
          path('search/', views.GlobalSearchAPIView.as_view(), name="search"),
          path('api/<version>/', include(router.urls)),  # 自定义rest api
          path('api-token-auth/', views.CustomTokenObtainPairView.as_view(),
               name='custom_token_obtain_pair'),  # rest_framework_simplejwt 生成
          path('api-token-refresh/', TokenRefreshView.as_view(),
               name='token_refresh'),  # rest_framework_simplejwt 刷新
          path('api-token-verify/', TokenVerifyView.as_view(),
               name='token_verify'),  # rest_framework_simplejwt 验证
          path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),  # rest framework api 可视化
      ]

    前端部分代码:

    • axios-filter.js(拦截器)

      import axios from 'axios'
      import { Message } from 'element-ui'
      import router from '@/router'
      
      // 读取cookie中的csrftoken
      axios.defaults.xsrfCookieName = 'csrftoken'
      // axios header设置X-CSRFTOKEN
      axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'
      // 设置Content-Type
      axios.defaults.headers = {
        'Content-Type': 'application/json;charset=UTF-8'
      }
      // 设置超时时间
      axios.defaults.timeout = 50000
      
      // request拦截器
      axios.interceptors.request.use(
        config => {
          // sessionStorage 如果存在jwt token
          if (sessionStorage.token) {
            // 则JWT token认证格式,写入header
            config.headers.Authorization = 'JWt ' + sessionStorage.token
            // console.log(sessionStorage.token)
          }
          return config
        },
        error => {
          // console.log(error)
          return Promise.reject(error)
        }
      )
      // reponse自定义拦截器 start
      function customAxiosResponseInterceptor () {
        const interceptor = axios.interceptors.response.use(
          response => response,
          error => {
            // console.log(error)
            var config = error.config
            if (error.response.status === 401) {
              axios.interceptors.response.eject(interceptor)
              let data = { 'refresh': sessionStorage.getItem('refresh') }
              return axios.post('/api-token-refresh/', data).then(response => {
                sessionStorage.setItem('token', response.data.access)
                console.log('refresh access token success')
                // 重新发送请求
                return axios(config)
              }).catch(error => {
                Message({
                  showClose: true,
                  message: '登录超时,请重新登录!!!',
                  type: 'error'
                })
                redirectLoginWithQuery()
                console.log(error)
                return Promise.reject(error)
              }).finally(customAxiosResponseInterceptor)
            } else {
              Message({
                showClose: true,
                message: error.response.data,
                type: 'error'
              })
              return Promise.reject(error)
            }
          }
        )
      }
      customAxiosResponseInterceptor()
      // reponse自定义拦截器 end
      // login with redirect
      function redirectLoginWithQuery () {
        router.push({
          path: '/login',
          query: {
            redirect: router.currentRoute.fullPath
          }
        })
      }
      export default axios
      
    • login.vue

      <template>
        <el-form
          @keyup.enter.native="submitForm('loginForm')"
          :model="loginForm"
          status-icon
          :rules="rules"
          ref="loginForm"
          label-width="55px"
          class="login-container"
        >
          <h3 class="title">系统登录</h3>
          <el-form-item label="用户名" prop="name">
            <el-input type="text" v-model="loginForm.name" autocomplete="off" placeholder="用户名"></el-input>
          </el-form-item>
          <el-form-item label="密码" prop="pass">
            <label slot="label">密&nbsp;&nbsp;&nbsp;&nbsp;码</label>
            <el-input type="password" v-model="loginForm.pass" autocomplete="off" placeholder="密码"></el-input>
          </el-form-item>
          <el-checkbox v-model="checked" class="remember">记住密码</el-checkbox>
          <el-form-item>
            <el-button
              ref="button_login"
              v-loding="loading"
              type="primary"
              @click="submitForm('loginForm')"
            >登录</el-button>
            <el-button @click="resetForm('loginForm')">重置</el-button>
          </el-form-item>
        </el-form>
      </template>
      
      <script>
      export default {
        data () {
          var validateName = (rule, value, callback) => {
            if (!value) {
              callback(new Error('请输入用户名'))
            } else {
              callback()
            }
          }
          var validatePass = (rule, value, callback) => {
            if (!value) {
              callback(new Error('请输入密码'))
            } else {
              callback()
            }
          }
          return {
            checked: false,
            loginForm: {
              name: '',
              pass: ''
            },
            rules: {
              name: [
                { validator: validateName, trigger: 'blur' }
              ],
              pass: [
                { validator: validatePass, trigger: 'blur' }
              ]
            },
            loading: false
          }
        },
        methods: {
          submitForm (formName) {
            this.$refs[formName].validate((valid) => {
              if (valid) {
                var that = this
                this.$refs.button_login.loading = true
                this.$axios.request(
                  // 发送axios请求
                  {
                    url: '/api-token-auth/', // 请求路径
                    method: 'POST', // 请求方式
                    data: {
                      // 要发送的数据
                      username: this.loginForm.name,
                      password: this.loginForm.pass
                    },
                    responseType: 'json' // 期望返回的类型是json格式
                  }
                ).then(response => {
                  // 把返回的结果交给回调函数处理
                  console.log(response)
                  sessionStorage.setItem('token', response.data.access)
                  sessionStorage.setItem('refresh', response.data.refresh)
                  sessionStorage.setItem('user_id', response.data.user_id)
                  sessionStorage.setItem('user_name', response.data.user_name)
                  this.$message.success('登录成功')
                  let redirect = decodeURIComponent(this.$route.query.redirect || '/')
                  that.$router.push({ path: redirect })
                  // console.log(sessionStorage)
                }).catch(error => {
                  this.$message.error(error)
                  console.log(error)
                  this.$refs.button_login.loading = false
                })
              } else {
                console.log('请输入合法用户名和密码')
                this.$message.error('请输入合法用户名和密码')
                return false
              }
            })
          },
          resetForm (formName) {
            this.$refs[formName].resetFields()
          },
          keyupEnter () {
            const that = this
            if (window.event.keyCode === 13) {
              that.submitForm('loginForm')
            }
          }
        },
        mounted () {
          window.addEventListener('keyup', this.keyupEnter, false)
        },
        beforeDestroy () {
          window.removeEventListener('keyup', this.keyupEnter, false)
        }
      }
      </script>
      
      <style lang="scss" scoped>
      .login-container {
        -webkit-border-radius: 5px;
        border-radius: 5px;
        -moz-border-radius: 5px;
        background-clip: padding-box;
        margin: 180px auto;
        width: 350px;
        padding: 35px 35px 15px 35px;
        background: #fff;
        border: 1px solid #eaeaea;
        box-shadow: 0 0 25px #cac6c6;
        .title {
          margin: 0px auto 40px auto;
          text-align: center;
          color: #505458;
        }
        .remember {
          margin: 0px 0px 35px 0px;
        }
      }
      </style>
      
  1. token相关接口使用说明

    方法功能说明特点
    TokenObtainPairView用户第一次登录,返回access和refresh的两个tokenaccess的token应该设置较短有效时间,refresh的token应该设置较长的合理有效时间
    TokenRefreshView传递有效时间内的refresh-token,接口返回有效时间的新access-tokenrefresh-token过期返回的状态码与access-token过期返回的状态码都是401(vue拦截器对此处理需要一点小技巧,请参考axios-filter.js)
    TokenVerifyView验证提交的token,返回当前token状态信息
  2. settings.py中simplejwt相关设置参数

    SIMPLE_JWT = {
        'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1),
        'REFRESH_TOKEN_LIFETIME': timedelta(minutes=2),
        'ROTATE_REFRESH_TOKENS': False,
        'BLACKLIST_AFTER_ROTATION': True,
        'UPDATE_LAST_LOGIN': False,
    
        'ALGORITHM': 'HS256',
        'SIGNING_KEY': SECRET_KEY,
        'VERIFYING_KEY': None,
        'AUDIENCE': None,
        'ISSUER': None,
    
        'AUTH_HEADER_TYPES': ('JWt',),
        'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
        'USER_ID_FIELD': 'id',
        'USER_ID_CLAIM': 'user_id',
        'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
    
        'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
        'TOKEN_TYPE_CLAIM': 'token_type',
    
        'JTI_CLAIM': 'jti',
    
        'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
        'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
        'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
    }

    完整参数请参考:simplejwt完整参数项

  3. 实际应用中:
    verify_jwt_token调用的频率可能比较少,
    如果对安全性没太大要求,可以只使用access的token,不启用refresh的token,access的token超时后,跳转登录页即可。
    当用户成功验证而应用程序不更新cookie时,这个时候就存在会话固定漏洞,攻击者可利用此漏洞发起会话劫持
    所以建议设置——ACCESS_TOKEN_LIFETIME为较短时间,设置REFRESH_TOKEN_LIFETIME为合适的较长时间,
    即——access为较短的有效期,refresh设置为合理的较长有效期,
    前端不断携带refresh token刷新接口,获取新的access token来进行业务交互。
  4. 简述一下djangorestframework-jwt和djangorestframework-simplejwt的区别

    1. 关于djangorestframework-jwt的模块,在使用obtain_jwt_token方法时,只会返回一个token,且该token只能在固定有效期时间内调用refresh_jwt_token,获取新的token
      (新的token有效期等于前面所有token有效期减去已消耗的时间,也就是t1+t2+t3+..........=t1(如果没有刷新token)=JWT_EXPIRATION_DELTA;

      Refresh with tokens can be repeated (token1 -> token2 -> token3), but this chain of token stores the time that the original token (obtained with username/password credentials), as orig_iat. You can only keep refreshing tokens up to JWT_REFRESH_EXPIRATION_DELTA.)

    2. 而如果使用djangorestframework-simplejwt模块,在使用TokenObtainPairView方法时,会直接返回两个(access和refresh)token,且设置不同有效期,携带refresh token访问TokenRefreshView,才能返回新的固定有效期的access token
  5. 参考链接:

    https://jpadilla.github.io/django-rest-framework-jwt
    https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
    https://simpleisbetterthancomplex.com/tutorial/2018/12/19/how-to-use-jwt-authentication-with-django-rest-framework.html
    https://www.remoteinning.com/blog/how-to-use-jwt-authentication-with-django-rest-framework
    https://stackoverflow.com/questions/51646853/automating-access-token-refreshing-via-interceptors-in-axios

原代码(同一个函数内,多次调用this.$message,弹窗内容会重叠):

submitForm (formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$refs.button_submit.loading = true
          this.$axios.request(
            // 发送axios请求
            {
              url: '/alterpassword/', // 请求路径
              method: 'put', // 请求方式
              data: {
                // 要发送的数据
                old_password: this.alterpasswdForm.oldpass,
                new_password: this.alterpasswdForm.newpass
              },
              responseType: 'json' // 期望返回的类型是json格式
            }
          ).then(function (response) {
            that.$refs.button_submit.loading = false
            // 把返回的结果交给回调函数处理
            console.log(response.data)
            if (response.data['code'] === 200) {
              this.$message.success(response.data)
              this.$message.info('请重新登录')
              this.isVisible = false
              sessionStorage.removeItem('token')
              sessionStorage.clear()
              this.$router.push('/login')
            } else {
              that.$message({ type: 'warning', message: response.data })
            }
          }).catch(error => {
            console.log(error)
            this.$message({ type: 'warning', message: error })
            this.$refs.button_submit.loading = false
          })
        } else {
          console.log('请输入合法用户名和密码')
          return false
        }
      })
    },

使用async、await实现不重叠:

submitForm (formName) {
      var that = this
      this.$refs[formName].validate((valid) => {
        if (valid) {
          this.$refs.button_submit.loading = true
          this.$axios.request(
            // 发送axios请求
            {
              url: '/alterpassword/', // 请求路径
              method: 'put', // 请求方式
              data: {
                // 要发送的数据
                old_password: this.alterpasswdForm.oldpass,
                new_password: this.alterpasswdForm.newpass
              },
              responseType: 'json' // 期望返回的类型是json格式
            }
          ).then(async function (response) {
            that.$refs.button_submit.loading = false
            // 把返回的结果交给回调函数处理
            console.log(response.data)
            if (response.data['code'] === 200) {
              await that.$message.success(response.data)
              await that.$message.info('请重新登录')
              that.isVisible = false
              sessionStorage.removeItem('token')
              sessionStorage.clear()
              that.$router.push('/login')
            } else {
              that.$message({ type: 'warning', message: response.data })
            }
          }).catch(error => {
            console.log(error)
            this.$message({ type: 'warning', message: error })
            this.$refs.button_submit.loading = false
          })
        } else {
          console.log('请输入合法用户名和密码')
          return false
        }
      })
    },

  1. vscode安装vetur插件
  2. 自定义vetur设置,文件——首选项——设置——扩展——Vetur——打开设置(右上角是个小图标)
  3. 输入以下内容保存:
    "vetur.format.defaultFormatter.js": "vscode-typescript",
    "javascript.format.insertSpaceBeforeFunctionParenthesis": true,
    "vetur.format.defaultFormatter.ts": "prettier-tslint",
    "vetur.format.defaultFormatter.html": "js-beautify-html",
    "vetur.format.defaultFormatterOptions": {
        "prettier": {
            "semi": false,
            "singleQuote": true,
        },
    },

        然后重新格式化vue文件试试,npm run build还报不报警告信息