从壹开始前后端分离 [ 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 查看,文末有地址

image

零、今天要完成右下角粉色区块的部分

image

一、如何实现权限验证的过程

大家一定还记得之前在 .net core api 系列文章中《框架之五 || Swagger的使用 3.3 JWT权限验证【修改】》,咱们通过对 JWT 的讲解,实现了对接口的验证,大家可以去了解一下,当时因为是没有前端,所以咱们就直接用的 Swagger 接口文档来手动设置的权限验证,当时群里有很多小伙伴对这个不是很明白,我也是简单说了下,通过手动在 swagger 中输入Header ,变成每次 vue 的 axios 请求的 Header 中添加 Token,这个 Token 就是咱们手动配置的那个,因为当时没有前后端搭配,所以只是比较笼统的说了下这个流程,今天呢,就重点来说下这个授权登陆验证,也为下边的管理后台铺路,这里配合 Vue 前端,再仔细讲讲是如何实现前后端同时验证的:

image

上图中说的也是很详细了,主要分为两个验证:

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 了

image

然后、在项目的入口配置文件 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>

添加路由后,测试页面是否可行

image

3、配合后台登陆请求

完善 BlogController.cs 页面,稍微调整了下接口,和之前的没有差别,并增加权限验证

image
 /// <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 中

注意:目前是用的 localStorage 本地存储的方法,Vuex 暂时没有完成,因为在路由 router 中,取不到,有知道的小伙伴请留言

<script> export default {
  methods: {
    login: function() {
      let that = this; 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);//保存到 Vuex,还没有实现
                window.localStorage.setItem("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;
        }
      });
    }
  },
  data() { return {
      user: {},
      rules: {
        name: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
        pass: [{ required: true, message: "密码不能为空", trigger: "blur" }]
      }
    };
  }
}; </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: ""//定义全局变量 token
 }, // 改变state里面的值得方法
 mutations: {
    getFormData(state, data) {
      state.formDatas = data;
    },
    saveToken(state, data) {//将 token 保存
      state.token = data;
    }
  }
}); // 输出模块
export default store;

6、这个时候要修改下之前我们封装的 http.js 方法,因为当时我们过滤掉了失败的方法,这里要打开下,大家自行修改下

image

这个时候,我们再登陆的话,已经发生变化

image

这个时候大家可以看到,我们成功的登陆了(右上角有欢迎提示),然后 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 里,更没有进行验证

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);
  }
};

运行项目查看:

image

大家观察可以看到,我们第一次点击 Home 的时候,发现跳转到了 Login 页面,然后登陆后,自动跳转首页,并成功获取到数据,登陆成功!

四、说明

今天因为时间的关系,没有把 Vuex 在路由中如何获取研究出来,这里先用了本地缓存来代替了,大家如果有知道的小伙伴,请留言哈~~~不胜感激,

五、CODE

前端:
https://github.com/anjoy8/Blog.Vue

后端:

https://github.com/anjoy8/Blog.Core

QQ群:
867095512 (blod.core)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容