Vue结合Django-Rest-Frameword实现登录认证(二)

3,751 阅读6分钟

作者:小土豆biubiubiu
博客园:www.cnblogs.com/HouJiao/
掘金:juejin.im/user/243617…

前言

在上一篇 Vue结合Django-Rest-Frameword结合实现登录认证(一) 文章中,我们利用token实现了一个非常基础的用户登录认证功能。

那这一节需要对前面实现的内容进行优化: 1. 优化axios:请求封装、认证信息的封装 2. 注销
3. 设置token过期时间

优化axios

axios的优化就是对axios进行一个封装,单独抽离出来一个模块,负责编写请求的API,在组件中只需要调用这个API传入对应的参数,就能在请求发送的同时实现认证信息的设置

// 代码位置:/src/utils/request.js
/*
 * @Description: 封装axios请求 axios官网:http://www.axios-js.com/zh-cn/
 * @version: 1.0.0
 * @Author: houjiaojiao
 * @Date: 2020-07-23 16:32:19
 * @LastEditors: houjiaojiao
 * @LastEditTime: 2020-09-01 17:30:46
 */ 
import axios from 'axios'

// 新建一个 axios 实例
let instance = axios.create({
  baseURL: '/api/cert/',
});
// 请求拦截器
instance.interceptors.request.use(
  // 在发送请求前做一些事情
  request => {
    // 在发送请求前给每个请求头带上Authorization字段
    const auth = 'Token ' + localStorage.getItem('token');
    request.headers.Authorization
    return request;
  },
  // 请求出现错误做一些事情
  error => {
    console.log('There are some problems with this request');
    console.log(error);
    return Promise.reject(error);
  }
)

//响应拦截器
instance.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    return Promise.reject(error);
  }
)

// 封装get请求
export function get(url, params){
  return new Promise((resolve, reject) => {
      instance.get(url, {
          params
        })
        .then(response => {
          resolve(response);
        }).catch(error => {
          reject(error)
        })
    
  })
}

// 封装post请求
export function post(url, params){
  return new Promise((resolve, reject) => {
    instance.post(url, params)
      .then(response => {
        resolve(response)
      }).catch(error => {
        reject(error)
      })
  })
}

可以看到,我们对axiosgetpost请求进行了封装,同时我们将认证需要添加到请求头部Authorization字段定义在了axios请求拦截器中,这样每一个请求都会携带这个头部字段

接着我们在对请求的API做一个封装,以登录为例。

// 代码位置:/src/api/login.js
import {get, post} from '@/utils/request.js'

export const login = (loginForm) => post('userAuth/login', loginForm)

然后我们在登录组件中调用这个API发起请求。

// 引入前面封装好的API接口
import {login} from '@/api/login.js'

export default {
    name: 'Login',
    data() {
        return {
            loginForm: {
                username: '',
                password: '',
            }
        }
    },
    methods: {
        login: function(){
            // 直接调用API接口
            login(this.loginForm).then(res => {
                const {result, detail, errorInfo}  = res.data;
                if(result == true){
                    // 登录成功 设置token
                    localStorage.setItem('token', detail.token);
                    // 跳转页面
                    this.$router.push('/certMake');
                }else{
                    this.$message({
                        showClose: true,
                        message: errorInfo,
                        type: 'error'
                    });
                }
           })
        }
    }
}

以上省略登录组件中template中的代码

最后在登录界面输入用户名密码,就可以正常登陆了。

之后我们在浏览器中点击其他的页面,会发现每个发出的请求头部都携带了Authorization字段。

注销

当用户点击注销时,我们应该做的就是清除本地保存的token

logout: function(){
  // 清除token
  localStorage.removeItem("token");
  // 跳转至登录页  登录页面在router.js中的配置的path就是‘/’
  this.$router.push("/");
}

清除以后呢,如果我们直接在浏览器中手动输入url进入某个页面,就可以看到响应出现401

此时用户只有再次进入登录页面进行登录,才能正常访问页面。

那对于上面注销之后返回的401,实际上比较合理的结果应该是直接跳转到登录页。因此我们还需要在发起请求前对token进行一个判断,如果没有token存在,则直接跳转至登录页。

上面描述的功能使用 守卫导航 实现
代码定义在router.js

// 给路由定义前置的全局守卫
router.beforeEach((to, from, next) => {
    let token = localStorage.getItem('token');
    if(token){
        // token存在 访问login 跳转至产品证书制作页面
        if(to.path == '/' || to.path  == '/login'){
            next('/certMake');
        }else{
            next();
        }
    }else{
    	// token不存在  路径'/'就是登录页面设置的path
        if(to.path === '/'){
            next();
        }else{
            next('/')
        }
    }

})

设置Token有效期

前面我们完成的登录功能,除了注销后需要登录,其他任何时候只要用户成功登录过一次,就不需要在此登录了。这样存在一个很大的安全隐患,那就是当用户的token不慎泄露后,别人是可以没有限制的操作我们的页面。

因此最好的办法就是给token设置一个有效期,当有效期到了以后,强制用户退出登录,在下一次登录的时候重新生成新的token

那接下来就是这个功能的代码实现了。

后端配置token有效期

后端在userAuth模块下新建一个auth.py,自定义一个用户认证类,继承TokenAuthentication,并且实现token过期的处理。

# -*- coding: utf-8 -*-
# Create your views here.

from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from django.utils import timezone
from datetime import timedelta
from django.conf import settings

# token过期时间处理
class ExpiringTokenAuthentication(TokenAuthentication):
    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token.')
        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted.')
        # 重点就在这句了,这里做了一个Token过期的验证
        # 如果当前的时间大于Token创建时间+DAYS天,那么就返回Token已经过期
        if timezone.now() > (token.created + timedelta(days=7)):  
            print "Token has expired"
            # 过期以后 响应为401
            raise exceptions.AuthenticationFailed('Token has expired')
        return (token.user, token)

这里设置token的有效期是7天

接着修改setting中配置的全局认证方案为我们自定义的用户认证ExpiringTokenAuthentication

# 设置全局身份认证方案
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'userAuth.auth.ExpiringTokenAuthentication', # token过期时间
        # 'rest_framework.authentication.TokenAuthentication',  # token认证
    )
}

接着我们在userAuth模块的views.py中定义退出登录的逻辑:退出登录时删除数据库中的token

@api_view(['GET'])
@permission_classes((AllowAny,))
@authentication_classes(())
def logout(request):
    """退出登录"""
    result = True
    errorInfo = u''
    detail = {}
    token = ''
    authInfo = request.META.get('HTTP_AUTHORIZATION')
    if authInfo:
        token = authInfo.split(' ')[1]
    try:
        # 退出登录 删除token
        tokenObj = Token.objects.get(key=token)
        tokenObj.delete()
    except Exception as e:
        traceback.print_exc(e)
        print 'token not exist'
        result = False
        errorInfo = u'退出登录失败'
    return Response({"result": result, "detail": {}, "errorInfo": errorInfo})

前端设置

token过期以后,后端会返回401,因此我们需要在响应拦截器中处理这个401,即当后端响应为401时,就弹框提示用户登录过期,强制用户退出登录。

//响应拦截器
instance.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    // 在这里处理一下token过期的逻辑
    // 后端验证token过期以后 会返回401 
    if(error.response.status == 401){
      MessageBox.confirm('登录过期,请重新登录', '确定登出', {
        confirmButtonText: '重新登录'
        type: 'warning'
      }).then(() => {
        // 调用接口退出登录
        get('/userAuth/logout').then( response => {
          // 移除本地缓存的token
          localStorage.removeItem("token");
          location.reload();
        })
      })
    }
    return Promise.reject(error);
  }
)

结果演示

到此前后端的逻辑就完成了,我们来演示一下最后的结果。

首先我们先看一下数据库中已有的token的创建时间。

可以看到数据库中已有的token的创建时间是2020-09-17,现在的时间是2020-10-10号,已经超出token的有效期。

前面设置token的有效期是7

然后我们刷新一下页面。

发现已经成功弹出强制用户重新登录

当我们点击重新登录按钮后,就会请求后端的logout接口,数据库中已有的token会被删除,删除成功之后本地缓存在localStorage中的token也会被删除,最后会跳转到产品的登录页面。

数据库中的token已经被删除

接着在登录页面输入用户名密码重新登录,就会发现数据库中的token已经更新。

最后

关于 《vue结合Django-Rest-Frameword结合实现登录认证》这个系列的文章就结束了。

文章基本都是实战操作,希望可以给大家一个参考。

文章索引

《Vue结合Django-Rest-Frameword实现登录认证(一)》
《Vue结合Django-Rest-Frameword实现登录认证(二)》

参考文章

👉 django-rest-framework官方文档#权限篇
👉 django-rest-framework官方文档#授权认证篇