1. 程式人生 > >從壹開始前後端分離 [ Vue2.0+.NET Core2.1] 二十四║ Vuex + JWT 實現授權驗證登陸

從壹開始前後端分離 [ Vue2.0+.NET Core2.1] 二十四║ Vuex + JWT 實現授權驗證登陸

壹週迴顧

哈嘍,又是元氣滿滿的一個週一,又與大家見面了,週末就是團圓節了,正好咱們的前後端也要團圓了,為什麼這麼說呢,因為以後的開發可能就需要前後端一起了,兩邊也終於會師了,還有幾天Vue系列就基本告一段落了,大家也好好加油鴨,今天將的內容呢,其實細心的你看到題目應該就能大家猜到了,前提是一直看本系列的小夥伴們,包括之前.net core部分,這裡先簡單說下上週咱們都說了什麼:

週一:《十九║Vue基礎: 樣式動態繫結+生命週期》重點說了下 Vue 開發中的八個生命週期,這個是一個重點,希望大家可以多看看,這個在以後的開發中會經常遇到;

週二:《二十║Vue基礎終篇:元件詳解+專案說明》重點說了下元件

的使用,包括定義、傳值、使用等等,這個更是重中之重,元件的使用在 Vue 的開發中必不可少;

週三:《二十一║Vue實戰:開發環境搭建【詳細版】》詳細的說了下開發環境的搭建,不僅講了如何搭建,還詳細的說明了每一個工具、外掛的使用意義;

週四:《二十二║Vue實戰:個人部落格第一版(axios+router)》根據週三搭建的環境,第一次建立了咱們第一版的個人部落格,封裝了 axios ,第一次連線上了咱們之前的 .net core api;

週五:《二十三║Vue實戰:Vuex 其實很簡單》通過一個小 DEMO 說明了 Vuex 是如何對我們的 Vue 實行狀態化管理的,讓大家對其使用有了一定的瞭解,為在以後的大專案中使用打下基礎;

週五的時候,咱們通過對錶單的元件化,來說明了 vuex 的存在意義,今天咱們還是會用到這個 vuex ,而且還會配合著 .net core api,到底是什麼呢?請看今天的講解。

注意:週四的時候,只寫了個人部落格的首頁,週末的時候,已經把詳情頁更新了,大家可以自行去 Git 檢視,文末有地址

 

零、今天要完成右下角粉色區塊的部分

 

一、如何實現許可權驗證的過程

大家一定還記得之前在 .net core api 系列文章中《框架之五 || Swagger的使用 3.3 JWT許可權驗證【修改】》,咱們通過對 JWT 的講解,實現了對介面的驗證,大家可以去了解一下,當時因為是沒有前端,所以咱們就直接用的 Swagger 介面文件來手動設定的許可權驗證,當時群裡有很多小夥伴對這個不是很明白,我也是簡單說了下,通過手動在 swagger 中輸入Header ,變成每次 vue 的 axios 請求的 Header 中新增 Token,這個 Token 就是咱們手動配置的那個,因為當時沒有前後端搭配,所以只是比較籠統的說了下這個流程,今天呢,就重點來說下這個授權登陸驗證,也為下邊的管理後臺鋪路,這裡配合 Vue 前端,再仔細講講是如何實現前後端同時驗證

的:

上圖中說的也是很詳細了,主要分為兩個驗證:

1、前端驗證(藍色部分),使用者訪問一個頁面,首先判斷是否需要驗證登陸,比如管理後臺,或者訂單系統的下單頁(首頁和詳情頁自然是不需要使用者登陸的,購物車和訂單等必須登陸,當然有些遊客也可以購買的除外),然後去驗證是否存在 Token,存在就新增到 axios 的 Header 中去請求後端 API,反之則去登陸頁登陸;

2、後端驗證(綠色部分),這個就是咱們之前在說 .net core api 的時候說到的 JWT 授權驗證,根據當前前端 axios 請求中傳來的 Token ,解析出是否被篡改,以及是否會相應的許可權,這樣就可以進一步返回資料了;

這個時候大家一定會有疑惑了,既然現在每一個介面都定義了許可權,為什麼要倆邊都需要驗證,只需要後端 api 一個驗證不就行了,何必這麼麻煩?我認為是這樣的:

首先前端驗證的主要目的是:通過手動配置,可以讓使用者去主動獲取 Token ,不用每次都去獲取,而且也減輕了後端請求的次數,總不能是先去傳送請求,再判斷當前頁面是否需要登陸吧,嗯,總結來說,

前端是為了頁面級登陸,後端是為了介面級驗證,而且也是想把 vue 前端工程化的思想。

二、結合API設計登入頁 —— 實現後端驗證

1、引入 ElementUI 樣式框架

 因為之後需要一個管理後臺,所以考慮著增加一個框架,目前比較流行的就是 ElementUI 和 IView,今天咱們先說一下引用 ElementUI

首先,在專案中 執行 npm install,初始化以後,在 node_modules 中檢視是否存在 element-ui 資料夾,如果沒有,則執行

 npm i element-ui -S

然後就可以看到專案中已經成功安裝 elementui 了

然後、在專案的入口配置檔案 main.js 中,引用

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI)

如果專案沒有報錯,到此則安裝成功。 

2、新增統一登陸頁面

第一、在 src 的 views 資料夾內,新增 Login.vue 頁面,並新增內容:

<template>
    <el-row type="flex" justify="center">
        <el-form ref="loginForm" :model="user" :rules="rules" status-icon label-width="50px">
            <el-form-item label="賬號" prop="name">
                <el-input v-model="user.name"></el-input>
            </el-form-item>
            <el-form-item label="密碼" prop="pass">
                <el-input v-model="user.pass" type="password"></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" icon="el-icon-upload" @click="login">登入</el-button>
            </el-form-item>
        </el-form>
    </el-row>
</template>

<script>
export default {
  methods: {
    login() {//使用elementui validate驗證
      this.$refs.loginForm.validate(valid => {
        if (valid) {//這裡在下邊會改寫成登陸資訊 感謝 @ 提醒註釋錯誤問題
          if (this.user.name === "admin" && this.user.pass === "123") {
            this.$notify({
              type: "success",
              message: "歡迎你," + this.user.name + "!",
              duration: 3000
            });
            this.$router.replace("/");
          } else {
            this.$message({
              type: "error",
              message: "使用者名稱或密碼錯誤",
              showClose: true
            });
          }
        } else {
          return false;
        }
      });
    }
  },
  data() {
    return {
      user: {},//配合頁面內的 prop 定義資料
      rules: {//配合頁面內的 prop 定義規則
        name: [{ required: true, message: "使用者名稱不能為空", trigger: "blur" }],
        pass: [{ required: true, message: "密碼不能為空", trigger: "blur" }]
      }
    };
  }
};
</script>

新增路由後,測試頁面是否可行

3、配合後臺登陸請求

完善  BlogController.cs 頁面,稍微調整了下介面,和之前的沒有差別,並增加許可權驗證

        /// <summary>
        /// 獲取部落格列表
        /// </summary>
        /// <param name="id"></param>
        /// <param name="page"></param>
        /// <param name="bcategory"></param>
        /// <returns></returns>
        [HttpGet]
        public async Task<object> Get(int id, int page = 1, string bcategory = "技術博文")
        {
            int intTotalCount = 6;
            int TotalCount = 1;
            List<BlogArticle> blogArticleList = new List<BlogArticle>();

            if (redisCacheManager.Get<object>("Redis.Blog") != null)
            {
                blogArticleList = redisCacheManager.Get<List<BlogArticle>>("Redis.Blog");
            }
            else
            {
                blogArticleList = await blogArticleServices.Query(a => a.bcategory == bcategory);
                redisCacheManager.Set("Redis.Blog", blogArticleList, TimeSpan.FromHours(2));
            }


            TotalCount = blogArticleList.Count() / intTotalCount;

            blogArticleList = blogArticleList.OrderByDescending(d => d.bID).Skip((page - 1) * intTotalCount).Take(intTotalCount).ToList();

            foreach (var item in blogArticleList)
            {
                if (!string.IsNullOrEmpty(item.bcontent))
                {
                    int totalLength = 500;
                    if (item.bcontent.Length > totalLength)
                    {
                        item.bcontent = item.bcontent.Substring(0, totalLength);
                    }
                }
            }

            var data = new { success = true, page = page, pageCount = TotalCount, data = blogArticleList };


            return data;
        }

        // GET: api/Blog/5
        /// <summary>
        /// 獲取詳情
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet("{id}", Name = "Get")]
        public async Task<object> Get(int id)
        {
            var model = await blogArticleServices.getBlogDetails(id);
            var data = new { success = true, data = model };
            return data;
        }

調整 LoginController.cs 的獲取 Token 方法:

  /// <summary>
        /// 獲取JWT的方法
        /// </summary>
        /// <param name="id">id</param>
        /// <param name="sub">角色</param>
        /// <returns></returns>
        [HttpGet]
        [Route("Token")]
        public JsonResult GetJWTStr(string name, string pass)
        {
            string jwtStr = string.Empty;
            bool suc = false;
            //這裡就是使用者登陸以後,通過資料庫去調取資料,分配許可權的操作
            //這裡直接寫死了
            if (name == "admins" && pass == "admins")
            {
                TokenModelJWT tokenModel = new TokenModelJWT();
                tokenModel.Uid = 1;
                tokenModel.Role = "Admin";

                jwtStr = JwtHelper.IssueJWT(tokenModel);
                suc = true;
            }
            else
            {
                jwtStr = "login fail!!!";
            }
            var result = new
            {
                data = new { success = suc, token = jwtStr }
            };


            return Json(result);
        }

4、修改 前端的 Login.vue 頁面的登陸方法,獲取到 Token ,並把其儲存到 Vuex 中

<template>
    <el-row type="flex" justify="center">
        <el-card v-if="isLogin">
            歡迎:admins
            <br>
            <br>
            <el-button type="primary" icon="el-icon-upload" @click="loginOut">退出登入</el-button>
        </el-card>
        <el-form v-else ref="loginForm" :model="user" :rules="rules" status-icon label-width="50px">
            <el-form-item label="賬號" prop="name">
                <el-input v-model="user.name"></el-input>
            </el-form-item>
            <el-form-item label="密碼" prop="pass">
                <el-input v-model="user.pass" type="password"></el-input>
            </el-form-item>
            <el-form-item>
                <el-button type="primary" icon="el-icon-upload" @click="login">登入</el-button>
            </el-form-item>
        </el-form>
    </el-row>
</template>

<script>
export default {
  methods: {
    login: function() {
      let that = this;
        that.$store.commit("saveToken", "");//清掉 token
        this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.$api.get(
            "Login/Token",
            { name: that.user.name, pass: that.user.pass },
            r => {
              if (r.data.success) {
                var token = r.data.token;
                that.$store.commit("saveToken", token);//儲存 token
                this.$notify({
                  type: "success",
                  message: "歡迎你," + this.user.name + "!",
                  duration: 3000
                });
                console.log(that.$store.state.token);
                this.$router.replace("/");
              } else {
                this.$message({
                  type: "error",
                  message: "使用者名稱或密碼錯誤",
                  showClose: true
                });
              }
            }
          );
        } else {
          return false;
        }
      });
    },
      loginOut(){
          this.isLogin=false;
          this.$store.commit("saveToken", "");//清掉 token

      }
  },
  data() {
    return {
        isLogin:false,
      user: {},
      rules: {
        name: [{ required: true, message: "使用者名稱不能為空", trigger: "blur" }],
        pass: [{ required: true, message: "密碼不能為空", trigger: "blur" }]
      }
    };
  },
    created() {
        if (window.localStorage.Token&&window.localStorage.Token.length>=128){
            this.isLogin=true;
        }
    }

};
</script>

5、修改 vuex 倉庫,把 token 存進store中

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
  // 初始化的資料
  state: {
    formDatas: null,
    token: "1"
  },
  // 改變state裡面的值得方法
  mutations: {
    getFormData(state, data) {
      state.formDatas = data;
    },
    saveToken(state, data) {
      state.token = data;
      window.localStorage.setItem("Token", data);//就是這裡,儲存到了 localStorage 中
    }
  }
});
// 輸出模組
export default store;

6、這個時候要修改下之前我們封裝的 http.js 方法,因為當時我們過濾掉了失敗的方法,這裡要開啟下,大家自行修改下

這個時候,我們再登陸的話,已經發生變化

 

這個時候大家可以看到,我們成功的登陸了(右上角有歡迎提示),然後 token 也成功的儲存到 stroe/localStorage 裡(下邊控制檯輸出),

因為我們在部落格頁增加了許可權,雖然我們是用的 admin 賬號,但是 Header 中還沒有新增Token,所以現在還是 401,那如何才能有效的增加請求 Header 呢,請往下看,許可權驗證前端部分。

三、實現一:登陸攔截驗證——路由攔截

 1、修改 router.js 路由,實現按需登陸

在需要登陸的地方,增加登陸要求欄位,

然後增加 beforeEach 鉤子函式(這裡有一個問題,只能獲取到本地快取資料,無法獲取 Vuex ,正在研究中)

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import FormVuex from "./views/FormVuex.vue";
import Content from "./views/content";
import Login from "./views/Login";
import store from "./store";

Vue.use(Router);

const router = new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home,
      meta: {
        requireAuth: true // 新增該欄位,表示進入這個路由是需要登入的
      }
    },
    {
      path: "/Vuex",
      name: "Vuex",
      component: FormVuex
    },
    {
      path: "/Content/:id",
      name: "Content",
      component: Content
    },
    {
      path: "/Login",
      name: "Login",
      component: Login
    },
    {
      path: "/about",
      name: "about",
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "about" */ "./views/Form.vue")
    }
  ]
});

router.beforeEach((to, from, next) => {
    if (to.meta.requireAuth) {  // 判斷該路由是否需要登入許可權
        if (window.localStorage.Token&&window.localStorage.Token.length>=128) {  // 通過vuex state獲取當前的token是否存在
            next();
        }
        else {
            next({
                path: '/login',
                query: {redirect: to.fullPath}  // 將跳轉的路由path作為引數,登入成功後跳轉到該路由
            })
        }
    }
    else {
        next();
    }
})

export default router;

 2、修改 http.js 封裝方法,自動在請求中把 Token 新增到 Header 中

上邊的路由設定,僅僅是對 Token 進行判斷,還沒有新增到 Header 裡,更沒有進行驗證

注意:目前是用的 localStorage 本地儲存的方法(這一點是受到微信小程式的啟發),

但是直接在 router.js 中直接獲取 store 的token屬性,取不到,有知道的小夥伴請留言 

import store from "../store";
import router from "../router.js";

// 配置API介面地址
var root = "http://localhost:58427/api";
var root1 = "http://123.206.33.109:8018/api";
// 引用axios
var axios = require("axios");
// 自定義判斷元素型別JS
function toType(obj) {
  return {}.toString
    .call(obj)
    .match(/\s([a-zA-Z]+)/)[1]
    .toLowerCase();
}
// 引數過濾函式
function filterNull(o) {
  for (var key in o) {
    if (o[key] === null) {
      delete o[key];
    }
    if (toType(o[key]) === "string") {
      o[key] = o[key].trim();
    } else if (toType(o[key]) === "object") {
      o[key] = filterNull(o[key]);
    } else if (toType(o[key]) === "array") {
      o[key] = filterNull(o[key]);
    }
  }
  return o;
}

// http request 攔截器
axios.interceptors.request.use(
  config => {
    if (window.localStorage.Token&&window.localStorage.Token.length>=128) {//store.state.token 獲取不到值??
      // 判斷是否存在token,如果存在的話,則每個http header都加上token
      config.headers.Authorization = window.localStorage.Token;
    }
    return config;
  },
  err => {
    return Promise.reject(err);
  }
);
// http response 攔截器
axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response) {
      switch (error.response.status) {
        case 401:
          // 返回 401 清除token資訊並跳轉到登入頁面
          router.replace({
            path: "login",
            query: { redirect: router.currentRoute.fullPath }
          });
      }
    }
    return Promise.reject(error.response.data); // 返回介面返回的錯誤資訊
  }
);
/*
  介面處理函式
  這個函式每個專案都是不一樣的,我現在調整的是適用於
  https://cnodejs.org/api/v1 的介面,如果是其他介面
  需要根據介面的引數進行調整。參考說明文件地址:
  https://cnodejs.org/topic/5378720ed6e2d16149fa16bd
  主要是,不同的介面的成功標識和失敗提示是不一致的。
  另外,不同的專案的處理方法也是不一致的,這裡出錯就是簡單的alert
*/

function apiAxios(method, url, params, success, failure) {
  if (params) {
    params = filterNull(params);
  }
  axios({
    method: method,
    url: url,
    data: method === "POST" || method === "PUT" ? params : null,
    params: method === "GET" || method === "DELETE" ? params : null,
    baseURL: root,
    withCredentials: false
  })
    .then(function(res) {
      success(res.data);
    })
    .catch(function(err) {
      let res = err.response;
      if (err) {
        window.alert("api error, HTTP CODE: " + res.status);
      }
    });
}

// 返回在vue模板中的呼叫介面
export default {
  get: function(url, params, success, failure) {
    return apiAxios("GET", url, params, success, failure);
  },
  post: function(url, params, success, failure) {
    return apiAxios("POST", url, params, success, failure);
  },
  put: function(url, params, success, failure) {
    return apiAxios("PUT", url, params, success, failure);
  },
  delete: function(url, params, success, failure) {
    return apiAxios("DELETE", url, params, success, failure);
  }
};

執行專案檢視:

大家觀察可以看到,我們第一次點選 Home 的時候,發現跳轉到了 Login 頁面,然後登陸後,自動跳轉首頁,併成功獲取到資料,登陸成功!

然後退出登陸,發現首頁已經進不去了,退出成功!

四、說明

 今天因為時間的關係,沒有把 Vuex 在路由中如何獲取研究出來,這裡先用了本地快取來代替了,大家如果有知道的小夥伴,請留言哈~~~不勝感激,

五、CODE

後端: