HI!,你好,我是zane,zanePerfor是一款我开发的一个前端性能监控平台,如今支持web浏览器端和微信小程序端。html
我定义为一款完整,高性能,高可用的前端性能监控系统,这是将来会达到的目的,现今的架构也基本支持了高可用,高性能的部署。实际上还不够,在不少地方还有优化的空间,我会持续的优化和升级。前端
开源不易,若是你也热爱技术,拥抱开源,但愿能小小的支持给个star。git
项目的github地址:github.com/wangweiange…github
项目开发文档说明:blog.seosiwei.com/performance…web
谈起Token登陆机制,相信绝大部分人都不陌生,相信不少的前端开发人员都有实际的开发实践。ajax
此文章的Token登陆机制主要针对于无实际开发经验或者开发过简单登陆机制的人员,若是你是大佬几乎能够略过了,若是你感兴趣或者闲来无事也能够稍微瞅它一瞅。redis
此文章不会教你一步一步的实现一套登陆逻辑,只会结合zanePerfor项目阐述它的登陆机制,讲明白其原理比写一堆代码来的更实在和简单。mongodb
zanePerfor项目的主要技术栈是 egg.js、redis和mongodb, 若是你不懂不要紧,由于他们都只是简单使用,很容易理解。数据库
咱们知道http是无状态的,所以若是要知道用户某次请求是否登陆就须要带必定的标识,浏览器端http请求带标识经常使用的方式有两种:一、使用cookie附带标识,二、使用header信息头附带标识。express
这里咱们推荐的方式是使用cooke附带标识,由于它至关于来讲更安全和更容易操做。
更安全体如今:cookie只能在同域下传输,还能够设置httpOnly来禁止js的更改。
更容易操做体如今:cookie传输是浏览器请求时自带的传输头信息,咱们不须要额外的操做,cookie还能精确到某一个路径,而且能够设置过时时间自动过时,这样就显得更可控。
固然header信息头也有它的优点和用武之地,这里不作阐述。
通常的项目咱们会把识别用户的标识放存放在Session中,可是Session有其使用的局限性。
Session的局限:Session 默认存放在 Cookie 中,可是若是咱们的 Session 对象过于庞大,浏览器可能拒绝保存,这样就失去了数据的完整性。当 Session 过大时还会对每次http请求带来额外的开销。还有一个比较大的局限性是Session存放在单台服务器中,当有多台服务器时没法保证统一的登陆态。还会带来代码的强耦合性,不能使得登陆逻辑代码解耦。
所以这里引入redis进行用户身份识别的储存。
redis的优点:redis使用简单,redis性能足够强悍,储存空间无限制,多台服务器可使用统一的登陆态,登陆逻辑代码的解耦。
前端统一登陆态应该是每位前端童鞋都作过的事情,下面以zanePerfor的Jquery的AJAX为例作简单的封装为例:
// 代码路径:app/public/js/util.js
ajax(json) {
// ...代码略...
return $.ajax({
type: json.type || "post",
url: url,
data: json.data || "",
dataType: "json",
async: asyncVal,
success: function(data) {
// ...代码略...
// success 时统一使用this.error方法进行处理
if (typeof(data) == 'string') {
This.error(JSON.parse(data), json);
} else {
This.error(data, json);
}
},
// ...代码略...
});
};
error(data, json) {
//判断code 并处理
var dataCode = parseInt(data.code);
// code 为1004表示未登陆 须要统一走登陆页面
if (!json.isGoingLogin && dataCode == 1004) {
//判断app或者web
if (window.location.href.indexOf(config.loginUrl) == -1) {
location.href = config.loginUrl + '?redirecturl=' + encodeURIComponent(location.href);
} else {
popup.alert({
type: 'msg',
title: '用户未登录,请登陆!'
});
}
} else {
switch (dataCode) {
// code 为1000表示请求成功
case 1000:
json.success && json.success(data);
break;
default:
if (json.goingError) {
//走error回调
json.error && json.error(data);
} else {
//直接弹出错误信息
popup.alert({
type: 'msg',
title: data.desc
});
};
}
};
}复制代码
// 代码路径 app/model/user.js
const UserSchema = new Schema({
user_name: { type: String }, // 用户名称
pass_word: { type: String }, // 用户密码
system_ids: { type: Array }, // 用户所拥有的系统Id
is_use: { type: Number, default: 0 }, // 是否禁用 0:正常 1:禁用
level: { type: Number, default: 1 }, // 用户等级(0:管理员,1:普通用户)
token: { type: String }, // 用户秘钥
usertoken: { type: String }, // 用户登陆态秘钥
create_time: { type: Date, default: Date.now }, // 用户访问时间
});复制代码
咱们先来一张登陆的页面
// 代码路径 app/service/user.js
// 用户登陆
async login(userName, passWord) {
// 检测用户是否存在
const userInfo = await this.getUserInfoForUserName(userName);
if (!userInfo.token) throw new Error('用户名不存在!');
if (userInfo.pass_word !== passWord) throw new Error('用户密码不正确!');
if (userInfo.is_use !== 0) throw new Error('用户被冻结不能登陆,请联系管理员!');
// 清空之前的登陆态
if (userInfo.usertoken) this.app.redis.set(`${userInfo.usertoken}_user_login`, '');
// 设置新的redis登陆态
const random_key = this.app.randomString();
this.app.redis.set(`${random_key}_user_login`, JSON.stringify(userInfo), 'EX', this.app.config.user_login_timeout);
// 设置登陆cookie
this.ctx.cookies.set('usertoken', random_key, {
maxAge: this.app.config.user_login_timeout * 1000,
httpOnly: true,
encrypt: true,
signed: true,
});
// 更新用户信息
await this.updateUserToken({ username: userName, usertoken: random_key });
return userInfo;
}复制代码
中间件的概念相信你们都不陌生,用过koa,express和redux都应该知道,egg.js的中间件来自于与koa,在这里就不说概念了。
在zanePerfor项目中咱们只须要对全部须要进行登陆校验的路由(请求)进行中间件校验便可。
在egg中可这样使用:
// 代码来源 app/router/api.js
// 得到controller 和 middleware(中间件)
const { controller, middleware } = app;
// 对须要校验的路由进行校验
// 退出登陆
apiV1Router.get('user/logout', tokenRequired, user.logout);复制代码
// 代码路径 app/middleware/token_required.js
// Token校验中间件
module.exports = () => {
return async function(ctx, next) {
const usertoken = ctx.cookies.get('usertoken', {
encrypt: true,
signed: true,
}) || '';
if (!usertoken) {
ctx.body = {
code: 1004,
desc: '用户未登陆',
};
return;
}
const data = await ctx.service.user.finUserForToken(usertoken);
if (!data || !data.user_name) {
ctx.cookies.set('usertoken', '');
const descr = data && !data.user_name ? data.desc : '登陆用户无效!';
ctx.body = {
code: 1004,
desc: descr,
};
return;
}
await next();
};
};
// finUserForToken方法代码路径
// 代码路径 app/service/user.js
// 根据token查询用户信息
async finUserForToken(usertoken) {
let user_info = await this.app.redis.get(`${usertoken}_user_login`);
if (user_info) {
user_info = JSON.parse(user_info);
if (user_info.is_use !== 0) return { desc: '用户被冻结不能登陆,请联系管理员!' };
} else {
return null;
}
return await this.ctx.model.User.findOne({ token: user_info.token }).exec();
}复制代码
// 代码路径 app/service/user.js
// 用户注册
async register(userName, passWord) {
// 检测用户是否存在
const userInfo = await this.getUserInfoForUserName(userName);
if (userInfo.token) throw new Error('用户注册:用户已存在!');
// 新增用户
const token = this.app.randomString();
const user = this.ctx.model.User();
user.user_name = userName;
user.pass_word = passWord;
user.token = token;
user.create_time = new Date();
user.level = userName === 'admin' ? 0 : 1;
user.usertoken = token;
const result = await user.save();
// 设置redis登陆态
this.app.redis.set(`${token}_user_login`, JSON.stringify(result), 'EX', this.app.config.user_login_timeout);
// 设置登陆cookie
this.ctx.cookies.set('usertoken', token, {
maxAge: this.app.config.user_login_timeout * 1000,
httpOnly: true,
encrypt: true,
signed: true,
});
return result;
}复制代码
退出登陆逻辑很简单,直接清除用户Token对应的redis信息和cookie token令牌便可。
// 登出
logout(usertoken) {
this.ctx.cookies.set('usertoken', '');
this.app.redis.set(`${usertoken}_user_login`, '');
return {};
}复制代码
冻结用户的逻辑也比较简单,惟一须要注意的是,冻结的时候须要清除用户Token对应的redis信息。
// 冻结解冻用户
async setIsUse(id, isUse, usertoken) {
// 冻结用户信息
isUse = isUse * 1;
const result = await this.ctx.model.User.update(
{ _id: id },
{ is_use: isUse },
{ multi: true }
).exec();
// 清空登陆态
if (usertoken) this.app.redis.set(`${usertoken}_user_login`, '');
return result;
}复制代码
删除用户逻辑跟冻结用户逻辑一致,也须要注意清除用户Token对应的redis信息。
// 删除用户
async delete(id, usertoken) {
// 删除
const result = await this.ctx.model.User.findOneAndRemove({ _id: id }).exec();
// 清空登陆态
if (usertoken) this.app.redis.set(`${usertoken}_user_login`, '');
return result;
}复制代码
根据zanePerfor的登陆校验机制能够得出如下的结论:
基于以上两点作第三方登陆咱们只须要实现如下几点便可:
// 代码地址 app/service/user.js
// github | 新浪微博 register
async githubRegister(userinfo, token) {
let userInfo = {};
userInfo = await this.getUserInfoForGithubId(token);
const random_key = this.app.randomString();
if (userInfo.token) {
// 存在则直接登陆
if (userInfo.is_use !== 0) {
userInfo = { desc: '用户被冻结不能登陆,请联系管理员!' };
} else {
// 清空之前的登陆态
if (userInfo.usertoken) this.app.redis.set(`${userInfo.usertoken}_user_login`, '');
// 设置redis登陆态
this.app.redis.set(`${random_key}_user_login`, JSON.stringify(userInfo), 'EX', this.app.config.user_login_timeout);
// 设置登陆cookie
this.ctx.cookies.set('usertoken', random_key, {
maxAge: this.app.config.user_login_timeout * 1000,
httpOnly: true,
encrypt: true,
signed: true,
});
// 更新用户信息
await this.updateUserToken({ username: userinfo, usertoken: random_key });
}
} else {
// 不存在 先注册 再登陆
const user = this.ctx.model.User();
user.user_name = userinfo;
user.token = token;
user.create_time = new Date();
user.level = 1;
user.usertoken = random_key;
userInfo = await user.save();
// 设置redis登陆态
this.app.redis.set(`${random_key}_user_login`, JSON.stringify(userInfo), 'EX', this.app.config.user_login_timeout);
// 设置登陆cookie
this.ctx.cookies.set('usertoken', random_key, {
maxAge: this.app.config.user_login_timeout * 1000,
httpOnly: true,
encrypt: true,
signed: true,
});
}
return userInfo;
}复制代码
详细的github第三方受权方式请参考:blog.seosiwei.com/performance…