zanePerfor中一套简单通用的Node前后端Token登录机制和github授权登录方式
HI!,你好,我是zane,zanePerfor是一款我开发的一个前端性能监控平台,现在支持web浏览器端和微信小程序端。
我定义为一款完整,高性能,高可用的前端性能监控系统,这是未来会达到的目的,现今的架构也基本支持了高可用,高性能的部署。实际上还不够,在很多地方还有优化的空间,我会持续的优化和升级。
开源不易,如果你也热爱技术,拥抱开源,希望能小小的支持给个star。
项目的github地址:https://github.com/wangweianger/zanePerfor
项目开发文档说明:https://blog.seosiwei.com/performance/index.html
谈起Token登录机制,相信绝大部分人都不陌生,相信很多的前端开发人员都有实际的开发实践。
此文章的Token登录机制主要针对于无实际开发经验或者开发过简单登录机制的人员,如果你是大佬几乎可以略过了,如果你感兴趣或者闲来无事也可以稍微瞅它一瞅。
此文章不会教你一步一步的实现一套登录逻辑,只会结合zanePerfor项目阐述它的登录机制,讲明白其原理比写一堆代码来的更实在和简单。
zanePerfor项目的主要技术栈是 egg.js、redis和mongodb, 如果你不懂没关系,因为他们都只是简单使用,很容易理解。
登录实现结果:
- 如果用户未注册时先注册然后直接登录
- 用户每次登录都会动态生成session令牌
- 同一账号在同一时刻只能在一个地方登录
cookie在项目中的作用
我们知道http是无状态的,因此如果要知道用户某次请求是否登录就需要带一定的标识,浏览器端http请求带标识常用的方式有两种:1、使用cookie附带标识,2、使用header信息头附带标识。
这里我们推荐的方式是使用cooke附带标识,因为它相当于来说更安全和更容易操作。
更安全体现在:cookie只能在同域下传输,还可以设置httpOnly来禁止js的更改。
更容易操作体现在:cookie传输是浏览器请求时自带的传输头信息,我们不需要额外的操作,cookie还能精确到某一个路径,并且可以设置过期时间自动过期,这样就显得更可控。
当然header信息头也有它的优势和用武之地,这里不做阐述。
redis在项目中的作用
一般的项目我们会把识别用户的标识放存放在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
});
};
}
};
}
- 前端的逻辑代码很简单,就是统一的判断返回code, 如果未登录则跳转到登录页面。
User表结构说明
// 代码路径 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 }, // 用户访问时间
});
- 用户表中 usertoken 字段比较重要,它表示每次用户登录时动态生成的Token令牌key, 也是存在在redis中用户信息的key值,此值每次用户登录时都会更新,并且是随机和唯一的。
Node Servers端登录逻辑
我们先来一张登录的页面


业务代码如下:
// 代码路径 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;
}
对照user表来进行逻辑的梳理。
- 每次登录前都会清除上一次在redis中的登录态信息,所以上一次的登录令牌对应的redis信息会失效,因此我们只需要做一个校验用户Token的信息在redis中是否存在即可判断用户当前登录态是否有效。
- 清除上一次登录态信息之后立即生成一个随机并唯一的key值做为新的Token令牌,并更新redis中Token的令牌信息 和 设置新的cookie令牌,这样就保证了以前的登录态失效,当前的登录态有效。
- redis 和 cookie 都设置相同的过期时间,以保证Token的时效性和安全性。
- cookie的httpOnly 我们需要开启,这样就保证的Token的不可操作性,encrypt 和 signed参数是egg.js 的参数,主要负责对cookie进行加密,让前端的cookie不已明文的方式呈现,提高安全性。
- 最后再更新用户的Token令牌信息,以保证用户的Token每次都是最新的,也用以下次登录时的清除操作。
Servers 端用户登录校验中间件
中间件的概念相信大家都不陌生,用过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();
}
逻辑梳理:
- 首先会获得上传的token令牌,这里cookie.get方法的 encrypt 和 signed 需要为true,这会把Token解析为明文。
- 在finUserForToken方法中主要是获取Token令牌对应的redis用户信息,只有当用户的信息为真值时才会通过校验
- 在中间件这一环节还有一个比较常规的验证 就是 验证请求的 referer, referer也是浏览器请求时自带的,在浏览器端不可操作,这相对的增加了一些安全性(项目中暂未做,这个验证比较简单,如果有需要的自己去实现)。
到此zanePerfor的Token校验机制其实已经完全实现完了,只是未做整体的总结,下面来继续的完成注册的逻辑。
用户注册逻辑实现

业务代码如下:
// 代码路径 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令牌信息, 这里都设置相同的过期时间,并加密cookie信息和httpOnly。
退出登录逻辑
退出登录逻辑很简单,直接清除用户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;
}
第三方github登录说明
根据zanePerfor的登录校验机制可以得出以下的结论:
- User表的用户名必须存在,密码可无,并且用户名在代码中强校验不能重复,但是在数据库中用户名是可以重复的。
- usertoken字段很重要,是实现所有Token机制的核心字段,每次登录和注册都会是随机并唯一的值
基于以上两点做第三方登录我们只需要实现以下几点即可:
- 只要给用户名赋值即可,因为用户密码登录和第三方登录是两套逻辑,因此用户名可以重复,这就解决了第三方登录一定不会存在用户已注册的提示。
- 第一次登录时注册用户,并把第三方的用户名当做表的用户名,第三方的secret作为用户的token字段。
- 第二次登录时使用token字段检测用户是否已注册,已注册走登录逻辑,未注册走注册逻辑。
// 代码地址 app/service/user.js // github register 核心注册逻辑
async githubRegister(data = {}) {
// 此字段为github用户名
const login = data.login;
// 此字段为github 唯一用户标识
const token = data.node_id;
let userInfo = {};
if (!login || !token) {
userInfo = { desc: 'github 权限验证失败, 请重试!' };
return;
}
// 通过token去查询用户是否存在
userInfo = await this.getUserInfoForGithubId(token);
// 身材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: login, usertoken: random_key });
}
} else {
// 不存在 先注册 再登录
const user = this.ctx.model.User();
user.user_name = login;
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第三方授权方式请参考:https://blog.seosiwei.com/performance/github.html
总结:
- 前端封装统一的登录验证,项目中 code 1004 为用户未登录,1000为成功。
- user数据表中储存一个usertoken字段,此字段是随机并唯一的标识,在注册时存入此字段,在每次登录时更新此字段。
- 浏览器端的Token令牌即usertoken字段,redis的每个Token存储的是相应的用户信息。
- 每次登录时清除上一次用户的登录信息,即清除redis登录校验信息,这样就能保证同一用户同一时间只能在一个地方登录。
- usertoken字段是随时在变的,redis用户信息和cookie Token令牌都有过期时间,cookie经过加密和httpOnly,更大的保证了Token的安全性。
- 对所有需要校验的http请求做中间件校验,通过Token令牌获取redis用户信息并验证,验证即通过,验证失败则重新去登录。
- 第三方登录使用token做用户是否重复校验,第一次时登录注册,第二次登录时则走登录逻辑。
zanePerfor中一套简单通用的Node前后端Token登录机制和github授权登录方式的更多相关文章
- 记node前后端代码共用的一次坑
项目背景 nodejs项目,webpack打包,用axios请求,Promise封装,nunjucks模板引擎: 之前已将nunjucks模板通过webpack打包策略,做成前后端共用: 目前需要将网 ...
- 一套.NET Core +WebAPI+Vue前后端分离权限框架
今天给大家推荐一个基于.Net Core开发的企业级的前后端分离权限框架. 项目简介 这是基于.NetCore开发的.构建的简单.跨平台.前后端分离的框架.此项目代码清晰.层级分明.有着完善的权限功能 ...
- Node前后端分离基本概括
首先从一个重要的概念“模板”说起. 广义上来说,web中的模板就是填充数据后可以生成文件的页面. 严格意义上来说,应该是模板引擎利用特定格式的文件和所提供的数据编译生成页面.模板大致分为前端模板(如e ...
- 一套基于SpringBoot+Vue+Shiro 前后端分离 开发的代码生成器
一.前言 最近花了一个月时间完成了一套基于Spring Boot+Vue+Shiro前后端分离的代码生成器,目前项目代码已基本完成 止步传统CRUD,进阶代码优化: 该项目可根据数据库字段动态生成 c ...
- 编写一个简单的flask的前后端交互的网页(flask简单知识的讲解)
实验原理: 1.什么是flask Flask是一个使用Python编写的轻量级Web应用框架,其WSGI工具采用Werkzeng,模板引擎使用Jinja2.Flask与 Django之间的区别就是Dj ...
- node前后端交互(Express)
1. Express框架是什么 1.1 Express是一个基于Node平台的web应用开发框架,它提供了一系列的强大特性,帮助你创建各种Web应用.我们可以使用 npm install expres ...
- Vue学习笔记-基于CDN引入方式简单前后端分离项目学习(Vue+Element+Axios)
一 使用环境 开发系统: windows 后端IDE: PyCharm 前端IDE: VSCode 数据库: msyql,navicat 编程语言: python3.7 (Windows x86- ...
- 没错,用三方 Github 做授权登录就是这么简单!(OAuth2.0实战)
本文收录在个人博客:www.chengxy-nds.top,技术资源共享. 上一篇<OAuth2.0 的四种授权方式>文末说过,后续要来一波OAuth2.0实战,耽误了几天今儿终于补上了. ...
- 数字IC前后端设计中的时序收敛(二)--Setup违反的修复方法
本文转自:自己的微信公众号<数字集成电路设计及EDA教程> 里面主要讲解数字IC前端.后端.DFT.低功耗设计以及验证等相关知识,并且讲解了其中用到的各种EDA工具的教程. 考虑到微信公众 ...
- 一套简单可依赖的Javascript库
还是[百度]的产品——Tangram不是我偏心,百度不是我亲戚这东西看上去确实不错 Tangram是一套简单可依赖的Javascript库,主要分为Base和Component两部分.Base提供了开 ...
随机推荐
- 安装anaconda遇到的一些问题
文章目录 前言 一.报错:jupyter notebook:Bad file descriptor (C:\ci\zeromq_1602704446950\work\src\epoll.cpp:100 ...
- JUC并发编程原理精讲(源码分析)
1. JUC前言知识 JUC即 java.util.concurrent 涉及三个包: java.util.concurrent java.util.concurrent.atomic java.ut ...
- 字符串处理------Brute Force与KMP
一,字符串的简单介绍 例:POJ1488 http://poj.org/problem?id=1488 题意:替换文本中的双引号: #include <iostream> #includ ...
- 2020-11-27:go中,map的读流程是什么?
福哥答案2020-11-27:[答案来自此链接:](https://www.bilibili.com/video/BV1Nr4y1w7aa?p=12)源码位于runtime/map.go文件中的map ...
- 2021-03-27:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。输入:head = 1→2→3→4→5, k = 2,输出:4→5→1→2→3。
2021-03-27:给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置.输入:head = 1→2→3→4→5, k = 2,输出:4→5→1→2→3. 福大大 答案20 ...
- 2021-06-03:布尔运算。给定一个布尔表达式和一个期望的布尔结果 result,布尔表达式由 0 (false)、1 (true)、& (AND)、 | (OR) 和 ^ (XOR) 符号组成。
2021-06-03:布尔运算.给定一个布尔表达式和一个期望的布尔结果 result,布尔表达式由 0 (false).1 (true).& (AND). | (OR) 和 ^ (XOR) 符 ...
- 2021-07-15:接雨水 II。给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
2021-07-15:接雨水 II.给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水. 福大大 答案2021-07-15: 小根堆 ...
- 2021-08-13:给定一个每一行有序、每一列也有序,整体可能无序的二维数组 ,在给定一个正数k,返回二维数组中,最小的第k个数。
2021-08-13:给定一个每一行有序.每一列也有序,整体可能无序的二维数组 ,在给定一个正数k,返回二维数组中,最小的第k个数. 福大大 答案2021-08-13: 二分法. 代码用golang编 ...
- 2023-05-20:go语言的slice和rust语言的Vec的扩容流程是什么?
2023-05-20:go语言的slice和rust语言的Vec的扩容流程是什么? 答案2023-05-20: go语言的slice扩容流程 go版本是1.20.4. 扩容流程见源码见runtime/ ...
- 一次redis主从切换导致的数据丢失与陷入只读状态故障
背景 最近一组业务redis数据不断增长需要扩容内存,而扩容内存则需要重启云主机,在按计划扩容升级执行主从切换时意外发生了数据丢失与master进入只读状态的故障,这里记录分享一下. 业务redis高 ...