Vue結合Django-Rest-Frameword結合實現登入認證(二)

小土豆biubiubiu發表於2020-10-12

作者:小土豆biubiubiu

部落格園:https://www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/2436173500265335

微信公眾號:土豆媽的碎碎念(掃碼關注,一起吸貓,一起聽故事,一起學習前端技術)

作者文章的內容均來源於自己的實踐,如果覺得有幫助到你的話,可以點贊❤️給個鼓勵或留下寶貴意見

前言

在上一篇 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結合實現登入認證(二)》

關於

作者

小土豆biubiubiu

一個努力學習的前端小菜鳥,知識是無限的。堅信只要不停下學習的腳步,總能到達自己期望的地方

同時還是一個喜歡小貓咪的人,家裡有一隻美短小母貓,名叫土豆

部落格園

https://www.cnblogs.com/HouJiao/

掘金

https://juejin.im/user/2436173500265335

微信公眾號

土豆媽的碎碎念

微信公眾號的初衷是記錄自己和身邊的一些故事,同時會不定期更新一些技術文章

歡迎大家掃碼關注,一起吸貓,一起聽故事,一起學習前端技術

作者寄語

小小總結,歡迎大家指導~

參考文章

? django-rest-framework官方文件#許可權篇
? django-rest-framework官方文件#授權認證篇

相關文章