基于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和小程序场景。
登录流程
次要流程如下:
- 用户关上页面的时候,首先判断是否属于白名单列表,如果属于,比方/login, /403, 间接放行。
- 本地local Storage如果保留了登录信息,阐明之前登录过,间接放行。
- 如果没有登录过,本地local Storage为空,跳转到登录页面。
- 尽管本地登录过了,然而可能过期了,这时候拜访任意一个API时候,会主动依据返回后果判断是否登录。
UI界面
登录页面比较简单,次要包含用户名、明码输入框和登录按钮,点击登录按钮会调用登录API。
代码构造
- api: 通过axios与后盾api交互
- assets:次要是一些图片之类的
- boot:动静加载库,比方axios、i18n等
- components:自定义组件
- css:css款式
- i18n:多语言信息
- layouts:布局
- pages:页面,包含了html,css和js三局部内容
- router:路由相干
- service:业务service,对api进行封装
- 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即可,代码同步更新。