常見的鑒權(quán)方式:Session/Cookie、Token(JWT)汰规、OAuth蜜唾、SSO
Session/Cookie
Session
基于Cookie
,Session
存在服務(wù)端找田,Cookie
存在客戶端。
// 服務(wù)端 - Node
const http = require('http');
http.createServer((req, res) => {
req.headers.cookie // 獲取客戶端攜帶的Cookie
res.setHeader('Set-Cookie', 'cookie1=abc;') // 讓客戶端設(shè)置Cookie
res.end()
}).listen(3000)
客戶端收到的Response Headers:
Set-Cookie: cookie1=abc;
客戶端再次請求時着憨,會在 Request Headers
中攜帶 Cookie
Cookie: cookie1=abc
Cookie
的容量有限,且容易被篡改务嫡,不安全甲抖,所以出現(xiàn)了Session
在客戶端的Cookie
中,存儲一個隨機(jī)的Session Id
心铃,Session
信息存儲在服務(wù)端
// 模擬實現(xiàn)Cookie-Session模式
let session = {}, sessionKey = 'sid'
http.createServer((req, res) => {
const cookie req.headers.cookie
if(cookie && cookie.indexOf(sessionKey) !== -1) {
res.end('Come Back!')
// 存在准谚,取出Session
const pattern = new RegExp(`${sessionKey}=(^;]+);?\s*`)
const sid = pattern.exec(cookie)[1]
session[sid] // Session
} else {
// 不存在Cookie-Session
const sid = (Math.random() * 999999).toFixed()
// 設(shè)置Cookie
res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
session[sid] = { name: 'Jack' }
res.end('Hi, Jack')
}
}).listen(3000)
Session
會話機(jī)制是一種服務(wù)端機(jī)制,它使用類似哈希表的結(jié)果來保存信息去扣。
在客戶端首次訪問服務(wù)器時創(chuàng)建
Session
柱衔,并為這個Session
生成一個唯一的表示字符串sid
,作為鍵愉棱。如果需要唆铐,還會通過密匙對sid
進(jìn)行簽名處理,避免客戶端篡改奔滑。把sid
作為Cookie
返回給客戶端艾岂。客戶端在隨后的HTTP
請求中會攜帶這個sid
朋其,服務(wù)端通過這個sid查詢客戶端的Session
信息王浴。
哈希 Hash
- 哈希 Hash 的特點
- 把一個不定長摘要定長結(jié)果
- 摘要
xialaoshi -> x4sdfdsafsdafl3s3 --
按照某種規(guī)則進(jìn)行摘要脆炎,防篡改 - 雪崩效應(yīng):牽一發(fā)動全身,如
xialaoshi
和zialaoshi
雖然只有一個字母不同氓辣,但摘要的結(jié)果完全不同秒裕,讓人完全找不到規(guī)律。
- 常見哈希算法:
MD5钞啸、SHA1几蜻、hmac
- 摘要(加密算法)
- 對稱
DES
- 非對稱
RSA
- 對稱
-
koa-session
中啟用簽名
const Koa = require('koa'), session = require('koa-session')
const app = new Koa()
app.keys = ['some secret'] //用于對cookie簽名的key
const SESS_CONFIG = {
key: 'ply:sess', // cookie的鍵名
maxAge: 86400000,
httpOnly: true, // 僅允許服務(wù)器修改
signed: true // 啟用簽名
}
app.use(session(SESS_CONFIG, app))
Session
通常存在于內(nèi)存、Redis
等介質(zhì)中爽撒,但分布式部署無法共享內(nèi)存入蛆,所以大多數(shù)情況下存儲于Redis
中。
Token 令牌
Token
是一個獨立的概念硕勿,是通過認(rèn)證后發(fā)放的某個 憑證哨毁,這個憑證可以通過Cookie
、自定義http
請求頭(如X-Token
)源武,URL
的query
進(jìn)行傳遞扼褪。所以Token
不存在跨域、跨平臺的問題粱栖。
- 常見加密形式(攜帶信息):
用戶Id + 過期時間 + 簽名
-
Token
需要服務(wù)端查庫驗證是否有效话浇,通常存儲于Redis
中。
JWT - 服務(wù)端無狀態(tài)化
JWT
Json Web Token
闹究,是Token
的子集幔崖,是一種規(guī)則,采用Json
格式以及附帶一些規(guī)范約束渣淤,將用戶信息加密到 Token
中赏寇。服務(wù)器不保存任何用戶狀態(tài)信息(無需查庫),而是通過保存的密匙校驗 Token
价认,也就是說 服務(wù)端是無狀態(tài)的嗅定,即使服務(wù)重啟也不會有影響。
JWT
的加密過程(原理) 包括三部分:頭部header
用踩,載荷payload
渠退,簽名signature
,用 .
分隔脐彩。
-
Header
用于描述元信息碎乃,通常包括Token
類型和Signature
的加密算法
經(jīng)過{ "alg": "HS256", // 默認(rèn)的哈希(加密)算法 "typ": "JWT" // Token類型 }
base64
編碼,得到第一部分 -
Payload
載荷信息惠奸,包含一些聲明Claim
(實體描述荠锭,通常是一個用戶信息,還有一些其他的元數(shù)據(jù))晨川,即攜帶希望向服務(wù)器發(fā)送的信息证九。
這些信息可以是官方字段如iss(Issuer)删豺、sub(Subject)、exp(Expiration time)
愧怜,也可以是自定義字段如userId呀页,data
經(jīng)過{ "iss": "JWT Builder", // 該JWT的簽發(fā)者 "sub": "aaa@example.com", // 該JWT所面向的用戶 "exp": 1448333419, // 過期時間 "userId": "b08f86af-35da-48f2-8fab-cef3904660bd" // 用戶Id }
base64
編碼,得到第二部分 -
Signature
第一部分+ "." +
第二個部分 拼接成字符串拥坛,通過Header
中聲明的加密方式進(jìn)行加鹽secret
組合加密蓬蝶,得到第三部分。
加鹽secret
也就是服務(wù)端提供的私鑰猜惋,防篡改丸氛。解密Token
時僅僅使用設(shè)定的secret
,無需查詢數(shù)據(jù)庫著摔,所以secret
一定不能泄露;捍堋!谍咆!
一旦客戶端得到這個secret
禾锤,那就意味著客戶端可以自我簽發(fā)JWT
了!
第一部分 + "." +
第二個部分 + "." +
第三個部分 = JWT
由此可知摹察,JWT(Token)
并不是為了隱藏或保密數(shù)據(jù)恩掷,而是為了確保它是被授權(quán)的,所以其中不能放諸如用戶密碼等 敏感信息供嚎。
// 前端
axios.interceptors.request.use(config => {
const token = window.localStorage.getItem('token')
if(token) {
// 通常約定 Bearer 是 JWT 的認(rèn)證頭部
config.headers.common['Authorization'] = 'Bearer ' + token
}
return config
}, err => Promise.reject(err))
Node Koa
后端:
koa黄娘、koa-router、koa-bodyparser克滴、koa-static
jsonwebtoken寸宏、koa-jwt、koa2-cors(跨域)
對比
-
Cookie-Session
模式會受跨域偿曙、分布式部署、服務(wù)重啟的影響羔巢; -
Token + Redis
模式需要每次都查詢數(shù)據(jù)庫望忆,驗證Token
是否有效; -
JWT
是一種認(rèn)證授權(quán)機(jī)制竿秆,服務(wù)端無狀態(tài)(無需存儲)启摄,也就不能主動清除,不能踢離線幽钢。
OAuth
隨著大量開放平臺的出現(xiàn)歉备,建立在開放平臺之上的各種第三方應(yīng)用也在大量冒出。出于對安全性和統(tǒng)一標(biāo)準(zhǔn)的要求匪燕,就有了OAuth
協(xié)議蕾羊。
簡單來說喧笔,OAuth是一種開放的協(xié)議,一種授權(quán)機(jī)制龟再,它能為第三方應(yīng)用提供一種簡單的標(biāo)準(zhǔn)方式去訪問需要用戶授權(quán)的API服務(wù)书闸。而且在為第三方提供服務(wù)的過程中,能夠保護(hù)用戶賬號的安全利凑。
2007年OAuth1.0
浆劲,存在嚴(yán)重漏洞。2009年6月OAuth1.0 Revision A
哀澈,修復(fù)了前一版本的安全漏洞牌借。
2010年4月OAuth2.0
,與1.0
互不兼容割按。OAuth 2.0
也是目前最流行的授權(quán)機(jī)制膨报,授權(quán)第三方應(yīng)用,獲取用戶數(shù)據(jù)哲虾。
舉個栗子:
我們想做一個關(guān)于新浪微博的第三方應(yīng)用丙躏,那么就需要新浪微博提供一系列API,但前提是要去授權(quán)驗證束凑,這就用到OAuth
了晒旅。作為第三方開發(fā)者,我們并沒有得到用戶實質(zhì)性的私密信息汪诉,只是獲取了一個短期的進(jìn)入令牌(必須保密废恋,不能泄露),確保了用戶賬號的安全扒寄。
在應(yīng)用OAuth
時鱼鼓,常常與OpenID
作比較:
OAuth
的關(guān)注點在于授權(quán),OpenID
的側(cè)重于證明鑒定该编。簡單來說迄本,OAuth
解決用戶能(想)做什么,是 WHAT
的問題课竣;OpenID
則為我們驗證用戶是誰嘉赎,解決WHO
的問題
1.0 與 2.0 的授權(quán)方式不同
瀏覽器、目標(biāo)服務(wù)器于樟、第三方認(rèn)證服務(wù)器
- 2.0授權(quán)過程分3步:
獲取授權(quán)碼Authorization Code -->
換取訪問令牌access_token -->
訪問授權(quán)資源
具體過程:瀏覽器向目標(biāo)服務(wù)器發(fā)起認(rèn)證請求-->
目標(biāo)服務(wù)器通知瀏覽器重定向到第三方認(rèn)證服務(wù)器-->
瀏覽器輸入第三方認(rèn)證的用戶名和密碼并發(fā)送-->
認(rèn)證成功后回調(diào)給目的服務(wù)器授權(quán)碼-->
目的服務(wù)器使用授權(quán)碼向第三方認(rèn)證服務(wù)器換取訪問令牌 --> 第三方認(rèn)證服務(wù)器返回令牌給目的服務(wù)器-->
目的服務(wù)器通知瀏覽器刷新頁面公条,登錄授權(quán)成功,使用令牌訪問得到授權(quán)的資源
- 1.0授權(quán)過程分4步
- 請求未授權(quán)的
Request Token
(比如顯示新浪微博的授權(quán)登錄界面) - 請求授權(quán)的
Request Token
迂曲,用戶確認(rèn)授權(quán)靶橱,認(rèn)證服務(wù)器返回授權(quán)的OAuth Token
- 用
OAuth Token
換取Access Token
,獲得訪問令牌oauth_token
和oauth_token_secret
- 用訪問令牌訪問得到授權(quán)的資源
- 請求未授權(quán)的
雖然
1.0
協(xié)議每個Token
都有加密,2.0
則不需要关霸,但2.0
要求使用https
協(xié)議传黄,安全性高于1.0
;而且2.0
充分考慮了客戶端的各種姿態(tài)谒拴,提供了多種途徑獲取訪問令牌尝江,包括授權(quán)碼,客戶端私有整數(shù)英上,資源擁有者密碼證書炭序,刷新令牌等等,且驗證過程更加簡潔苍日。相比之下惭聂,1.0
只有一個用戶授權(quán)流程。
以Github
認(rèn)證為例
需要先開通Github
賬號相恃,主頁頭像 --> Settings --> Developer settings --> OAuth Apps --> New OAuth App
注冊一個第三方OAuth
認(rèn)證的應(yīng)用(應(yīng)用名稱辜纲、應(yīng)用的主頁URL
、認(rèn)證回調(diào)的URL
)拦耐,得到 Client ID耕腾,Client Secret
- 前端頁面提供第三方
Github
登錄的入口:
<a href="/github/login">login with github</a>
- 后端處理第三方認(rèn)證:
const Koa = require('koa'),
Router = require('@koa/router'),
static = require('koa-static'),
querystring = require('querystring'),
axios = require('axios');
const config = {
client_id: '76a2dacb24ead9fc4596',
client_secret: '657124b0a8073d5538d12ad68f6d56114761283c'
}
// github 的認(rèn)證接口
const OAuthURL = 'https://github.com/login/oauth/authorize';
// github 的Token接口
const TokenURL = 'https://github.com/login/oauth/access_token'
let thirdUser; // 保存第三方認(rèn)證用戶的數(shù)據(jù)
const app = new Koa();
const router = new Router();
app.use(static(__dirname + '/'));
router.get('/github/login', async ctx => {
// 重定向到Github認(rèn)證接口
const url = OAuthURL + '?client_id=' + config.client_id;
ctx.redirect(url)
});
/**
Github上配置的認(rèn)證回調(diào)接口:
http://localhost:7000/auth/github/callback
*/
router.get('/auth/github/callback', async ctx => {
// 獲取到授權(quán)碼Code
const code = ctx.query.code;
const params = Object.assign({ code }, config)
// 根據(jù)授權(quán)碼 換取令牌Token
let resp = await axios.post(TokenURL, params)
const access_token = querystring.parse(resp.data).access_token
// 已經(jīng)獲取了Token,可以請求Github的接口了:獲取用戶數(shù)據(jù)
thirdUser = await axios.get('https://api.github.com/user?access_token='+access_token)
// 重定向到主頁
ctx.redirect('/home')
});
router.get('/home', async ctx => {
const { login, avatar_url } = thirdUser.data
ctx.body = `
<h1>Hello ${login}</h1>
<img src="${avatar_url}" />
`
})
app.use(router.routes()).use(router.allowedMethods()).listen(7000);
SSO
單點登錄:Single Sign On - SSO
是一個在多系統(tǒng)共存的環(huán)境下杀糯,用戶登錄其中一個系統(tǒng)扫俺,便可在其他所有系統(tǒng)中得到授權(quán) 而無需再次登錄。包括 單點登錄 和 單點注銷 兩部分固翰。
Web系統(tǒng)由單系統(tǒng)發(fā)展成多個系統(tǒng)組成的應(yīng)用群狼纬,復(fù)雜性應(yīng)該由系統(tǒng)內(nèi)部承擔(dān),而不是用戶骂际。無論web系統(tǒng)內(nèi)部多么復(fù)雜疗琉,對用戶而言,都是一個統(tǒng)一的整體歉铝;也就是說盈简,用戶訪問Web系統(tǒng)的整個應(yīng)用群與訪問單個系統(tǒng)一樣,登錄/注銷只要一次就夠了太示。
單點登錄
單點登錄可分為同域和跨域兩種場景
同域
同域單點登錄的核心就是共享Cookie
柠贤,所有子系統(tǒng)都使用同一個一級域名,通過不同的二級域名來區(qū)分先匪。
舉個栗子:公司有一個一級域名 zlt.com
,三個子系統(tǒng):門戶系統(tǒng)sso.zlt.com
弃衍,應(yīng)用1app1.zlt.com
呀非,應(yīng)用2app2.zlt.com
- 設(shè)置它們的
Cookie
域為一級域名(domain=zlt.com
),這樣就可以共享門戶系統(tǒng)的Cookie
給所有使用*.zlt.com
作為域名的系統(tǒng)。 - 使用
Spring Session
等技術(shù)讓所有系統(tǒng)共享Session
岸裙。這樣只要門戶系統(tǒng)登錄之后猖败,無論跳轉(zhuǎn) 應(yīng)用1 還是應(yīng)用2,都能通過門戶系統(tǒng)Cookie
中的sessionId
讀取到Session
中的登錄信息降允,實現(xiàn)單點登錄恩闻。
然而,共享Cookie
的方式存在眾多局限剧董。首先幢尚,應(yīng)用群域名必須統(tǒng)一;其次翅楼,應(yīng)用群各系統(tǒng)使用的技術(shù)(至少是Web
服務(wù)器)要相同尉剩,否則Cookie
的key
值(Tomcat
為JSESSIONID
)不同,無法維持會話毅臊,即共享Cookie
的方式是無法實現(xiàn)跨語言技術(shù)平臺登錄的理茎,比如java、php管嬉、.net
系統(tǒng)之間皂林;第三,Cookie
本身不安全蚯撩。
跨域
單點登錄之間的系統(tǒng)域名不同础倍,無法共享Cookie
,需要一個獨立的 授權(quán)系統(tǒng)/
認(rèn)證中心求厕。子系統(tǒng)本身不參與登錄著隆,而是通過認(rèn)證中心驗證身份,完成間接授權(quán)呀癣。當(dāng)一個系統(tǒng)登錄成功后寇甸,認(rèn)證中心會頒發(fā)一個令牌給子系統(tǒng),子系統(tǒng)拿著令牌去獲取受保護(hù)的資源味赃。為了減少頻繁認(rèn)證嫂用,各個子系統(tǒng)在被認(rèn)證中心授權(quán)以后,會建立一個局部會話沼沈,在一定時間內(nèi)無需再次向認(rèn)證中心發(fā)起認(rèn)證流酬。
- 用戶訪問系統(tǒng)1的受保護(hù)資源,系統(tǒng)1發(fā)現(xiàn)用戶未登錄列另,跳轉(zhuǎn)至
SSO
認(rèn)證中心芽腾,并將自己的地址作為參數(shù) -
SSO
認(rèn)證中心發(fā)現(xiàn)用戶未登錄,將用戶引導(dǎo)至登錄頁面 - 用戶輸入用戶名密碼提交登錄申請
-
SSO
認(rèn)證中心校驗用戶信息页衙,創(chuàng)建用戶與SSO
認(rèn)證中心之間的會話摊滔,稱為全局會話阴绢,同時創(chuàng)建授權(quán)令牌 -
SSO
認(rèn)證中心帶著令牌跳轉(zhuǎn)回最初的請求地址(系統(tǒng)1) -
系統(tǒng)1拿到令牌,去
SSO認(rèn)
證中心校驗令牌是否有效(安全性艰躺,必須校驗) -
SSO
認(rèn)證中心校驗令牌呻袭,返回有效,注冊系統(tǒng)1 - 系統(tǒng)1使用該令牌創(chuàng)建與用戶的會話腺兴,稱為局部會話左电,返回受保護(hù)資源
- 用戶訪問系統(tǒng)2的受保護(hù)資源,系統(tǒng)2發(fā)現(xiàn)用戶未登錄页响,跳轉(zhuǎn)至
SSO
認(rèn)證中心篓足,并將自己的地址作為參數(shù) -
SSO
認(rèn)證中心發(fā)現(xiàn)用戶已登錄,跳轉(zhuǎn)回系統(tǒng)2的地址拘泞,并附上令牌 -
系統(tǒng)2拿到令牌纷纫,去
SSO
認(rèn)證中心校驗令牌是否有效,SSO
認(rèn)證中心返回有效陪腌,注冊系統(tǒng)2 - 系統(tǒng)2使用該令牌創(chuàng)建與用戶的局部會話辱魁,返回受保護(hù)資源
用戶登錄成功之后,會與SSO
認(rèn)證中心及各個子系統(tǒng)建立會話诗鸭,用戶與SSO
認(rèn)證中心建立的會話稱為全局會話染簇,與各個子系統(tǒng)建立的會話稱為局部會話。局部會話建立之后强岸,用戶訪問子系統(tǒng)受保護(hù)資源將不再通過SSO
認(rèn)證中心锻弓。
全局會話與局部會話的約束關(guān)系:
- 局部會話存在,全局會話一定存在
- 全局會話存在蝌箍,局部會話不一定存在
- 全局會話銷毀青灼,局部會話必須銷毀
SSO
系統(tǒng)登錄后,帶著令牌跳回原業(yè)務(wù)系統(tǒng)時妓盲,業(yè)務(wù)系統(tǒng)還要拿令牌再次訪問SSO
進(jìn)行驗證杂拨,這個步驟看似有點多余。如果SSO
登錄認(rèn)證通過后悯衬,通過回調(diào)地址將用戶信息返回給原業(yè)務(wù)系統(tǒng)弹沽,原業(yè)務(wù)系統(tǒng)直接設(shè)置登錄狀態(tài),這樣流程簡單筋粗,也完成了登錄娜亿,不是很好嗎辰斋?!
其實問題是很嚴(yán)重的旁仿,如果在SSO
沒有登錄办悟,而是直接在瀏覽器中敲入回調(diào)的地址炫加,并帶上偽造的用戶信息魄健,是不是業(yè)務(wù)系統(tǒng)也認(rèn)為登錄了呢?這是很可怕的绿满!
單點注銷
單點登錄自然也要單點注銷,在一個子系統(tǒng)中注銷姐叁,所有子系統(tǒng)的會話都將被銷毀挠唆;
SSO
認(rèn)證中心一直監(jiān)聽全局會話的狀態(tài),一旦全局會話銷毀嘱吗,監(jiān)聽器將通知所有注冊系統(tǒng)執(zhí)行注銷操作
- 用戶向
系統(tǒng)1
發(fā)起注銷請求玄组; -
系統(tǒng)1
根據(jù)與用戶建立的會話id
拿到令牌,向SSO
認(rèn)證中心發(fā)起注銷請求谒麦; -
SSO
認(rèn)證中心校驗令牌有效俄讹,銷毀全局會話,同時取出所有用此令牌注冊的系統(tǒng)地址绕德,向所有注冊系統(tǒng)發(fā)起注銷請求患膛; - 各注冊系統(tǒng)接收
SSO
認(rèn)證中心的注銷請求,銷毀局部會話耻蛇; -
SSO
認(rèn)證中心引導(dǎo)用戶至登錄頁面踪蹬。