写点什么

「开源免费」基于 Vue 和 Quasar 的前端 SPA 项目 crudapi 后台管理系统实战之用户登录(二)

用户头像
crudapi
关注
发布于: 2021 年 04 月 07 日
「开源免费」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之用户登录(二)

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

回顾

通过上一篇文章 https://xie.infoq.cn/article/c01a1520026561535e05db167的介绍,我们已经搭建好本地开发环境并且运行成功了,今天主要介绍登录功能。

简介

通常为了安全考虑,需要用户登录之后才可以访问。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 interceptoraxiosInstance.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 interceptoraxiosInstance.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.$axiosVue.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 即可,代码同步更新。

发布于: 2021 年 04 月 07 日阅读数: 23
用户头像

crudapi

关注

crudapi是crud+api组合,表示增删改查接口 2019.06.19 加入

使用crudapi可以告别枯燥无味的增删改查代码,让您更加专注业务,节约大量成本,从而提高工作效率。crudapi的目标是让处理数据变得更简单!官网:https://crudapi.cn 演示:https://demo.crudapi.cn/crudapi/login

评论

发布
暂无评论
「开源免费」基于Vue和Quasar的前端SPA项目crudapi后台管理系统实战之用户登录(二)