摘要:的優勢使用簡單,性能足夠強悍,儲存空間無限制,多臺服務器可以使用統一的登錄態,登錄邏輯代碼的解耦。每次登錄時清除上一次用戶的登錄信息,即清除登錄校驗信息,這樣就能保證同一用戶同一時間只能在一個地方登錄。
HI!,你好,我是zane,zanePerfor是一款我開發的一個前端性能監控平臺,現在支持web瀏覽器端和微信小程序端。
我定義為一款完整,高性能,高可用的前端性能監控系統,這是未來會達到的目的,現今的架構也基本支持了高可用,高性能的部署。實際上還不夠,在很多地方還有優化的空間,我會持續的優化和升級。
開源不易,如果你也熱愛技術,擁抱開源,希望能小小的支持給個star。
項目的github地址:https://github.com/wangweiang...
項目開發文檔說明:https://blog.seosiwei.com/per...
談起Token登錄機制,相信絕大部分人都不陌生,相信很多的前端開發人員都有實際的開發實踐。
此文章的Token登錄機制主要針對于無實際開發經驗或者開發過簡單登錄機制的人員,如果你是大佬幾乎可以略過了,如果你感興趣或者閑來無事也可以稍微瞅它一瞅。
此文章不會教你一步一步的實現一套登錄邏輯,只會結合zanePerfor項目闡述它的登錄機制,講明白其原理比寫一堆代碼來的更實在和簡單。
zanePerfor項目的主要技術棧是 egg.js、redis和mongodb, 如果你不懂沒關系,因為他們都只是簡單使用,很容易理解。
登錄實現結果:cookie在項目中的作用如果用戶未注冊時先注冊然后直接登錄
用戶每次登錄都會動態生成session令牌
同一賬號在同一時刻只能在一個地方登錄
我們知道http是無狀態的,因此如果要知道用戶某次請求是否登錄就需要帶一定的標識,瀏覽器端http請求帶標識常用的方式有兩種:1、使用cookie附帶標識,2、使用header信息頭附帶標識。
這里我們推薦的方式是使用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 }); }; } }; }
User表結構說明前端的邏輯代碼很簡單,就是統一的判斷返回code, 如果未登錄則跳轉到登錄頁面。
// 代碼路徑 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 }, // 用戶訪問時間 });
Node Servers端登錄邏輯用戶表中 usertoken 字段比較重要,它表示每次用戶登錄時動態生成的Token令牌key, 也是存在在redis中用戶信息的key值,此值每次用戶登錄時都會更新,并且是隨機和唯一的。
我們先來一張登錄的頁面
// 代碼路徑 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表來進行邏輯的梳理。
Servers 端用戶登錄校驗中間件每次登錄前都會清除上一次在redis中的登錄態信息,所以上一次的登錄令牌對應的redis信息會失效,因此我們只需要做一個校驗用戶Token的信息在redis中是否存在即可判斷用戶當前登錄態是否有效。
清除上一次登錄態信息之后立即生成一個隨機并唯一的key值做為新的Token令牌,并更新redis中Token的令牌信息 和 設置新的cookie令牌,這樣就保證了以前的登錄態失效,當前的登錄態有效。
redis 和 cookie 都設置相同的過期時間,以保證Token的時效性和安全性。
cookie的httpOnly 我們需要開啟,這樣就保證的Token的不可操作性,encrypt 和 signed參數是egg.js 的參數,主要負責對cookie進行加密,讓前端的cookie不已明文的方式呈現,提高安全性。
最后再更新用戶的Token令牌信息,以保證用戶的Token每次都是最新的,也用以下次登錄時的清除操作。
中間件的概念相信大家都不陌生,用過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(); }邏輯梳理:
到此zanePerfor的Token校驗機制其實已經完全實現完了,只是未做整體的總結,下面來繼續的完成注冊的邏輯。 用戶注冊邏輯實現 業務代碼如下:首先會獲得上傳的token令牌,這里cookie.get方法的 encrypt 和 signed 需要為true,這會把Token解析為明文。
在finUserForToken方法中主要是獲取Token令牌對應的redis用戶信息,只有當用戶的信息為真值時才會通過校驗
在中間件這一環節還有一個比較常規的驗證 就是 驗證請求的 referer, referer也是瀏覽器請求時自帶的,在瀏覽器端不可操作,這相對的增加了一些安全性(項目中暫未做,這個驗證比較簡單,如果有需要的自己去實現)。
// 代碼路徑 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/per...
總結:前端封裝統一的登錄驗證,項目中 code 1004 為用戶未登錄,1000為成功。
user數據表中儲存一個usertoken字段,此字段是隨機并唯一的標識,在注冊時存入此字段,在每次登錄時更新此字段。
瀏覽器端的Token令牌即usertoken字段,redis的每個Token存儲的是相應的用戶信息。
每次登錄時清除上一次用戶的登錄信息,即清除redis登錄校驗信息,這樣就能保證同一用戶同一時間只能在一個地方登錄。
usertoken字段是隨時在變的,redis用戶信息和cookie Token令牌都有過期時間,cookie經過加密和httpOnly,更大的保證了Token的安全性。
對所有需要校驗的http請求做中間件校驗,通過Token令牌獲取redis用戶信息并驗證,驗證即通過,驗證失敗則重新去登錄。
第三方登錄使用token做用戶是否重復校驗,第一次時登錄注冊,第二次登錄時則走登錄邏輯。
原文地址:https://blog.seosiwei.com/det...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/19442.html
摘要:概念英文全稱,單點登錄。登錄如上述流程圖一致。系統和系統使用認證登錄。退出上圖,表示的是從某一個系統退出的流程圖。與的關系如果企業有多個管理系統,現由原來的每個系統都有一個登錄,調整為統一登錄認證。 概念 SSO 英文全稱 Single Sign On,單點登錄。 在多個應用系統中,只需要登錄一次,就可以訪問其他相互信任的應用系統。 比如:淘寶網(www.taobao.com),天貓網...
摘要:什么是鑒權鑒權是指驗證用戶是否擁有訪問系統的權利。傳統的鑒權是通過密碼來驗證的。這種方式的前提是,每個獲得密碼的用戶都已經被授權。接下來就一一介紹一下這三種鑒權方式。 在系統級項目開發時常常會遇到一個問題就是鑒權,身為一個前端來說可能我們距離鑒權可能比較遠,一般來說我們也只是去應用,并沒有對權限這一部分進行深入的理解。 什么是鑒權 鑒權:是指驗證用戶是否擁有訪問系統的權利。傳統的鑒權是...
摘要:本文講解的就是授權登錄的教程。從拿到的用戶信息如下圖最終效果參與文章如何設計第三方授權登錄的用戶表第三方授權登錄的時候,第三方的用戶信息是存數據庫原有的表還是新建一張表呢答案這得看具體項目了,做法多種,請看下文。 showImg(https://segmentfault.com/img/remote/1460000018372844?w=1210&h=828); 需求:在網站上想評論一...
摘要:訪問令牌表示授權授予授予的范圍持續時間和其他屬性。該規范還定義了一組通用客戶端元數據字段和值,供客戶端在注冊期間使用。授權服務器可以為客戶端元數據中遺漏的任何項提供默認值。 以前的開發模式是以MVC為主,但是隨著互聯網行業快速的發展逐漸的演變成了前后端分離,若項目中需要做登錄的話,那么token成為前后端唯一的一個憑證。 token即標志、記號的意思,在IT領域也叫作令牌。在計算機身份...
閱讀 1230·2021-11-11 16:54
閱讀 1744·2021-10-13 09:40
閱讀 940·2021-10-08 10:05
閱讀 3503·2021-09-22 15:50
閱讀 3706·2021-09-22 15:41
閱讀 1800·2021-09-22 15:08
閱讀 2345·2021-09-07 10:24
閱讀 3578·2019-08-30 12:52