摘要:的優(yōu)勢使用簡單,性能足夠強(qiáng)悍,儲存空間無限制,多臺服務(wù)器可以使用統(tǒng)一的登錄態(tài),登錄邏輯代碼的解耦。每次登錄時清除上一次用戶的登錄信息,即清除登錄校驗(yàn)信息,這樣就能保證同一用戶同一時間只能在一個地方登錄。
HI!,你好,我是zane,zanePerfor是一款我開發(fā)的一個前端性能監(jiān)控平臺,現(xiàn)在支持web瀏覽器端和微信小程序端。
我定義為一款完整,高性能,高可用的前端性能監(jiān)控系統(tǒng),這是未來會達(dá)到的目的,現(xiàn)今的架構(gòu)也基本支持了高可用,高性能的部署。實(shí)際上還不夠,在很多地方還有優(yōu)化的空間,我會持續(xù)的優(yōu)化和升級。
開源不易,如果你也熱愛技術(shù),擁抱開源,希望能小小的支持給個star。
項(xiàng)目的github地址:https://github.com/wangweiang...
項(xiàng)目開發(fā)文檔說明:https://blog.seosiwei.com/per...
談起Token登錄機(jī)制,相信絕大部分人都不陌生,相信很多的前端開發(fā)人員都有實(shí)際的開發(fā)實(shí)踐。
此文章的Token登錄機(jī)制主要針對于無實(shí)際開發(fā)經(jīng)驗(yàn)或者開發(fā)過簡單登錄機(jī)制的人員,如果你是大佬幾乎可以略過了,如果你感興趣或者閑來無事也可以稍微瞅它一瞅。
此文章不會教你一步一步的實(shí)現(xiàn)一套登錄邏輯,只會結(jié)合zanePerfor項(xiàng)目闡述它的登錄機(jī)制,講明白其原理比寫一堆代碼來的更實(shí)在和簡單。
zanePerfor項(xiàng)目的主要技術(shù)棧是 egg.js、redis和mongodb, 如果你不懂沒關(guān)系,因?yàn)樗麄兌贾皇呛唵问褂茫苋菀桌斫狻?/p> 登錄實(shí)現(xiàn)結(jié)果:
cookie在項(xiàng)目中的作用如果用戶未注冊時先注冊然后直接登錄
用戶每次登錄都會動態(tài)生成session令牌
同一賬號在同一時刻只能在一個地方登錄
我們知道http是無狀態(tài)的,因此如果要知道用戶某次請求是否登錄就需要帶一定的標(biāo)識,瀏覽器端http請求帶標(biāo)識常用的方式有兩種:1、使用cookie附帶標(biāo)識,2、使用header信息頭附帶標(biāo)識。
這里我們推薦的方式是使用cooke附帶標(biāo)識,因?yàn)樗喈?dāng)于來說更安全和更容易操作。
更安全體現(xiàn)在:cookie只能在同域下傳輸,還可以設(shè)置httpOnly來禁止js的更改。
更容易操作體現(xiàn)在:cookie傳輸是瀏覽器請求時自帶的傳輸頭信息,我們不需要額外的操作,cookie還能精確到某一個路徑,并且可以設(shè)置過期時間自動過期,這樣就顯得更可控。
當(dāng)然header信息頭也有它的優(yōu)勢和用武之地,這里不做闡述。
一般的項(xiàng)目我們會把識別用戶的標(biāo)識放存放在Session中,但是Session有其使用的局限性。
Session的局限:Session 默認(rèn)存放在 Cookie 中,但是如果我們的 Session 對象過于龐大,瀏覽器可能拒絕保存,這樣就失去了數(shù)據(jù)的完整性。當(dāng) Session 過大時還會對每次http請求帶來額外的開銷。還有一個比較大的局限性是Session存放在單臺服務(wù)器中,當(dāng)有多臺服務(wù)器時無法保證統(tǒng)一的登錄態(tài)。還會帶來代碼的強(qiáng)耦合性,不能使得登錄邏輯代碼解耦。
因此這里引入redis進(jìn)行用戶身份識別的儲存。
redis的優(yōu)勢:redis使用簡單,redis性能足夠強(qiáng)悍,儲存空間無限制,多臺服務(wù)器可以使用統(tǒng)一的登錄態(tài),登錄邏輯代碼的解耦。
前端統(tǒng)一登錄態(tài)封裝前端統(tǒng)一登錄態(tài)應(yīng)該是每位前端童鞋都做過的事情,下面以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 時統(tǒng)一使用this.error方法進(jìn)行處理 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表示未登錄 需要統(tǒng)一走登錄頁面 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回調(diào) json.error && json.error(data); } else { //直接彈出錯誤信息 popup.alert({ type: "msg", title: data.desc }); }; } }; }
User表結(jié)構(gòu)說明前端的邏輯代碼很簡單,就是統(tǒng)一的判斷返回code, 如果未登錄則跳轉(zhuǎn)到登錄頁面。
// 代碼路徑 app/model/user.js const UserSchema = new Schema({ user_name: { type: String }, // 用戶名稱 pass_word: { type: String }, // 用戶密碼 system_ids: { type: Array }, // 用戶所擁有的系統(tǒng)Id is_use: { type: Number, default: 0 }, // 是否禁用 0:正常 1:禁用 level: { type: Number, default: 1 }, // 用戶等級(0:管理員,1:普通用戶) token: { type: String }, // 用戶秘鑰 usertoken: { type: String }, // 用戶登錄態(tài)秘鑰 create_time: { type: Date, default: Date.now }, // 用戶訪問時間 });
Node Servers端登錄邏輯用戶表中 usertoken 字段比較重要,它表示每次用戶登錄時動態(tài)生成的Token令牌key, 也是存在在redis中用戶信息的key值,此值每次用戶登錄時都會更新,并且是隨機(jī)和唯一的。
我們先來一張登錄的頁面
// 代碼路徑 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("用戶被凍結(jié)不能登錄,請聯(lián)系管理員!"); // 清空以前的登錄態(tài) if (userInfo.usertoken) this.app.redis.set(`${userInfo.usertoken}_user_login`, ""); // 設(shè)置新的redis登錄態(tài) const random_key = this.app.randomString(); this.app.redis.set(`${random_key}_user_login`, JSON.stringify(userInfo), "EX", this.app.config.user_login_timeout); // 設(shè)置登錄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表來進(jìn)行邏輯的梳理。
Servers 端用戶登錄校驗(yàn)中間件每次登錄前都會清除上一次在redis中的登錄態(tài)信息,所以上一次的登錄令牌對應(yīng)的redis信息會失效,因此我們只需要做一個校驗(yàn)用戶Token的信息在redis中是否存在即可判斷用戶當(dāng)前登錄態(tài)是否有效。
清除上一次登錄態(tài)信息之后立即生成一個隨機(jī)并唯一的key值做為新的Token令牌,并更新redis中Token的令牌信息 和 設(shè)置新的cookie令牌,這樣就保證了以前的登錄態(tài)失效,當(dāng)前的登錄態(tài)有效。
redis 和 cookie 都設(shè)置相同的過期時間,以保證Token的時效性和安全性。
cookie的httpOnly 我們需要開啟,這樣就保證的Token的不可操作性,encrypt 和 signed參數(shù)是egg.js 的參數(shù),主要負(fù)責(zé)對cookie進(jìn)行加密,讓前端的cookie不已明文的方式呈現(xiàn),提高安全性。
最后再更新用戶的Token令牌信息,以保證用戶的Token每次都是最新的,也用以下次登錄時的清除操作。
中間件的概念相信大家都不陌生,用過koa,express和redux都應(yīng)該知道,egg.js的中間件來自于與koa,在這里就不說概念了。
在zanePerfor項(xiàng)目中我們只需要對所有需要進(jìn)行登錄校驗(yàn)的路由(請求)進(jìn)行中間件校驗(yàn)即可。
在egg中可這樣使用:// 代碼來源 app/router/api.js // 獲得controller 和 middleware(中間件) const { controller, middleware } = app; // 對需要校驗(yàn)的路由進(jìn)行校驗(yàn) // 退出登錄 apiV1Router.get("user/logout", tokenRequired, user.logout);業(yè)務(wù)代碼如下:
// 代碼路徑 app/middleware/token_required.js // Token校驗(yàn)中間件 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 // 根據(jù)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: "用戶被凍結(jié)不能登錄,請聯(lián)系管理員!" }; } else { return null; } return await this.ctx.model.User.findOne({ token: user_info.token }).exec(); }邏輯梳理:
到此zanePerfor的Token校驗(yàn)機(jī)制其實(shí)已經(jīng)完全實(shí)現(xiàn)完了,只是未做整體的總結(jié),下面來繼續(xù)的完成注冊的邏輯。 用戶注冊邏輯實(shí)現(xiàn) 業(yè)務(wù)代碼如下:首先會獲得上傳的token令牌,這里cookie.get方法的 encrypt 和 signed 需要為true,這會把Token解析為明文。
在finUserForToken方法中主要是獲取Token令牌對應(yīng)的redis用戶信息,只有當(dāng)用戶的信息為真值時才會通過校驗(yàn)
在中間件這一環(huán)節(jié)還有一個比較常規(guī)的驗(yàn)證 就是 驗(yàn)證請求的 referer, referer也是瀏覽器請求時自帶的,在瀏覽器端不可操作,這相對的增加了一些安全性(項(xiàng)目中暫未做,這個驗(yàn)證比較簡單,如果有需要的自己去實(shí)現(xiàn))。
// 代碼路徑 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(); // 設(shè)置redis登錄態(tài) this.app.redis.set(`${token}_user_login`, JSON.stringify(result), "EX", this.app.config.user_login_timeout); // 設(shè)置登錄cookie this.ctx.cookies.set("usertoken", token, { maxAge: this.app.config.user_login_timeout * 1000, httpOnly: true, encrypt: true, signed: true, }); return result; }
退出登錄邏輯用戶注冊的代碼比較簡單,首先檢測用戶是否存在,不存在則儲存
生成動態(tài)并唯一的Token令牌,并保持?jǐn)?shù)據(jù)到redis 和設(shè)置 cookie令牌信息, 這里都設(shè)置相同的過期時間,并加密cookie信息和httpOnly。
退出登錄邏輯很簡單,直接清除用戶Token對應(yīng)的redis信息和cookie token令牌即可。
// 登出 logout(usertoken) { this.ctx.cookies.set("usertoken", ""); this.app.redis.set(`${usertoken}_user_login`, ""); return {}; }凍結(jié)用戶邏輯
凍結(jié)用戶的邏輯也比較簡單,唯一需要注意的是,凍結(jié)的時候需要清除用戶Token對應(yīng)的redis信息。
// 凍結(jié)解凍用戶 async setIsUse(id, isUse, usertoken) { // 凍結(jié)用戶信息 isUse = isUse * 1; const result = await this.ctx.model.User.update( { _id: id }, { is_use: isUse }, { multi: true } ).exec(); // 清空登錄態(tài) if (usertoken) this.app.redis.set(`${usertoken}_user_login`, ""); return result; }刪除用戶邏輯
刪除用戶邏輯跟凍結(jié)用戶邏輯一致,也需要注意清除用戶Token對應(yīng)的redis信息。
// 刪除用戶 async delete(id, usertoken) { // 刪除 const result = await this.ctx.model.User.findOneAndRemove({ _id: id }).exec(); // 清空登錄態(tài) if (usertoken) this.app.redis.set(`${usertoken}_user_login`, ""); return result; }第三方github登錄說明
根據(jù)zanePerfor的登錄校驗(yàn)機(jī)制可以得出以下的結(jié)論:
User表的用戶名必須存在,密碼可無,并且用戶名在代碼中強(qiáng)校驗(yàn)不能重復(fù),但是在數(shù)據(jù)庫中用戶名是可以重復(fù)的。
usertoken字段很重要,是實(shí)現(xiàn)所有Token機(jī)制的核心字段,每次登錄和注冊都會是隨機(jī)并唯一的值
基于以上兩點(diǎn)做第三方登錄我們只需要實(shí)現(xiàn)以下幾點(diǎn)即可:
只要給用戶名賦值即可,因?yàn)橛脩裘艽a登錄和第三方登錄是兩套邏輯,因此用戶名可以重復(fù),這就解決了第三方登錄一定不會存在用戶已注冊的提示。
第一次登錄時注冊用戶,并把第三方的用戶名當(dāng)做表的用戶名,第三方的secret作為用戶的token字段。
第二次登錄時使用token字段檢測用戶是否已注冊,已注冊走登錄邏輯,未注冊走注冊邏輯。
// 代碼地址 app/service/user.js // github register 核心注冊邏輯 async githubRegister(data = {}) { // 此字段為github用戶名 const login = data.login; // 此字段為github 唯一用戶標(biāo)識 const token = data.node_id; let userInfo = {}; if (!login || !token) { userInfo = { desc: "github 權(quán)限驗(yàn)證失敗, 請重試!" }; return; } // 通過token去查詢用戶是否存在 userInfo = await this.getUserInfoForGithubId(token); // 身材Token隨機(jī)并唯一令牌 const random_key = this.app.randomString(); if (userInfo.token) { // 存在則直接登錄 if (userInfo.is_use !== 0) { userInfo = { desc: "用戶被凍結(jié)不能登錄,請聯(lián)系管理員!" }; } else { // 清空以前的登錄態(tài) if (userInfo.usertoken) this.app.redis.set(`${userInfo.usertoken}_user_login`, ""); // 設(shè)置redis登錄態(tài) this.app.redis.set(`${random_key}_user_login`, JSON.stringify(userInfo), "EX", this.app.config.user_login_timeout); // 設(shè)置登錄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(); // 設(shè)置redis登錄態(tài) this.app.redis.set(`${random_key}_user_login`, JSON.stringify(userInfo), "EX", this.app.config.user_login_timeout); // 設(shè)置登錄cookie this.ctx.cookies.set("usertoken", random_key, { maxAge: this.app.config.user_login_timeout * 1000, httpOnly: true, encrypt: true, signed: true, }); } return userInfo; }
詳細(xì)的github第三方授權(quán)方式請參考:https://blog.seosiwei.com/per...
總結(jié):前端封裝統(tǒng)一的登錄驗(yàn)證,項(xiàng)目中 code 1004 為用戶未登錄,1000為成功。
user數(shù)據(jù)表中儲存一個usertoken字段,此字段是隨機(jī)并唯一的標(biāo)識,在注冊時存入此字段,在每次登錄時更新此字段。
瀏覽器端的Token令牌即usertoken字段,redis的每個Token存儲的是相應(yīng)的用戶信息。
每次登錄時清除上一次用戶的登錄信息,即清除redis登錄校驗(yàn)信息,這樣就能保證同一用戶同一時間只能在一個地方登錄。
usertoken字段是隨時在變的,redis用戶信息和cookie Token令牌都有過期時間,cookie經(jīng)過加密和httpOnly,更大的保證了Token的安全性。
對所有需要校驗(yàn)的http請求做中間件校驗(yàn),通過Token令牌獲取redis用戶信息并驗(yàn)證,驗(yàn)證即通過,驗(yàn)證失敗則重新去登錄。
第三方登錄使用token做用戶是否重復(fù)校驗(yàn),第一次時登錄注冊,第二次登錄時則走登錄邏輯。
原文地址:https://blog.seosiwei.com/det...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/19442.html
摘要:概念英文全稱,單點(diǎn)登錄。登錄如上述流程圖一致。系統(tǒng)和系統(tǒng)使用認(rèn)證登錄。退出上圖,表示的是從某一個系統(tǒng)退出的流程圖。與的關(guān)系如果企業(yè)有多個管理系統(tǒng),現(xiàn)由原來的每個系統(tǒng)都有一個登錄,調(diào)整為統(tǒng)一登錄認(rèn)證。 概念 SSO 英文全稱 Single Sign On,單點(diǎn)登錄。 在多個應(yīng)用系統(tǒng)中,只需要登錄一次,就可以訪問其他相互信任的應(yīng)用系統(tǒng)。 比如:淘寶網(wǎng)(www.taobao.com),天貓網(wǎng)...
摘要:什么是鑒權(quán)鑒權(quán)是指驗(yàn)證用戶是否擁有訪問系統(tǒng)的權(quán)利。傳統(tǒng)的鑒權(quán)是通過密碼來驗(yàn)證的。這種方式的前提是,每個獲得密碼的用戶都已經(jīng)被授權(quán)。接下來就一一介紹一下這三種鑒權(quán)方式。 在系統(tǒng)級項(xiàng)目開發(fā)時常常會遇到一個問題就是鑒權(quán),身為一個前端來說可能我們距離鑒權(quán)可能比較遠(yuǎn),一般來說我們也只是去應(yīng)用,并沒有對權(quán)限這一部分進(jìn)行深入的理解。 什么是鑒權(quán) 鑒權(quán):是指驗(yàn)證用戶是否擁有訪問系統(tǒng)的權(quán)利。傳統(tǒng)的鑒權(quán)是...
摘要:本文講解的就是授權(quán)登錄的教程。從拿到的用戶信息如下圖最終效果參與文章如何設(shè)計(jì)第三方授權(quán)登錄的用戶表第三方授權(quán)登錄的時候,第三方的用戶信息是存數(shù)據(jù)庫原有的表還是新建一張表呢答案這得看具體項(xiàng)目了,做法多種,請看下文。 showImg(https://segmentfault.com/img/remote/1460000018372844?w=1210&h=828); 需求:在網(wǎng)站上想評論一...
摘要:訪問令牌表示授權(quán)授予授予的范圍持續(xù)時間和其他屬性。該規(guī)范還定義了一組通用客戶端元數(shù)據(jù)字段和值,供客戶端在注冊期間使用。授權(quán)服務(wù)器可以為客戶端元數(shù)據(jù)中遺漏的任何項(xiàng)提供默認(rèn)值。 以前的開發(fā)模式是以MVC為主,但是隨著互聯(lián)網(wǎng)行業(yè)快速的發(fā)展逐漸的演變成了前后端分離,若項(xiàng)目中需要做登錄的話,那么token成為前后端唯一的一個憑證。 token即標(biāo)志、記號的意思,在IT領(lǐng)域也叫作令牌。在計(jì)算機(jī)身份...
閱讀 1231·2021-11-11 16:54
閱讀 1748·2021-10-13 09:40
閱讀 943·2021-10-08 10:05
閱讀 3508·2021-09-22 15:50
閱讀 3711·2021-09-22 15:41
閱讀 1807·2021-09-22 15:08
閱讀 2350·2021-09-07 10:24
閱讀 3580·2019-08-30 12:52