今天
作者:ssshooter
不知不覺也寫得比較長(zhǎng)了,一次看不完建議收藏夾!本文主要解釋與請(qǐng)求狀態(tài)相關(guān)的術(shù)語(yǔ)(cookie、session、token)和幾種常見登錄的實(shí)現(xiàn)方式,希望大家看完本文后可以有比較清晰的理解,有感到迷惑的地方請(qǐng)?jiān)谠u(píng)論區(qū)提出。
眾所周知,http 是無(wú)狀態(tài)協(xié)議,瀏覽器和服務(wù)器不可能憑協(xié)議的實(shí)現(xiàn)辨別請(qǐng)求的上下文。
于是 cookie 登場(chǎng),既然協(xié)議本身不能分辨鏈接,那就在請(qǐng)求頭部手動(dòng)帶著上下文信息吧。
舉個(gè)例子,以前去旅游的時(shí)候,到了景區(qū)可能會(huì)需要存放行李,被大包小包壓著,旅游也不開心啦。在存放行李后,服務(wù)員會(huì)給你一個(gè)牌子,上面寫著你的行李放在哪個(gè)格子,離開時(shí),你就能憑這個(gè)牌子和上面的數(shù)字成功取回行李。
cookie 做的正是這么一件事,旅客就像客戶端,寄存處就像服務(wù)器,憑著寫著數(shù)字的牌子,寄存處(服務(wù)器)就能分辨出不同旅客(客戶端)。
你會(huì)不會(huì)想到,如果牌子被偷了怎么辦,cookie 也會(huì)被偷嗎?確實(shí)會(huì),這就是一個(gè)很常被提到的網(wǎng)絡(luò)安全問(wèn)題——CSRF。
cookie 誕生初似乎是用于電商存放用戶購(gòu)物車一類的數(shù)據(jù),但現(xiàn)在前端擁有兩個(gè) storage(local、session),兩種數(shù)據(jù)庫(kù)(websql、IndexedDB),根本不愁信息存放問(wèn)題,所以現(xiàn)在基本上 100% 都是在連接上證明客戶端的身份。例如登錄之后,服務(wù)器給你一個(gè)標(biāo)志,就存在 cookie 里,之后再連接時(shí),都會(huì)自動(dòng)帶上 cookie,服務(wù)器便分清誰(shuí)是誰(shuí)。另外,cookie 還可以用于跟蹤一個(gè)用戶,這就產(chǎn)生了隱私問(wèn)題,于是也就有了“禁用 cookie”這個(gè)選項(xiàng)(然而現(xiàn)在這個(gè)時(shí)代禁用 cookie 是挺麻煩的事情)。
現(xiàn)實(shí)世界的例子明白了,在計(jì)算機(jī)中怎么才能設(shè)置 cookie 呢?一般來(lái)說(shuō),安全起見,cookie 都是依靠 set-cookie
頭設(shè)置,且不允許 JavaScript 設(shè)置。
Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure
// Multiple attributes are also possible, for example:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly
其中 <cookie-name>=<cookie-value>
這樣的 kv 對(duì),內(nèi)容隨你定,另外還有 HttpOnly、SameSite 等配置,一條 Set-Cookie
只配置一項(xiàng) cookie。
/
全匹配Secure 和 HttpOnly 是強(qiáng)烈建議開啟的。SameSite 選項(xiàng)需要根據(jù)實(shí)際情況討論,因?yàn)?SameSite 可能會(huì)導(dǎo)致即使你用 CORS 解決了跨越問(wèn)題,依然會(huì)因?yàn)檎?qǐng)求沒自帶 cookie 引起一系列問(wèn)題,一開始還以為是 axios 配置問(wèn)題,繞了一大圈,然而根本沒關(guān)系。
其實(shí)因?yàn)?Chrome 在某一次更新后把沒設(shè)置 SameSite
默認(rèn)為 Lax
,你不在服務(wù)器手動(dòng)把 SameSite
設(shè)置為 None
就不會(huì)自動(dòng)帶 cookie 了。
參考 MDN,cookie 的發(fā)送格式如下(其中 PHPSESSID 相關(guān)內(nèi)容下面會(huì)提到):
Cookie: <cookie-list>
Cookie: name=value
Cookie: name=value; name2=value2; name3=value3
Cookie: PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1
在發(fā)送 cookie 時(shí),并不會(huì)把上面提到的 Expires
等配置傳到服務(wù)器,因?yàn)榉?wù)器在設(shè)置后就不需要關(guān)心這些信息了,只要現(xiàn)代瀏覽器運(yùn)作正常,收到的 cookie 就是沒問(wèn)題的。
從 cookie 說(shuō)到 session,是因?yàn)?session 才是真正的“信息”,如上面提到的,cookie 是容器,里面裝著 PHPSESSID=298zf09hf012fh2;
,這就是一個(gè) session ID。
不知道 session 和 session id 會(huì)不會(huì)讓你看得有點(diǎn)頭暈?
當(dāng)初 session 的存在就是要為客戶端和服務(wù)器連接提供的信息,所以我將 session 理解為信息,而 session id 是獲取信息的鑰匙,通常是一串唯一的哈希碼。
接下來(lái)分析兩個(gè) node.js express 的中間件,理解兩種 session 的實(shí)現(xiàn)方式。
session 信息可以儲(chǔ)存在客戶端,如 cookie-session,也可以儲(chǔ)存在服務(wù)器,如 express-session。使用 session ID 就是把 session 放在服務(wù)器里,用 cookie 里的 id 尋找服務(wù)器的信息。
對(duì)于 cookie-session 庫(kù),比較容易理解,其實(shí)就是把所有信息加密后塞到 cookie 里。其中涉及到 cookies 庫(kù)。在設(shè)置 session 時(shí)其實(shí)就是調(diào)用 cookies.set,把信息寫到 set-cookie 里,再返回瀏覽器。換言之,取值和賦值的本質(zhì)都是操作 cookie。
瀏覽器在接收到 set-cookie 頭后,會(huì)把信息寫到 cookie 里。在下次發(fā)送請(qǐng)求時(shí),信息又通過(guò) cookie 原樣帶回來(lái),所以服務(wù)器什么東西都不用存,只負(fù)責(zé)獲取和處理 cookie 里的信息,這種實(shí)現(xiàn)方法不需要 session ID。
這是一段使用 cookie-session 中間件為請(qǐng)求添加 cookie 的代碼:
const express = require('express')
var cookieSession = require('cookie-session')
const app = express()
app.use(
cookieSession({
name: 'session',
keys: [
/* secret keys */
'key',
],
// Cookie Options
maxAge: 24 * 60 * 60 * 1000, // 24 hours
})
)
app.get('/', function(req, res) {
req.session.test = 'hey'
res.json({
wow: 'crazy',
})
})
app.listen(3001)
在通過(guò) app.use(cookieSession())
使用中間件之前,請(qǐng)求是不會(huì)設(shè)置 cookie 的,添加后再訪問(wèn)(并且在設(shè)置 req.session 后,若不添加 session 信息就沒必要、也沒內(nèi)容寫到 cookie 里),就能看到服務(wù)器響應(yīng)頭部新增了下面兩行,分別寫入 session 和 session.sig:
Set-Cookie: session=eyJ0ZXN0IjoiaGV5In0=; path=/; expires=Tue, 23 Feb 2021 01:07:05 GMT; httponly
Set-Cookie: session.sig=QBoXofGvnXbVoA8dDmfD-GMMM6E; path=/; expires=Tue, 23 Feb 2021 01:07:05 GMT; httponly
然后你就能在 DevTools 的 Application 標(biāo)簽看到 cookie 成功寫入。session 的值 eyJ0ZXN0IjoiaGV5In0=
通過(guò) base64 解碼即可得到 {"test":"hey"}
,這就是所謂的“將 session 信息放到客戶端”,因?yàn)?base64 編碼并不是加密,這就跟明文傳輸沒啥區(qū)別,所以請(qǐng)不要在客戶端 session 里放用戶密碼之類的機(jī)密信息。
即使現(xiàn)代瀏覽器和服務(wù)器做了一些約定,例如使用 https、跨域限制、還有上面提到 cookie 的 httponly 和 sameSite 配置等,保障了 cookie 安全。但是想想,傳輸安全保障了,如果有人偷看你電腦里的 cookie,密碼又恰好存在 cookie,那就能無(wú)聲無(wú)息地偷走密碼。相反的,只放其他信息或是僅僅證明“已登錄”標(biāo)志的話,只要退出一次,這個(gè) cookie 就失效了,算是降低了潛在危險(xiǎn)。
說(shuō)回第二個(gè)值 session.sig,它是一個(gè) 27 字節(jié)的 SHA1 簽名,用以校驗(yàn) session 是否被篡改,是 cookie 安全的又一層保障。
既然要儲(chǔ)存在服務(wù)器,那么 express-session 就需要一個(gè)容器 store,它可以是內(nèi)存、redis、mongoDB 等等等等,內(nèi)存應(yīng)該是最快的,但是重啟程序就沒了,redis 可以作為備選,用數(shù)據(jù)庫(kù)存 session 的場(chǎng)景感覺不多。
express-session 的源碼沒 cookie-session 那么簡(jiǎn)明易懂,里面有一個(gè)有點(diǎn)繞的問(wèn)題,req.session
到底是怎么插入的?
不關(guān)注實(shí)現(xiàn)可以跳過(guò)下面幾行,有興趣的話可以跟著思路看看 express-session 的源碼:
我們可以從 .session =
這個(gè)關(guān)鍵詞開始找,找到:
store.generate
否決這個(gè),容易看出這個(gè)是初始化使用的Store.prototype.createSession
這個(gè)是根據(jù) req 和 sess 參數(shù)在 req 中設(shè)置 session 屬性,沒錯(cuò),就是你了于是全局搜索 createSession
,鎖定 index 里的 inflate
(就是填充的意思)函數(shù)。
最后尋找 inflate
的調(diào)用點(diǎn),是使用 sessionID 為參數(shù)的 store.get
的回調(diào)函數(shù),一切說(shuō)得通啦——
在監(jiān)測(cè)到客戶端送來(lái)的 cookie 之后,可以從 cookie 獲取 sessionID,再使用 id 在 store 中獲取 session 信息,掛到 req.session
上,經(jīng)過(guò)這個(gè)中間件,你就能順利地使用 req 中的 session。
那賦值怎么辦呢?這就和上面儲(chǔ)存在客戶端不同了,上面要修改客戶端 cookie 信息,但是對(duì)于儲(chǔ)存在服務(wù)器的情況,你修改了 session 那就是“實(shí)實(shí)在在地修改”了嘛,不用其他花里胡哨的方法,內(nèi)存中的信息就是修改了,下次獲取內(nèi)存里的對(duì)應(yīng)信息也是修改后的信息。(僅限于內(nèi)存的實(shí)現(xiàn)方式,使用數(shù)據(jù)庫(kù)時(shí)仍需要額外的寫入)
在請(qǐng)求沒有 session id 的情況下,通過(guò) store.generate
創(chuàng)建新的 session,在你寫 session 的時(shí)候,cookie 可以不改變,只要根據(jù)原來(lái)的 cookie 訪問(wèn)內(nèi)存里的 session 信息就可以了。
var express = require('express')
var parseurl = require('parseurl')
var session = require('express-session')
var app = express()
app.use(
session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
})
)
app.use(function(req, res, next) {
if (!req.session.views) {
req.session.views = {}
}
// get the url pathname
var pathname = parseurl(req).pathname
// count the views
req.session.views[pathname] = (req.session.views[pathname] || 0) + 1
next()
})
app.get('/foo', function(req, res, next) {
res.json({
session: req.session,
})
})
app.get('/bar', function(req, res, next) {
res.send('you viewed this page ' + req.session.views['/bar'] + ' times')
})
app.listen(3001)
首先還是計(jì)算機(jī)世界最重要的哲學(xué)問(wèn)題:時(shí)間和空間的抉擇。
儲(chǔ)存在客戶端的情況,解放了服務(wù)器存放 session 的內(nèi)存,但是每次都帶上一堆 base64 處理的 session 信息,如果量大的話傳輸就會(huì)很緩慢。
儲(chǔ)存在服務(wù)器相反,用服務(wù)器的內(nèi)存拯救了帶寬。
另外,在退出登錄的實(shí)現(xiàn)和結(jié)果,也是有區(qū)別的。
儲(chǔ)存在服務(wù)器的情況就很簡(jiǎn)單,如果 req.session.isLogin = true
是登錄,那么 req.session.isLogin = false
就是退出。
但是狀態(tài)存放在客戶端要做到真正的“即時(shí)退出登錄”就很困難了。你可以在 session 信息里加上過(guò)期日期,也可以直接依靠 cookie 的過(guò)期日期,過(guò)期之后,就當(dāng)是退出了。
但是如果你不想等到 session 過(guò)期,現(xiàn)在就想退出登錄!怎么辦?認(rèn)真想想你會(huì)發(fā)現(xiàn),僅僅依靠客戶端儲(chǔ)存的 session 信息真的沒有辦法做到。
即使你通過(guò) req.session = null
刪掉客戶端 cookie,那也只是刪掉了,但是如果有人曾經(jīng)把 cookie 復(fù)制出來(lái)了,那他手上的 cookie 直到 session 信息里的過(guò)期時(shí)間前,都是有效的。
說(shuō)“即時(shí)退出登錄”有點(diǎn)標(biāo)題黨的意味,其實(shí)我想表達(dá)的是,你沒辦法立即廢除一個(gè) session,這可能會(huì)造成一些隱患。
session 說(shuō)完了,那么出現(xiàn)頻率超高的關(guān)鍵字 token 又是什么?
不妨谷歌搜一下 token 這個(gè)詞,可以看到冒出來(lái)幾個(gè)(年紀(jì)大的人)比較熟悉的圖片:密碼器。過(guò)去網(wǎng)上銀行不是只要短信認(rèn)證就能轉(zhuǎn)賬,還要經(jīng)過(guò)一個(gè)密碼器,上面顯示著一個(gè)變動(dòng)的密碼,在轉(zhuǎn)賬時(shí)你需要輸入密碼器中的代碼才能轉(zhuǎn)賬,這就是 token 現(xiàn)實(shí)世界中的例子。憑借一串碼或是一個(gè)數(shù)字證明自己身份,這事情不就和上面提到的行李問(wèn)題還是一樣的嗎……
**其實(shí)本質(zhì)上 token 的功能就是和 session id 一模一樣。**你把 session id 說(shuō)成 session token 也沒什么問(wèn)題(Wikipedia 里就寫了這個(gè)別名)。
其中的區(qū)別在于,session id 一般存在 cookie 里,自動(dòng)帶上;token 一般是要你主動(dòng)放在請(qǐng)求中,例如設(shè)置請(qǐng)求頭的 Authorization
為 bearer:<access_token>
。
然而上面說(shuō)的都是一般情況,根本沒有明確規(guī)定!
劇透一下,下面要講的 JWT(JSON Web Token)!他是一個(gè) token!但是里面放著 session 信息!放在客戶端,并且可以隨你選擇放在 cookie 或是手動(dòng)添加在 Authorization!但是他就叫 token!
所以,個(gè)人覺得你不能通過(guò)存放的位置判斷是 token 或是 session id,也不能通過(guò)內(nèi)容判斷是 token 或是 session 信息,session、session id 以及 token 都是很意識(shí)流的東西,只要你明白他是什么、怎么用就好了,怎么稱呼不太重要。
另外在搜索資料時(shí)也看到有些文章說(shuō) session 和 token 的區(qū)別就是新舊技術(shù)的區(qū)別,好像有點(diǎn)道理。
在 session 的 Wikipedia 頁(yè)面上 HTTP session token 這一欄,舉例都是 JSESSIONID (JSP)、PHPSESSID (PHP)、CGISESSID (CGI)、ASPSESSIONID (ASP) 等比較傳統(tǒng)的技術(shù),就像 SESSIONID 是他們的代名詞一般;而在研究現(xiàn)在各種平臺(tái)的 API 接口和 OAuth2.0 登錄時(shí),都是使用 access token 這樣的字眼,這個(gè)區(qū)別著實(shí)有點(diǎn)意思。
理解 session 和 token 的聯(lián)系之后,可以在哪里能看到“活的” token 呢?
打開 GitHub 進(jìn)入設(shè)置,找到 Settings / Developer settings,可以看到 Personal access tokens 選項(xiàng),生成新的 token 后,你就可以帶著它通過(guò) GitHub API,證明“你就是你”。
在 OAuth 系統(tǒng)中也使用了 Access token 這個(gè)關(guān)鍵詞,寫過(guò)微信登錄的朋友應(yīng)該都能感受到 token 是個(gè)什么啦。
Token 在權(quán)限證明上真的很重要,不可泄漏,誰(shuí)拿到 token,誰(shuí)就是“主人”。所以要做一個(gè) Token 系統(tǒng),刷新或刪除 Token 是必須要的,這樣在盡快彌補(bǔ) token 泄漏的問(wèn)題。
在理解了三個(gè)關(guān)鍵字和兩種儲(chǔ)存方式之后,下面我們正式開始說(shuō)“用戶登錄”相關(guān)的知識(shí)和兩種登錄規(guī)范——JWT 和 OAuth2.0。
接著你可能會(huì)頻繁見到 Authentication 和 Authorization 這兩個(gè)單詞,它們都是 Auth 開頭,但可不是一個(gè)意思,簡(jiǎn)單來(lái)說(shuō)前者是驗(yàn)證,后者是授權(quán)。在編寫登錄系統(tǒng)時(shí),要先驗(yàn)證用戶身份,設(shè)置登錄狀態(tài),給用戶發(fā)送 token 就是授權(quán)。
全稱 JSON Web Token(RFC 7519),是的,JWT 就是一個(gè) token。為了方便理解,提前告訴大家,JWT 用的是上面客戶端儲(chǔ)存的方式,所以這部分可能會(huì)經(jīng)常用到上面提到的名稱。
雖說(shuō) JWT 就是客戶端儲(chǔ)存 session 信息的一種,但是 JWT 有著自己的結(jié)構(gòu):Header.Payload.Signature
(分為三個(gè)部分,用 .
隔開)
{
"alg": "HS256",
"typ": "JWT"
}
typ 說(shuō)明 token 類型是 JWT,alg 代表簽名算法,HMAC、SHA256、RSA 等。然后將其 base64 編碼。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Payload 是放置 session 信息的位置,最后也要將這些信息進(jìn)行 base64 編碼,結(jié)果就和上面客戶端儲(chǔ)存的 session 信息差不多。
不過(guò) JWT 有一些約定好的屬性,被稱為 Registered claims,包括:
最后一部分是簽名,和上面提到的 session.sig
一樣是用于防止篡改,不過(guò) JWT 把簽名和內(nèi)容組合到一起罷了。
JWT 簽名的生成算法是這樣的:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
使用 Header 里 alg 的算法和自己設(shè)定的密鑰 secret 編碼 base64UrlEncode(header) + "." + base64UrlEncode(payload)
最后將三部分通過(guò) .
組合在一起,你可以通過(guò) jwt.io Debugger 形象地看到 JWT 的組成原理:
在驗(yàn)證用戶,順利登錄后,會(huì)給用戶返回 JWT。因?yàn)?JWT 的信息沒有加密,所以別往里面放密碼,詳細(xì)原因在客戶端儲(chǔ)存的 cookie 中提到。
用戶訪問(wèn)需要授權(quán)的連接時(shí),可以把 token 放在 cookie,也可以在請(qǐng)求頭帶上 Authorization: Bearer <token>
。(手動(dòng)放在請(qǐng)求頭不受 CORS 限制,不怕 CSRF)
這樣可以用于自家登錄,也可以用于第三方登錄。單點(diǎn)登錄也是 JWT 的常用領(lǐng)域。
JWT 也因?yàn)樾畔?chǔ)存在客戶端造成無(wú)法讓自己失效的問(wèn)題,這算是 JWT 的一個(gè)缺點(diǎn)。
HTTP authentication 是一種標(biāo)準(zhǔn)化的校驗(yàn)方式,不會(huì)使用 cookie 和 session 相關(guān)技術(shù)。請(qǐng)求頭帶有 Authorization: Basic <credentials>
格式的授權(quán)字段。
其中 credentials 就是 Base64 編碼的用戶名 + :
+ 密碼(或 token),以后看到 Basic authentication,意識(shí)到就是每次請(qǐng)求都帶上用戶名密碼就好了。
Basic authentication 大概比較適合 serverless,畢竟他沒有運(yùn)行著的內(nèi)存,無(wú)法記錄 session,直接每次都帶上驗(yàn)證就完事了。
OAuth 2.0(RFC 6749)也是用 token 授權(quán)的一種協(xié)議,它的特點(diǎn)是你可以在有限范圍內(nèi)使用別家接口,也可以借此使用別家的登錄系統(tǒng)登錄自家應(yīng)用,也就是第三方應(yīng)用登錄。(注意啦注意啦,OAuth 2.0 授權(quán)流程說(shuō)不定面試會(huì)考哦?。?/p>
既然是第三方登錄,那除了應(yīng)用本身,必定存在第三方登錄服務(wù)器。在 OAuth 2.0 中涉及三個(gè)角色:用戶、應(yīng)用提供方、登錄平臺(tái),相互調(diào)用關(guān)系如下:
+--------+ +---------------+
| |--(A)- Authorization Request ->| Resource |
| | | Owner |
| |<-(B)-- Authorization Grant ---| |
| | +---------------+
| |
| | +---------------+
| |--(C)-- Authorization Grant -->| Authorization |
| Client | | Server |
| |<-(D)----- Access Token -------| |
| | +---------------+
| |
| | +---------------+
| |--(E)----- Access Token ------>| Resource |
| | | Server |
| |<-(F)--- Protected Resource ---| |
+--------+ +---------------+
很多大公司都提供 OAuth 2.0 第三方登錄,這里就拿小聾哥的微信舉例吧——
一般來(lái)說(shuō),應(yīng)用提供方需要先在登錄平臺(tái)申請(qǐng)好 AppID 和 AppSecret。(微信使用這個(gè)名稱,其他平臺(tái)也差不多,一個(gè) ID 和一個(gè) Secret)
什么是授權(quán)臨時(shí)票據(jù)(code)?答:第三方通過(guò) code 進(jìn)行獲取
access_token
的時(shí)候需要用到,code 的超時(shí)時(shí)間為 10 分鐘,一個(gè) code 只能成功換取一次access_token
即失效。code 的臨時(shí)性和一次保障了微信授權(quán)登錄的安全性。第三方可通過(guò)使用 https 和 state 參數(shù),進(jìn)一步加強(qiáng)自身授權(quán)登錄的安全性。
在這一步中,用戶先在登錄平臺(tái)進(jìn)行身份校驗(yàn)。
https://open.weixin.qq.com/connect/qrconnect?
appid=APPID&
redirect_uri=REDIRECT_URI&
response_type=code&
scope=SCOPE&
state=STATE
#wechat_redirect
參數(shù) | 是否必須 | 說(shuō)明 |
---|---|---|
appid | 是 | 應(yīng)用唯一標(biāo)識(shí) |
redirect_uri | 是 | 請(qǐng)使用 urlEncode 對(duì)鏈接進(jìn)行處理 |
response_type | 是 | 填 code |
scope | 是 | 應(yīng)用授權(quán)作用域,擁有多個(gè)作用域用逗號(hào)(,)分隔,網(wǎng)頁(yè)應(yīng)用目前僅填寫 snsapi_login |
state | 否 | 用于保持請(qǐng)求和回調(diào)的狀態(tài),授權(quán)請(qǐng)求后原樣帶回給第三方。該參數(shù)可用于防止 csrf 攻擊(跨站請(qǐng)求偽造攻擊) |
注意一下 scope 是 OAuth2.0 權(quán)限控制的特點(diǎn),定義了這個(gè) code 換取的 token 可以用于什么接口。
正確配置參數(shù)后,打開這個(gè)頁(yè)面看到的是授權(quán)頁(yè)面,在用戶授權(quán)成功后,登錄平臺(tái)會(huì)帶著 code 跳轉(zhuǎn)到應(yīng)用提供方指定的 redirect_uri
:
redirect_uri?code=CODE&state=STATE
授權(quán)失敗時(shí),跳轉(zhuǎn)到
redirect_uri?state=STATE
也就是失敗時(shí)沒 code。
在跳轉(zhuǎn)到重定向 URI 之后,應(yīng)用提供方的后臺(tái)需要使用微信給你的code獲取 token,同時(shí),你也可以用傳回來(lái)的 state 進(jìn)行來(lái)源校驗(yàn)。
要獲取 token,傳入正確參數(shù)訪問(wèn)這個(gè)接口:
https://api.weixin.qq.com/sns/oauth2/access_token?
appid=APPID&
secret=SECRET&
code=CODE&
grant_type=authorization_code
參數(shù) | 是否必須 | 說(shuō)明 |
---|---|---|
appid | 是 | 應(yīng)用唯一標(biāo)識(shí),在微信開放平臺(tái)提交應(yīng)用審核通過(guò)后獲得 |
secret | 是 | 應(yīng)用密鑰 AppSecret,在微信開放平臺(tái)提交應(yīng)用審核通過(guò)后獲得 |
code | 是 | 填寫第一步獲取的 code 參數(shù) |
grant_type | 是 | 填 authorization_code,是其中一種授權(quán)模式,微信現(xiàn)在只支持這一種 |
正確的返回:
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200,
"refresh_token": "REFRESH_TOKEN",
"openid": "OPENID",
"scope": "SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
得到 token 之后你就可以根據(jù)之前申請(qǐng) code 填寫的 scope 調(diào)用接口了。
授權(quán)作用域(scope) | 接口 | 接口說(shuō)明 |
---|---|---|
snsapi_base | /sns/oauth2/access_token | 通過(guò) code 換取 access_token 、refresh_token 和已授權(quán) scope |
snsapi_base | /sns/oauth2/refresh_token | 刷新或續(xù)期 access_token 使用 |
snsapi_base | /sns/auth | 檢查 access_token 有效性 |
snsapi_userinfo | /sns/userinfo | 獲取用戶個(gè)人信息 |
例如獲取個(gè)人信息就是 GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
注意啦,在微信 OAuth 2.0,access_token
使用 query 傳輸,而不是上面提到的 Authorization。
使用 Authorization 的例子,如 GitHub 的授權(quán),前面的步驟基本一致,在獲取 token 后,這樣請(qǐng)求接口:
curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com
說(shuō)回微信的 userinfo 接口,返回的數(shù)據(jù)格式如下:
{
"openid": "OPENID",
"nickname": "NICKNAME",
"sex": 1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
"privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
在使用 token 獲取用戶個(gè)人信息后,你可以接著用 userinfo 接口返回的 openid,結(jié)合 session 技術(shù)實(shí)現(xiàn)在自己服務(wù)器登錄。
// 登錄
req.session.id = openid
if (req.session.id) {
// 已登錄
} else {
// 未登錄
}
// 退出
req.session.id = null
// 清除 session
總結(jié)一下 OAuth2.0 的流程和重點(diǎn):
OAuth2.0 著重于第三方登錄和權(quán)限限制。而且 OAuth2.0 不止微信使用的這一種授權(quán)方式,其他方式可以看阮老師的OAuth 2.0 的四種方式。
JWT 和 OAuth2.0 都是成體系的鑒權(quán)方法,不代表登錄系統(tǒng)就一定要這么復(fù)雜。
簡(jiǎn)單登錄系統(tǒng)其實(shí)就以上面兩種 session 儲(chǔ)存方式為基礎(chǔ)就能做到。
req.session.isLogin = true
的方法標(biāo)志該 session 的狀態(tài)為已登錄。{
"exp": 1614088104313,
"usr": "admin"
}
(就是和 JWT 原理基本一樣,不過(guò)沒有一套體系)
let store = {}
// 登錄成功后
store[HASH] = true
cookie.set('token', HASH)
// 需要鑒權(quán)的請(qǐng)求鐘
const hash = cookie.get('token')
if (store[hash]) {
// 已登錄
} else {
// 未登錄
}
// 退出
const hash = cookie.get('token')
delete store[hash]
以下列出本文重點(diǎn):
set-cookie
請(qǐng)求頭設(shè)置聯(lián)系客服