文章目录
前言
文章内容是我的crm解决方案里实现的原理的解析crm解决方案。
该套方案只适用于中小型后台管理工程中,逻辑比较简单,安全性也不是很好,可作为解决方案初学者学习之路的垫脚石,觉得写的不错点个赞吧!
有些代码示例中看不到引入,因为我这里只记录实现的思路,具体的可以看我项目源码。
api管理
登陆的api可以放在公共api文件中,例如src/api/sys.js
:
// 系统级的接口
import request from "@/utils/request";
/**
* 登请求
*/
export const login = (data) => {
return request({
url: "/sys/login",
method: "POST",
data,
});
};
/**
* 获取用户信息
*/
export const getUserInfo = () => {
return request({
url: "/sys/profile",
});
};
/**
* 获取所有权限
*/
export const permissionList = () => {
return request({
url: "/sys/permission",
});
};
登陆逻辑方法
关于登陆的相关逻辑推荐写在vuex中的用户模块src\views\login\index.vue
,这样的写好处是:
- 登陆相关逻辑可以在除了普通登陆的场景中使用,还可以在后续的需求中使用。
- 能储存一些用户状态,方便在各个页面中使用。
简单语言总结一下
登陆逻辑:
- 获取用户输入的账户和密码(密码需要做加密)
- 发送登陆请求
- 获取到token,设置好token,前端计时token(无感刷新token的方案以后再说)
- 跳转到首页
获取用户信息:
- 在全局路由守卫中获取用户信息(和权限一起处理)
退出登陆:
- 清空本地token和用户信息,以及本地所有数据
- 跳转至登陆页面
// 引入略...
export default {
namespaced: true,
state: () => ({
token: getItem(TOKEN) || "",
userInfo: {}, // 用户信息
}),
mutations: {
// 设置token
setToken(state, token) {
state.token = token;
setItem(TOKEN, token); // 具体实现先不管
},
// 设置用户信息
setUserInfo(state, userInfo) {
state.userInfo = userInfo;
},
},
actions: {
// 登陆动作
login(context, userInfo) {
const { username, password } = userInfo; // 拿到表单填入的
return new Promise((resolve, reject) => { // 返回promise为了让组件调用的时候能做异步处理
login({ // 调用登陆接口
username,
password: md5(password), // 加密处理(不推荐使用md5,推荐使用前后端配合的公私钥加密)
})
.then((data) => {
this.commit("user/setToken", data.token); // 存储token
// 登录后操作
// 为什么不接着去请求用户数据,要放在权限相关的路由守卫中,是因为浏览器刷新的时候不会走这里,也就拿不到最新的用户数据
router.push("/");
setTimeStamp(); // 设置token获取的时间(具体实现先不用管)
resolve();
})
.catch((err) => {
reject(err);
});
});
},
// 获取用户信息
async getUserInfo(context) {
const res = await getUserInfo(); // 调用获取用户信息的接口
this.commit("user/setUserInfo", res);
return res; // 可以返回出去,可能以后会用到
},
// 退出登陆
logout() {
// 清空vuex相关数据
this.commit("user/setToken", "");
this.commit("user/setUserInfo", {});
removeAllItem(); // 清空所有localStorage数据
router.push("/login");
},
},
};
密码加密
md5加密
这个是最简单的密码明文加密方式,但个人认为只能使用在登陆动作上,不能使用在注册动作上。因为登陆时,数据库是能够找到对应用户名的密码,同样根据md5加密后与前端发送来的加密后密码做比对,一致则登陆成功。注册就不能这样子了。
并且,如果md5加密方式不做其他处理,使用暴力解法一般能够获取到原值。所以不推荐使用。
jsencrypt公私钥加密
使用公私钥加密可以纯运用在前端,比如“记住我”的动作,还可以用在与后端交互的登陆和注册动作,当然后者是需要后端提供公钥的。
下载第三方库jsencrypt,对应代码写在目录源码src\utils\jsencrypt.js
中。
比较靠谱的登陆方案思路
知道了上面的思路后,我这里提一下一种比较靠谱的登陆方案(源码里没实现),可作为参考。
先填入用户账号和密码,并作人工验证(验证是否人工和防止高并发请求),验证通过后拿到人工验证成功码。
用账号和密码以及其他所需数据(例如手机端指纹识别码fingerprintjs2、人工验证成功码等等)去发送登陆请求,在此之前先向后端请求获取加密的公钥,用这个公钥加上前端生成的uuid码与数据通过JSEncrypt打包加密。
发送登陆请求后拿到token码,用token去发送请求获取用户的后端状态,例如,如果有需要验证的状态,就跳转到手机验证页面,输入手机验证码后发送请求与后端对比验证,对比成功后,再用公钥把手机端指纹识别码加密后发送给后端,后端返回用户数据、权限数据、界面数据等信息,前端存储起来,跳转到首页。 跳转到首页的同时做个回调通过账户id发送接口,进一步获取用户的详细信息。全程注意失败的情况。
使用token的好处就是,因为token是服务端的算法生成的,所以拿到客户端传来的token只需要解密即可,token都由客户端存储,减少服务端的压力。
登录鉴权
继续我们的代码实现。
这里登录鉴权主要分为两个角度:
- 当用户未登陆时,只能进入登陆页面。
- 登陆后,token没过期时,不能进入登陆页面。
具体在目录src\permission.js
文件中,与用户信息获取逻辑一起处理,且用全局路由守卫去触发。
路由守卫的主要大致逻辑分为:
已有token:
- 判断是否已有token,如果有token了,且在登陆页面,就自动跳转到首页。
- 已有token进入除登陆页外的其他页面,判断是否有用户信息,没有说明是新打开的页面,就去请求接口获取用户信息。
- 获取到用户信息后,处理里面的权限信息,例如路由权限(这里先不涉及,以后开个新的文章讲)。
没有token:
- 判断没有token,说明没有登陆,那么看是否访问的白名单页面,不是就强制跳转至登陆页面。
/* 处理登陆的路由鉴权 */
import router from "./router";
import store from "./store";
/* 页面跳转进度条 */
import NProgress from "nprogress";
import "nprogress/nprogress.css";
NProgress.configure({ showSpinner: false });
// 白名单
const whiteList = ["/login"];
/**
* 路由前置守卫
*/
router.beforeEach(async (to, from, next) => {
// 没想到async还可以写在这里
NProgress.start(); // 开启进度条
// 存在 token说明已登陆,如果进去的是login页面给跳到首页,如果去其他页面放行
if (store.getters.token) {
if (to.path === "/login") {
next("/");
NProgress.done();
} else {
// 判断用户资料是否获取
// 若不存在用户信息,则需要获取用户信息
if (!store.getters.hasUserInfo) {
// 为什么不把用户数据存在本地缓存,应该是为了安全考虑
// 触发获取用户信息的 action,并获取用户当前权限
const { permission } = await store.dispatch("user/getUserInfo");
// 处理用户权限,筛选出需要添加的权限 ------------ 分割线之内的可以先不用看,以后讲动态菜单会去细讲
const filterRoutes = await store.dispatch(
"permission/filterRoutes",
permission.menus
);
// 利用 addRoute 循环添加动态路由配置
// console.log("filterRoutes", filterRoutes);
filterRoutes.forEach((item) => {
router.addRoute(item);
});
// 添加完动态路由之后,需要在进行一次主动跳转(完成用户的页面跳转动作)-----------
return next(to.path);
}
next();
}
} else {
// 没有token的情况下,可以进入白名单
if (whiteList.indexOf(to.path) > -1) {
next();
} else {
next("/login"); // 其余页面跳转到login页面
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done(); // 保守的关掉页面跳转进度条
});
记住密码
这个动作实现比较简单,将输入信息通过cookie进行缓存即可。借助第三方库js-cookie实现。具体可以看src\views\login\index.vue
文件里的内容。
写组件登陆逻辑
以上步骤都准备好,就可以在登陆页面src\views\login\index.vue
上写逻辑了。
退出登陆方案
有两种动作:
- 主动退出:用户点击登录按钮之后退出。
- 被动退出:token过期或被其他人登陆”顶下来“时退出。
主动退出
在vuex目录src\store\modules\user.js
文件写主动退出登陆的逻辑(前面代码示例有)。
被动退出
被动退出分为主动处理和被动处理
主动处理
前端通过计算token的时效,超过后自动退出登陆。
首先需要能处理token时效性判断的函数,写在目录src\utils\auth.js
中,然后每次登陆的时候存储token获取的时间,在接口请求拦截器中判断是否超过token失效时间,超过就主动退出登陆。
被动处理
被动处理包含两个方面:
- token失效,服务端会对token进行时效性记录,失效了就会在请求接口返回401。
- 单点登陆,用户在其他设备登陆了,当前用户被挤下来(此项目暂不实现,原理也是后端接口返回一个特殊状态码进行判断)
token失效只需要在响应拦截器中对401做处理即可。
单点登录
这玩意就是当一个公司产品线多了时,不同产品之间想共享用户信息,搞出来个单点登录。
会有个用户中心服务,专门去统一管理这些用户信息。
然后单点登录分为两种实现方式
cookie+session模式
客户端想在子系统登录,客户端会向用户认证中心服务发送登录消息, 认证中心生成一个sid以set cookie
的形式返回给客户端,并且自己往一张表里写入sid值: 用户信息xxxx
记录用户信息,这个表就是session。
以后客户端请求都带上cookie,把里面的sid给子系统,子系统会拿着这个sid去和认证中心求证,后者在session中验证无误后回答子系统,子系统再返回对应的数据给客户端。
优点:
- 可以通过删除sid让用户及时下线
缺点:
- 成本高,当用户量大了,表的搭建成本和服务的防护也要提升
token模式
就是认证中心服务没有session表了,直接给客户端返回token,客户端和子系统请求的时候带上token,子系统自己拿token验证。
如果采用了双token模式,认证中心返回一个短期token和长期token,短token用于和子系统交互。当短期token失效了,再从新从认证中心用长token去重新更新token。如果想要让用户下线,对短token做不了文章,但只需要把长token失效就可以了,时间到客户端再访问认证中心直接过期。
双token前端实现参考:【场景方案】关于前端对接口行为的控制合集:轮番查询、并发请求、服务端通知、token无感刷新、请求取消
优点:
- 认证中心压力小
缺点:
- 让用户下线没那么及时