乐趣区

关于javascript:免费开源-基于Vue和Quasar的前端SPA项目实战之用户登录二

基于 Vue 和 Quasar 的前端 SPA 我的项目实战之用户登录(二)

回顾

通过上一篇文章 基于 Vue 和 Quasar 的前端 SPA 我的项目实战之环境搭建(一)的介绍,咱们曾经搭建好本地开发环境并且运行胜利了,明天次要介绍登录性能。

简介

通常为了平安思考,须要用户登录之后才能够拜访。crudapi admin web 我的项目也须要引入登录性能,用户登录胜利之后,跳转到治理页面,否则提醒没有权限。

技术调研

SESSION

SESSION 通常会用到 Cookie,Cookie 有时也用其复数模式 Cookies。类型为“小型文本文件”,是某些网站为了分别用户身份,进行 Session 跟踪而贮存在用户本地终端上的数据(通常通过加密),由用户客户端计算机临时或永恒保留的信息。
用户登录胜利后,后盾服务记录登录状态,并用 SESSIONID 进行惟一辨认。浏览器通过 Cookie 记录了 SESSIONID 之后,下一次拜访同一域名下的任何网页的时候会主动带上蕴含 SESSIONID 信息的 Cookie,这样后盾就能够判断用户是否曾经登录过了,从而进行下一步动作。长处是使用方便,浏览器主动解决 Cookie,毛病是容易受到 XSS 攻打。

JWT Token

Json web token (JWT), 是为了在网络应用环境间传递申明而执行的一种基于 JSON 的凋谢规范((RFC 7519). 该 token 被设计为紧凑且平安的,特地实用于分布式站点的单点登录(SSO)场景。JWT 的申明个别被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够减少一些额定的其它业务逻辑所必须的申明信息,该 token 也可间接被用于认证,也可被加密。
JWT 校验形式更加简略便捷化,无需通过缓存,而是间接依据 token 取出保留的用户信息,以及对 token 可用性校验,单点登录更为简略。毛病是登记不是很不便,并且因为 JWT Token 是 base64 加密,可能有平安方面隐患。
因为目前零碎次要是在浏览器环境中应用,所以抉择了 SESSION 的登录形式,后续思考应用 JWT 登录形式,JWT 更适宜 APP 和小程序场景。

登录流程


次要流程如下:

  1. 用户关上页面的时候,首先判断是否属于白名单列表,如果属于,比方 /login, /403, 间接放行。
  2. 本地 local Storage 如果保留了登录信息,阐明之前登录过,间接放行。
  3. 如果没有登录过,本地 local Storage 为空,跳转到登录页面。
  4. 尽管本地登录过了,然而可能过期了,这时候拜访任意一个 API 时候,会主动依据返回后果判断是否登录。

UI 界面


登录页面比较简单,次要包含用户名、明码输入框和登录按钮,点击登录按钮会调用登录 API。

代码构造

  1. api: 通过 axios 与后盾 api 交互
  2. assets:次要是一些图片之类的
  3. boot:动静加载库,比方 axios、i18n 等
  4. components:自定义组件
  5. css:css 款式
  6. i18n:多语言信息
  7. layouts:布局
  8. pages:页面,包含了 html,css 和 js 三局部内容
  9. router:路由相干
  10. service:业务 service,对 api 进行封装
  11. store:Vuex 状态治理,Vuex 是实现组件全局状态 (数据) 治理的一种机制,能够不便的实现组件之间数据的共享

配置文件

quasar.conf.js 是全局配置文件,所有的配置相干内容都能够这个文件外面设置。

外围代码

配置 quasar.conf.js

plugins: [
    'LocalStorage',
    'Notify',
    'Loading'
]

因为须要用到本地存储 LocalStorage,音讯提醒 Notify 和期待提醒 Loading 插件,所以在 plugins 外面增加。

配置全局款式

批改文件 quasar.variables.styl 和 app.styl, 比方设置主色彩为淡蓝色

$primary = #35C8E8

封装 axios

import Vue from 'vue'
import axios from 'axios'
import {Notify} from "quasar";
import qs from "qs";
import Router from "../router/index";
import {permissionService} from "../service";

Vue.prototype.$axios = axios

// We create our own axios instance and set a custom base URL.
// Note that if we wouldn't set any config here we do not need
// a named export, as we could just `import axios from 'axios'`
const axiosInstance = axios.create({baseURL: process.env.API});

axiosInstance.defaults.transformRequest = [function(data, headers) {
    // Do whatever you want to transform the data
    let contentType = headers["Content-Type"] || headers["content-type"];
    if (!contentType) {
      contentType = "application/json";
      headers["Content-Type"] = "application/json";
    }

    if (contentType.indexOf("multipart/form-data") >= 0) {return data;} else if (contentType.indexOf("application/x-www-form-urlencoded") >= 0) {return qs.stringify(data);
    }

    return JSON.stringify(data);
  }
];

// Add a request interceptor
axiosInstance.interceptors.request.use(function(config) {if (config.permission && !permissionService.check(config.permission)) {
      throw {message: "403 forbidden"};
    }

    return config;
  },
  function(error) {
    // Do something with request error
    return Promise.reject(error);
  }
);

function login() {setTimeout(() => {
    Router.push({path: "/login"});
  }, 1000);
}

// Add a response interceptor
axiosInstance.interceptors.response.use(function(response) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    // Do something with response data
    return response;
  },
  function(error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    // Do something with response error

    if (error.response) {if (error.response.status === 401) {
        Notify.create({
          message:  error.response.data.message,
          type: 'negative'
        });
        login();} else if (error.response.data && error.response.data.message) {
        Notify.create({
          message: error.response.data.message,
          type: 'negative'
        });
      } else {
        Notify.create({
          message: error.response.statusText || error.response.status,
          type: 'negative'
        });
      }
    } else if (error.message.indexOf("timeout") > -1) {
      Notify.create({
        message: "Network timeout",
        type: 'negative'
      });
    } else if (error.message) {
      Notify.create({
        message: error.message,
        type: 'negative'
      });
    } else {
      Notify.create({
        message: "http request error",
        type: 'negative'
      });
    }

    return Promise.reject(error);
  }
);

// for use inside Vue files through this.$axios
Vue.prototype.$axios = axiosInstance

// Here we define a named export
// that we can later use inside .js files:
export {axiosInstance}

axios 配置一个实例,做一些对立解决,比方网络申请数据预处理,验证权限,401 跳转,403 提醒等。

用户 api 和 service

import {axiosInstance} from "boot/axios";

const HEADERS = {"Content-Type": "application/x-www-form-urlencoded"};

const user = {login: function(data) {
    return axiosInstance.post("/api/auth/login",
      data,
      {headers: HEADERS}
    );
  },
  logout: function() {
    return axiosInstance.get("/api/auth/logout",
      {headers: HEADERS}
    );
  }
};

export {user};

登录 api 为 /api/auth/login,登记 api 为 /api/auth/logout

import {user} from "../api";
import {LocalStorage} from "quasar";

const userService = {login: async function(data) {var res = await user.login(data);
    return res.data;
  },
  logout: async function() {var res = await user.logout();
    return res.data;
  },
  getUserInfo: async function() {return LocalStorage.getItem("userInfo") || {};},
  setUserInfo: function(userInfo) {LocalStorage.set("userInfo", userInfo);
  }
};

export {userService};

用户 service 次要是对 api 的封装,而后还提供保留用户信息到 LocalStorage 接口

Vuex 治理登录状态

import {userService} from "../../service";
import {permissionService} from "../../service";

export const login = ({commit}, userInfo) => {return new Promise((resolve, reject) => {
    userService
      .login(userInfo)
      .then(data => {
          //session 形式登录,其实不须要 token,这里为了 JWT 登录预留,用 username 代替。// 通过 Token 是否为空判断本地有没有登录过,不便后续解决。commit("updateToken", data.principal.username);

          const newUserInfo = {
            username: data.principal.username,
            realname: data.principal.realname,
            avatar: "",
            authorities: data.principal.authorities || [],
            roles: data.principal.roles || []};
          commit("updateUserInfo", newUserInfo);

          let permissions = data.authorities || [];
          let isSuperAdmin = false;
          if (permissions.findIndex(t => t.authority === "ROLE_SUPER_ADMIN") >= 0) {isSuperAdmin = true;}

          permissionService.set({
            permissions: permissions,
            isSuperAdmin: isSuperAdmin
          });

          resolve(newUserInfo);
      })
      .catch(error => {reject(error);
      });
  });
};

export const logout = ({commit}) => {return new Promise((resolve, reject) => {
    userService
      .logout()
      .then(() => {resolve();
      })
      .catch(error => {reject(error);
      })
      .finally(() => {commit("updateToken", "");
        commit("updateUserInfo", {
          username: "",
          realname: "",
          avatar: "",
          authorities: [],
          roles: []});

        permissionService.set({permissions: [],
          isSuperAdmin: false
        });
      });
  });
};

export const getUserInfo = ({commit}) => {return new Promise((resolve, reject) => {
    userService
      .getUserInfo()
      .then(data => {commit("updateUserInfo", data);
        resolve();})
      .catch(error => {reject(error);
      });
  });
};

登录胜利之后,会把利用 Vuex 把用户和权限信息保留在全局状态中,而后 LocalStorage 也保留一份,这样刷新页面的时候会从 LocalStorage 读取到 Vuex 中。

路由跳转治理

import Vue from 'vue'
import VueRouter from 'vue-router'

import routes from './routes'
import {authService} from "../service";
import store from "../store";

Vue.use(VueRouter)

/*
 * If not building with SSR mode, you can
 * directly export the Router instantiation;
 *
 * The function below can be async too; either use
 * async/await or return a Promise which resolves
 * with the Router instance.
 */
const Router = new VueRouter({scrollBehavior: () => ({x: 0, y: 0}),
  routes,

  // Leave these as they are and change in quasar.conf.js instead!
  // quasar.conf.js -> build -> vueRouterMode
  // quasar.conf.js -> build -> publicPath
  mode: process.env.VUE_ROUTER_MODE,
  base: process.env.VUE_ROUTER_BASE
});

const whiteList = ["/login", "/403"];

function hasPermission(router) {if (whiteList.indexOf(router.path) !== -1) {return true;}

  return true;
}

Router.beforeEach(async (to, from, next) => {let token = authService.getToken();
  if (token) {
    let userInfo = store.state.user.userInfo;
    if (!userInfo.username) {
      try {await store.dispatch("user/getUserInfo");
        next();} catch (e) {if (whiteList.indexOf(to.path) !== -1) {next();
        } else {next("/login");
        }
      }
    } else {if (hasPermission(to)) {next();
      } else {next({ path: "/403", replace: true});
      }
    }
  } else {if (whiteList.indexOf(to.path) !== -1) {next();
    } else {next("/login");
    }
  }
});

export default Router;

通过复写 Router.beforeEach 办法,在页面跳转之前进行预处理,实现后面登录流程图外面的性能。

登录页面

submit() {if (!this.username) {this.$q.notify("用户名不能为空!");
    return;
  }

  if (!this.password) {this.$q.notify("明码不能为空!");
    return;
  }

  this.$q.loading.show({message: "登录中"});

  this.$store
    .dispatch("user/login", {
      username: this.username,
      password: this.password,
    })
    .then(async (data) => {this.$router.push("/");
      this.$q.loading.hide();})
    .catch(e => {this.$q.loading.hide();
      console.error(e);
    });
}

submit 办法中执行 this.$store.dispatch("user/login") 进行登录,示意调用 user store action 外面的 login 办法,如果胜利,执行this.$router.push("/")

配置 devServer 代理

devServer: {
  https: false,
  port: 8080,
  open: true, // opens browser window automatically
  proxy: {
    "/api/*": {
      target: "https://demo.crudapi.cn",
      changeOrigin: true
    }
  }
}

配置 proxy 之后,所有的 api 结尾的申请就会转发到后盾服务器,这样就能够解决了跨域拜访的问题。

验证


首先,成心输出一个谬误的用户名,提醒登录失败。


输出正确的用户名和明码,登录胜利,主动跳转到后盾治理页面。


F12 开启 chrome 浏览器 debug 模式,查看 localstorage,发现 userInfo,permission,token 内容和预期统一,其中权限 permission 相干内容在后续 rbac 章节中具体介绍。

小结

本文次要介绍了用户登录性能,用到了 axios 网络申请,Vuex 状态治理,Router 路由,localStorage 本地存储等 Vue 基本知识,而后还用到了 Quasar 的三个插件,LocalStorage, Notify 和 Loading。尽管登录性能比较简单,然而它残缺地实现了前端到后端之间的交互过程。

demo 演示

官网地址:https://crudapi.cn
测试地址:https://demo.crudapi.cn/crudapi/login

附源码地址

GitHub 地址

https://github.com/crudapi/crudapi-admin-web

Gitee 地址

https://gitee.com/crudapi/crudapi-admin-web

因为网络起因,GitHub 可能速度慢,改成拜访 Gitee 即可,代码同步更新。

退出移动版