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…