day77:luffy:導航欄的實現&DjangoRestFramework JWT&多條件登入

Poke發表於2020-10-29

目錄

1.導航欄的實現

2.登入前戲:使用者表初始化

3.DjangoRestFramework JWT

4.多條件登入

5.登入狀態的判斷和退出登入

1.導航欄的實現

1.設計導航欄的model模型類

apps/home/models.py

class Nav(BaseModel):
    """導航選單模型"""
    POSITION_OPTION = (
        (1, "頂部導航"),
        (2, "腳部導航"),
    )
    title = models.CharField(max_length=500, verbose_name="導航標題")
    link = models.CharField(max_length=500, verbose_name="導航連結")
    position = models.IntegerField(choices=POSITION_OPTION, default=1, verbose_name="導航位置")
    is_site = models.BooleanField(default=False, verbose_name="是否是站外地址")

    class Meta:
        db_table = 'ly_nav'
        verbose_name = '導航選單'
        verbose_name_plural = verbose_name

    # 自定義方法[自定義欄位或者自定義工具方法]
    def __str__(self):
        return self.title

2.在Xadmin中註冊導航欄模型類

apps/home/adminx.py

# 導航選單
class NavModelAdmin(object):
    list_display=["title","link","is_show","is_site","position"]
xadmin.site.register(models.Nav, NavModelAdmin)

執行資料庫遷移同步指令

python manage.py makemigrations
python manage.py migrate

3.在Xadmin中新增一些導航欄資料

 4.註冊導航欄的URL

from django.urls import path,re_path
from . import views
urlpatterns = [
    ......
    path("nav/header/", views.HeaderNavListAPIView.as_view()),
    
]

5.新建導航欄的檢視類

from .models import Nav
from .serializers import NavModelSerializer
class HeaderNavListAPIView(ListAPIView):
    """頂部導航選單檢視"""
    queryset = Nav.objects.filter(is_show=True, is_deleted=False,position=1).order_by("-orders","-id")[:constants.HEADER_NAV_LENGTH]
    serializer_class = NavModelSerializer

'''
position=1代表是頂部導航,position=2代表是底部導航
'''

6.新建導航欄的序列化器

from .models import Nav
class NavModelSerializer(serializers.ModelSerializer):
    """導航選單序列化器"""
    class Meta:
        model = Nav
        fields = ["id","title","link","is_site"]

7.除錯一下看是否能獲取到資料

8.編寫導航欄vue元件的程式碼

1.Vheader元件載入時,通過axios的get請求去請求後端的資料

<script>
    export default {
      name: "Header",
      data(){
        return {
         ......
          nav_data_list:[],
        }
      },
      methods:{
        ......
        get_nav_data(){
          this.$axios.get(`${this.$settings.Host}/home/nav/header/`,).then((res)=>{
            this.nav_data_list = res.data;
          })
          .catch((error)=>{
            console.log(error);
          })
        }
      },
      
      // 注意:一定要寫created方法來觸發函式
      created() {
        this.get_nav_data();
      }

    }
    
</script>

2.後端資料已經拿到,接下來要在前端展示出來

思路:for迴圈取出導航欄所有的資料,判斷導航欄的標題是站內跳轉還是站外跳轉

如果是站內跳轉就用router-link,如果是站外跳轉就用a href

value就是你從後端獲取到的資料 value.link value.is_site value.title就可以取到相應的值

 <el-col class="nav" :span="10">
            <el-row>
                <el-col :span="3" v-for="(value,index) in nav_data_list" :key="index">

                 <a :href="value.link" class="active" v-if="value.is_site">{{value.title}}</a>

                  <router-link :to="value.link" v-else>{{value.title}}</router-link>

                </el-col>

              </el-row>

          </el-col>

2.登入前戲:使用者表初始化

1.Login.vue

<template>
    <div class="login box">
        <img src="../../static/img/Loginbg.3377d0c.jpg" alt="">
        <div class="login">
            <div class="login-title">
                <img src="../../static/img/Logotitle.1ba5466.png" alt="">
                <p>幫助有志向的年輕人通過努力學習獲得體面的工作和生活!</p>
            </div>
            <div class="login_box">
                <div class="title">
                    <span @click="login_type=0">密碼登入</span>
                    <span @click="login_type=1">簡訊登入</span>
                </div>
                <div class="inp" v-if="login_type==0">
                    <input v-model = "username" type="text" placeholder="使用者名稱 / 手機號碼" class="user">
                    <input v-model = "password" type="password" name="" class="pwd" placeholder="密碼">
                    <div id="geetest1"></div>
                    <div class="rember">
                        <p>
                            <input type="checkbox" class="no" name="a" v-model="remember"/>
                            <span>記住密碼</span>
                        </p>
                        <p>忘記密碼</p>
                    </div>
                    <button class="login_btn" @click="loginHandle">登入</button>
                    <p class="go_login" >沒有賬號 <span>立即註冊</span></p>
                </div>
                <div class="inp" v-show="login_type==1">
                    <input v-model = "username" type="text" placeholder="手機號碼" class="user">
                    <input v-model = "password"  type="text" class="pwd" placeholder="簡訊驗證碼">
          <button id="get_code">獲取驗證碼</button>
                    <button class="login_btn">登入</button>
                    <p class="go_login" >沒有賬號 <span>立即註冊</span></p>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
  name: 'Login',
  data(){
    return {
        login_type: 0,
        username:"",
        password:"",
        remember:false,
    }
  },

  methods:{
    loginHandle(){
      this.$axios.post(`${this.$settings.Host}/users/login/`,{
        username:this.username,
        password:this.password,

      }).then((res)=>{
        console.log(res);
        // console.log(this.remember);
        if (this.remember){
          localStorage.token = res.data.token;
          localStorage.username = res.data.username;
          localStorage.id = res.data.id;
          sessionStorage.removeItem('token');
          sessionStorage.removeItem('username');
          sessionStorage.removeItem('id');

        }else {
          sessionStorage.token = res.data.token;
          sessionStorage.username = res.data.username;
          sessionStorage.id = res.data.id;
          localStorage.removeItem('token');
          localStorage.removeItem('username');
          localStorage.removeItem('id');
        }
        this.$router.push('/');

      }).catch((error)=>{
        this.$alert('使用者名稱password error', 'error msg', {
          confirmButtonText: '確定',

        });
      })

    }

  },

};
</script>

<style scoped>
.box{
    width: 100%;
  height: 100%;
    position: relative;
  overflow: hidden;
}
.box img{
    width: 100%;
  min-height: 100%;
}
.box .login {
    position: absolute;
    width: 500px;
    height: 400px;
    top: 0;
    left: 0;
  margin: auto;
  right: 0;
  bottom: 0;
  top: -338px;
}
.login .login-title{
     width: 100%;
    text-align: center;
}
.login-title img{
    width: 190px;
    height: auto;
}
.login-title p{
    font-family: PingFangSC-Regular;
    font-size: 18px;
    color: #fff;
    letter-spacing: .29px;
    padding-top: 10px;
    padding-bottom: 50px;
}
.login_box{
    width: 400px;
    height: auto;
    background: #fff;
    box-shadow: 0 2px 4px 0 rgba(0,0,0,.5);
    border-radius: 4px;
    margin: 0 auto;
    padding-bottom: 40px;
}
.login_box .title{
    font-size: 20px;
    color: #9b9b9b;
    letter-spacing: .32px;
    border-bottom: 1px solid #e6e6e6;
     display: flex;
        justify-content: space-around;
        padding: 50px 60px 0 60px;
        margin-bottom: 20px;
        cursor: pointer;
}
.login_box .title span:nth-of-type(1){
    color: #4a4a4a;
        border-bottom: 2px solid #84cc39;
}

.inp{
    width: 350px;
    margin: 0 auto;
}
.inp input{
    border: 0;
    outline: 0;
    width: 100%;
    height: 45px;
    border-radius: 4px;
    border: 1px solid #d9d9d9;
    text-indent: 20px;
    font-size: 14px;
    background: #fff !important;
}
.inp input.user{
    margin-bottom: 16px;
}
.inp .rember{
     display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    margin-top: 10px;
}
.inp .rember p:first-of-type{
    font-size: 12px;
    color: #4a4a4a;
    letter-spacing: .19px;
    margin-left: 22px;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    /*position: relative;*/
}
.inp .rember p:nth-of-type(2){
    font-size: 14px;
    color: #9b9b9b;
    letter-spacing: .19px;
    cursor: pointer;
}

.inp .rember input{
    outline: 0;
    width: 30px;
    height: 45px;
    border-radius: 4px;
    border: 1px solid #d9d9d9;
    text-indent: 20px;
    font-size: 14px;
    background: #fff !important;
}

.inp .rember p span{
    display: inline-block;
  font-size: 12px;
  width: 100px;
  /*position: absolute;*/
/*left: 20px;*/

}
#geetest{
    margin-top: 20px;
}
.login_btn{
     width: 100%;
    height: 45px;
    background: #84cc39;
    border-radius: 5px;
    font-size: 16px;
    color: #fff;
    letter-spacing: .26px;
    margin-top: 30px;
}
.inp .go_login{
    text-align: center;
    font-size: 14px;
    color: #9b9b9b;
    letter-spacing: .26px;
    padding-top: 20px;
}
.inp .go_login span{
    color: #84cc39;
    cursor: pointer;
}
</style>

2.在index.js中新增Login元件的路由

import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Login from '@/components/Login'

Vue.use(Router)

export default new Router({
  mode:'history',
  routes: [
   ......
    {
      path:'/user/login',
      component: Login
    }
  ]
})

3.將導航欄Vheader元件的登入按鈕設定一個跳轉連線

<!-- 點選登入按鈕 跳轉到登入的頁面:/user/login -->
<router-link to="user/login"><button class="signin">登入</button></router-link>

4.將導航欄Vheader元件中的登入狀態token改為false

將token值改為false,讓首頁是未登入狀態,這樣才能顯示登入註冊的按鈕

<script>
    export default {
      name: "Header",
      data(){
        return {
          
          token:false,
    }

5.建立users應用

1.建立user應用

python manage.py startapp users

2.在dev.py配置檔案的INSTALL_APPS加上users

3.為user應用配置總路由

  path('users/',include("users.urls")),

6.建立user模型類

在django的auth模組是自帶一個使用者表的,我們寫使用者表時可以繼承django自帶的使用者表.

from django.db import models
from django.contrib.auth.models import AbstractUser # AbstractUser是django自帶的一個使用者表


class User(AbstractUser):
    phone = models.CharField(max_length=16, null=True, blank=True)
    wechat = models.CharField(max_length=16, null=True, blank=True)

    class Meta:
        db_table = 'ly_user'
        verbose_name = '使用者表'
        verbose_name_plural = verbose_name

7.關於使用者表要做的兩件事

我們想做的兩件事:

  1.使用django自帶的Abstractuser表加自己的擴充套件欄位作為專案的使用者表【已經完成】

  2.後臺登入Xadmin的後臺管理系統的那些使用者不再使用原來django自帶的使用者表,而是使用自己建立的使用者表

Xadmin使用自己建立的使用者表

需要在setting中配置一下,讓django別再使用自帶的那個user表,而是使用我們自己編寫的user表

dev.py

AUTH_USER_MODEL = 'users.User'

注意:如果現在執行資料庫遷移指令會報錯。

上面的操作應該建立在第一次資料庫遷移之前。

但是我們之前已經進行過幾次資料庫遷移了。所以需要做以下幾步操作:

// 0. 先把現有的資料庫匯出備份,然後清掉資料庫中所有的資料表。

// 1. 把開發者建立的所有子應用下面的migrations目錄下除了__init__.py以外的所有遷移檔案,只要涉及到使用者的,一律刪除,並將django-migrations表中的資料全部刪除。

// 2. 把django.contrib.admin.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。

// 3. 把django.contrib.auth.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。

// 4. 把reversion.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。這個不在django目錄裡面,在site-packages裡面,是xadmin安裝的時候帶的,它會記錄使用者資訊,也需要刪除

// 5. 把xadmin.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。

// 6. 刪除我們當前資料庫中的所有表

// 7. 接下來,執行資料遷移(makemigrations和migrate),回顧第0步中的資料,將資料匯入就可以了,以後如果要修改使用者相關資料,不需要重複本次操作,直接資料遷移即可。// 

這些完成之後,建立一個超級使用者

python3 manage.py createsuperuser

會發現xadmin超級使用者的使用者名稱和密碼存到了自己的ly_user表中。我們達到了目的。

3.DjangoRestFramework JWT

1.jwt的安裝

pip install djangorestframework-jwt -i https://mirrors.aliyun.com/pypi/simple/

2.jwt的配置

dev.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}
import datetime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 
}

注:JWT_EXPIRATION_DELTA 指明token的有效期

3.為jwt配置路由

apps/users/urls.py

為jwt設定路由 ,用來前端訪問url 獲取jwt token值

from rest_framework_jwt.views import obtain_jwt_token
from django.urls import path

urlpatterns = [
    path(r'login/', obtain_jwt_token),
]

訪問 www.lyapi.com:8001/users/login 新增使用者名稱和密碼欄位 post提交後可得token值,如圖所示

4.前端通過axios請求獲取後端傳過來的token

使用者在登入介面輸入使用者名稱和密碼,點選登入 往後臺傳送post請求

後端返回給前端一個token值,前端接收token值並儲存起來

1.獲取使用者在前端輸入的使用者名稱和密碼

methods:{
    loginHandle(){
      this.$axios.post(`${this.$settings.Host}/users/login/`,{
        username:this.username,
        password:this.password,

2.為login.vue的登入按鈕繫結一個LoginHandle事件

<button class="login_btn" @click="loginHandle">登入</button>

這個時候我們訪問www.lycity.com/user/login 輸入使用者名稱和密碼

檢視console 可以看到data中已經存放著token值 ==>這個時候前端已經拿到token值

那麼接下來的問題是前端如何將token值儲存起來呢?

3.引入session storage 和 local storage===>實現前端對token的儲存

session storage和local storage的區別

session storage 是臨時儲存 關閉瀏覽器就沒有了

local storage 是永久儲存

5.前端儲存token值

登入頁面有一個記住密碼的選項,我們就可以使用session storage 和 loca storage 來做一些事情

1.將記住密碼checkbox設定為v-model

當使用者勾選/沒有勾選 記住密碼 這個選項時,remember的值發生改變

Login.vue

<p>
    <input type="checkbox" class="no" name="a" v-model="remember"/>
    <span>記住密碼</span>
</p>

2.我們先讓remember的值預設為false

export default {
  name: 'Login',
  data(){
    return {
         ...
        remember:false,
    }
  },

當使用者勾選記住密碼 remember的值為true 反之為false

所以根據 remember的值 就可以做if判斷了

6.擴充套件預設的返回值

預設的返回值僅有token值 ,但是我們還需在返回值中增加username和id ,方便在客戶端頁面中顯示當前登入使用者。

所以需要擴充套件一下

通過修改該檢視的返回值可以完成我們的需求

user/utils.py

def jwt_response_payload_handler(token, user=None, request=None):
    """
    自定義jwt認證成功返回資料
    """
    return {
        'token': token,
        'id': user.id,
        'username': user.username
    }

除了擴充套件一下這個,還要修改一下下面的配置

settings/dev.py

# JWT
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}

這個配置的作用是:告訴jwt 響應結果的時候,按照我指定的 jwt_response_payload_handler這個方法來返回

jwt 認證成功返回了三個欄位 token id username 響應給前端

前端也應該接収這幾個欄位

  methods:{
    loginHandle(){
      this.$axios.post(`${this.$settings.Host}/users/login/`,{
        username:this.username,
        password:this.password,

      }).then((res)=>{
        console.log(res);
        if (this.remember){
          localStorage.token = res.data.token;
          localStorage.username = res.data.username;
          localStorage.id = res.data.id;
           ...

        }else {
          sessionStorage.token = res.data.token;
          sessionStorage.username = res.data.username;
          sessionStorage.id = res.data.id;
            ...
        }

final

'''
實現思路:
如果使用者標選了記住密碼 就將token值儲存在local storage中
如果使用者沒有標記 記住密碼 就將token值儲存在session storage中 
'''

  methods:{
    loginHandle(){
      this.$axios.post(`${this.$settings.Host}/users/login/`,{
        username:this.username,
        password:this.password,

      }).then((res)=>{
        console.log(res);
        // console.log(this.remember);
        if (this.remember){
          localStorage.token = res.data.token;
          localStorage.username = res.data.username;
          localStorage.id = res.data.id;
          sessionStorage.removeItem('token');
          sessionStorage.removeItem('username');
          sessionStorage.removeItem('id');

        }else {
          sessionStorage.token = res.data.token;
          sessionStorage.username = res.data.username;
          sessionStorage.id = res.data.id;
          localStorage.removeItem('token');
          localStorage.removeItem('username');
          localStorage.removeItem('id');
        }
        this.$router.push('/');

Tip:關於session storage和local storage的使用方法

sessionStorage.變數名 = 變數值   // 儲存資料
sessionStorage.setItem("變數名","變數值") // 儲存資料
sessionStorage.變數名  // 讀取資料
sessionStorage.getItem("變數名") // 讀取資料
sessionStorage.removeItem("變數名") // 清除單個資料
sessionStorage.clear()  // 清除所有sessionStorage儲存的資料

localStorage.變數名 = 變數值   // 儲存資料
localStorage.setItem("變數名","變數值") // 儲存資料
localStorage.變數名  // 讀取資料
localStorage.getItem("變數名") // 讀取資料
localStorage.removeItem("變數名") // 清除單個資料
localStorage.clear()  // 清除所有sessionStorage儲存的資料

4.多條件登入

擴充套件的登入檢視,在收到使用者名稱與密碼時,也是呼叫Django的認證系統中提供的authenticate()來檢查使用者名稱與密碼是否正確。

我們可以通過修改Django認證系統的認證後端(主要是authenticate方法)來支援登入賬號既可以是使用者名稱也可以是手機號。

官方說:修改Django認證系統的認證後端需要繼承django.contrib.auth.backends.ModelBackend,並重寫authenticate方法。

authenticate(self, request, username=None, password=None, **kwargs)方法的引數說明:

  • request 本次認證的請求物件

  • username 本次認證提供的使用者賬號

  • password 本次認證提供的密碼

我們想要讓使用者既可以以使用者名稱登入,也可以以手機號登入,那麼對於authenticate方法而言,username引數即表示使用者名稱或者手機號。

重寫authenticate方法的思路:

  1. 根據username引數查詢使用者User物件,username引數可能是使用者名稱,也可能是手機號

  2. 若查詢到User物件,呼叫User物件的check_password方法檢查密碼是否正確

users/utils.py

def get_user_by_account(account):
    """
    根據帳號獲取user物件
    :param account: 賬號,可以是使用者名稱,也可以是手機號
    :return: User物件 或者 None
    """
    try:
        
        # 查詢使用者名稱是否存在
        user = models.User.objects.filter(Q(username=account)|Q(mobile=account)).first()
    except models.User.DoesNotExist:
        return None 
    else:
        return user # 存在返回使用者名稱

from . import models
from django.db.models import Q
from django.contrib.auth.backends import ModelBackend
class UsernameMobileAuthBackend(ModelBackend):
    """
    自定義使用者名稱或手機號認證
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        user = get_user_by_account(username)
        #if user is not None and user.check_password(password) :
        if user is not None and user.check_password(password) and user.is_authenticated:
            #user.is_authenticated是看他有沒有許可權的,這裡可以不加上它
            return user

在配置檔案settings/dev.py中告知Django使用我們自定義的認證後端

AUTHENTICATION_BACKENDS = [
    'users.utils.UsernameMobileAuthBackend',
]

以上就實現了我們通過使用者名稱或者手機號的一個多條件登入。

5.登入狀態的判斷和退出登入

在Vheader元件中新增如下內容

<script>
export default {
  name: "Header",
  data() {
    return {
        .....
      token: true,
      .....
    
    }
  },
  methods: {
    ......
    check_login() {
      // 二者只要其中之一有值就是true 代表使用者已經登入
      this.token = localStorage.token || sessionStorage.token;
    },

    logout() {
      // 使用者登出時,無論是臨時儲存還是永久儲存都應該清除掉
      sessionStorage.removeItem('token');
      sessionStorage.removeItem('username');
      sessionStorage.removeItem('id');
      localStorage.removeItem('token');
      localStorage.removeItem('username');
      localStorage.removeItem('id');
      this.check_login();
    }
  },

  created() {
    this.get_nav_data();
  }

相關文章