無論用哪一種方式認證用戶,均可能被中間人攻擊竊取 SessionID 或 Token,從而發生 CSRF 攻擊。解決方式就是全站 HTTPS。如今 Let’s Encrypt 已經支持免費的通配符 HTTPS 證書了。php
HTTP 協議是無狀態的,要保存用戶狀態須要額外的機制。html
剛開始時,多數公司使用的技術棧是:單臺雲服務器上安裝所需的全部軟件,包括 Nginx 提供 Web 服務,MySQL 數據庫,PHP-FPM 應用程序服務。這時候使用的用戶認證協議使用最簡單的 Session。客戶端的每一個請求都會攜帶 Cookie,其中保存了 SessionID 字段,服務器能夠經過這個 SessionID 字段訪問到對應的 Session(例如 PHP 中的 $_SESSION
),從而識別出用戶登陸狀態。Session 中還能夠添加一些經常使用的字段進來(好比用戶名、手機號等),避免對數據庫的頻繁訪問。前端
後來,隨着用戶量增大、併發增大,單臺服務器搞不定了,因而搞了個水平擴展的服務器集羣,經過 Nginx 或 LVS 實現負載均衡。這時發現個問題,用戶登陸後 Session 是保存到集羣中的某一臺服務器上的。要使 Session 機制能夠在分佈式環境下繼續工做,須要一些額外操做。並且對於如今的大前端(瀏覽器、APP、小程序)趨勢來講,Cookie 機制略顯累贅。java
而這時,JWT 認證協議徹底知足需求。協議簡單清晰,花一個下午就能夠搞清楚。nginx
公司發展過程當中,產品線會慢慢增多,好比百度的貼吧、網盤、瀏覽器等。這時,須要一套單點登陸機制 SSO(Single sign-on),用戶只要一次登陸,就可使用這一系列產品。SSO 描述了認證的問題。laravel
SSO 須要一個獨立的認證中心 CAS(Central Authentication Service,中央認證服務),只有認證中心能提供登陸入口,接受用戶的用戶名密碼等憑證,其餘系統無登陸入口,只接受認證中心的間接受權。這裏有個開源的 CAS:apereo CAS,其服務端用 Java 實現,客戶端支持多種語言。其架構文檔能夠參考 這裏。git
單體項目拆分紅微服務後,能夠更加靈活。一般全部的服務都在網關以後,全部請求都發送到網關,由網關統一轉發。微服務的網關一般實現了 OAuth,成爲認證受權中心,用於判斷是否有足夠權限。微服務之間能夠經過 JWT 進行訪問鑑權,避免身份認證。github
隨着公司用戶增多(假設跟微信同樣,有幾億用戶),合做企業也愈來愈多。若是每次都要在後臺經過人工給合做夥伴配置帳號密碼,分配權限管理,那太麻煩了。同時,一些企業有本身的平臺,想要利用個人用戶帳號體系實如今這些平臺上的登陸(受權登陸)。對於用戶的圖片,一些圖片打印公司也想在通過用戶贊成後,直接訪問到我服務器上的用戶圖片,優化體驗。web
總之,就是隻要用戶贊成,他能夠分享本身的全部資源(帳號、圖片等)。這時,就須要 OAuth2 了。這是一個受權框架,描述了各類受權的問題。算法
例如,用戶登陸論壇時,須要先用用戶名和密碼認證用戶有沒有權限登陸,若是密碼正確則認證經過,登陸成功。用戶登陸後,判斷其角色並授予相應的權限,例如超級管理員能夠刪除全部人,版主能夠刪除其版塊的帖子。
最傳統的用戶認證方式。用戶首次訪問應用服務器後創建會話,服務器可使用 Set-Cookie 這個 HTTP Header,將會話的 SessionID 寫入在用戶端保存的 Cookie 中(具體的名字能夠自行設置,系統中統一便可)。下次用戶再次向這個域名發請求時會攜帶全部 Cookie 信息,包括這個 SessionID。
Session 信息保存在服務器端,而用於惟一標識這個 Session 的 SessionID 則保存在對應客戶端的 Cookie 中。SessionID 這個會話標識符本質上是一個隨機字符串,每一個用戶的 SessionID 都不同。
Session 中能夠保存不少信息。例如設置一個 IsLogin 字段,用戶經過帳號密碼登陸後,將這個字段設置爲 TRUE。這樣,在 Session 的有效期內(好比 2 小時),即便用戶關閉網頁,再次打開後仍會保持登陸狀態(除非用戶清理了 Cookie,致使其訪問服務器時沒有攜帶 SessionID 字段)。對於其餘的經常使用字段(如 userID、userName等)也能夠添加到 Session 中,以減小數據庫的訪問壓力,但注意不要太大,由於全部用戶的會話信息都是保存在服務器的內存中的。
下面的示例,基於 PHP 語言,CodeIgniter 框架。同時,省略了無關的 HTTP Header,重點分析 Session 相關字段。
在第一次訪問一個網站時,瀏覽器中沒有對應 Cookie 信息,全部請求的 HTTP Header 中沒有 Cookie 這個字段。若是應用服務器支持會話,能夠在爲這個用戶建立 Session 後,經過在響應的 HTTP Header 中使用 Set-Cookie 字段將這個會話的 SessionID 保存到瀏覽器的 Cookie 中。能夠看到我這裏對應的 SessionID 的名字是 ci_session:
-----------------------------------------請求的 HTTP Header-----------------------------------------
GET http://tuan.local.cn/ HTTP/1.1
Host: tuan.local.cn
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
If-Modified-Since: Thu, 10 May 2018 06:20:36 GMT
...
-----------------------------------------響應的 HTTP Header-----------------------------------------
HTTP/1.1 200 OK
Date: Thu, 10 May 2018 06:21:13 GMT
Content-Type: text/html; charset=UTF-8
Set-Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf; expires=Thu, 10-May-2018 08:21:13 GMT; Max-Age=7200; path=/; HttpOnly
...
這裏 Set-Cookie 中的各個字段解釋以下,完整的中文版解釋參考 這裏:
Document.cookie
屬性或 XMLHttpRequest 和 Request 這兩個 API 訪問,避免 XSS(cross-site scripting,跨站腳本攻擊)。每次經過域名或 IP 地址訪問時,瀏覽器都會檢查是否有可用的 Cookie,若是有,則放到請求的 HTTP Header 中一同發送到服務器:
-----------------------------------------請求的 HTTP Header-------------------------------------------
GET http://tuan.local.cn/ HTTP/1.1
Host: tuan.local.cn
Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf
...
-----------------------------------------響應的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Thu, 10 May 2018 06:22:02 GMT
Content-Type: text/html; charset=UTF-8
...
登陸成功以後,登陸請求對應的響應會再次設置 Cookie 字段,從新設置 Cookie 字段的有效期。個人應用程序中設置 Session 爲兩個小時的有效期:
這裏演示的是經過 AJAX 登陸,因此有 Origin 和 X-Requested-With 這兩個由瀏覽器自動設置的字段:
-----------------------------------------請求的 HTTP Header-------------------------------------------
POST http://tuan.local.cn/index/login_password HTTP/1.1
Host: tuan.local.cn
Origin: http://tuan.local.cn
X-Requested-With: XMLHttpRequest
Content-Type: application/json
Referer: http://tuan.local.cn/
Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf
...
{"Mobile":"18866668888","Password":"888666"}
-----------------------------------------響應的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Set-Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf; expires=Thu, 10-May-2018 08:22:33 GMT; Max-Age=7200; path=/; HttpOnly
...
跟正常訪問沒有區別,只是攜帶的 Cookie 中有 SessionID,且服務器端對應的 Session 中須要(好比 IsLogin=true,本身設置)標識已登陸狀態:
-----------------------------------------請求的 HTTP Header-------------------------------------------
GET http://tuan.local.cn/ HTTP/1.1
Host: tuan.local.cn
Cookie: ci_session=hfvn0lq3ct62mppl86n2du535bc4stcf
...
-----------------------------------------響應的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Thu, 10 May 2018 06:22:34 GMT
...
Session 的主要問題有:
還有,就是目前大前端的發展,除了瀏覽器外,各類 APP、小程序層出不窮,而非瀏覽器下環境下避免使用 Cookie 可能會更簡單。
Session 之因此這麼麻煩,是由於須要在服務器端保存信息,那我把信息保存在客戶端,不就能夠避免這個麻煩了嘛。JWT 就是這麼個思路,服務器端保存加密機制及密鑰,對用戶指定字段進行加密後的字符串保存在客戶端,用戶下次請求時攜帶加密前的字段和加密後的字符串,若是跟服務器加密結果匹配,則認爲登陸成功。
JWT(JSON web token)是一種認證協議,能夠發佈接入令牌(Access Token,保持在客戶端)並對發佈的簽名接入令牌進行驗證。令牌(Token)自己包含一系列聲明,應用程序能夠根據這些聲明限制用戶對資源的訪問。
JWT 由三段信息構成的:
JWT 示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjU5NDM4MTksIm5iZiI6MTUyNTk0Mzg3OSwiZXhwIjoxNTI1OTQ3NDE5LCJ1aWQiOjF9.jL-Hrl8obZlLGutjr-nVPCSoF2ObFh-rWfSwSZxoxzs
Header 部分用於聲明協議類型和加密方式。
上面的 JWT 示例的 header 部分通過 base64_decode 後獲得原始 JSON 字符串,內容以下:
{
"typ":"JWT",
"alg":"HS256",
"jti":"4f1g23a12aa" }
其中,typ 內容固定爲 JWT,alg 表示加密算法,這裏使用的是 HMAC SHA256。
payload 部分用於存放負載,將明文信息通過 base64 編碼後存儲,未經加密,不可存儲敏感信息。包括如下三種:
JWT 標準中註冊的聲明(不強制使用)有如下幾種,完整版能夠 參考這裏:
上面 JWT 示例中的 payload 部分對應的 JSON 字符串爲:
{
"iss":"http:\/\/example.com",
"aud":"http:\/\/example.org",
"jti":"4f1g23a12aa",
"iat":1525943995,
"nbf":1525944055,
"exp":1525947595,
"userID":6666,
"userName":"kika",
"userSex":"m" }
這個 payload 中添加了幾個自定義字段。
將 header 和 payload 通過 base64 編碼後,用 .
句點拼接成一個字符串,經過 HMACSHA256(Java 的方法)或 hash_hmac(PHP 的方法),使用指定密鑰加密這個字符串獲得 signature。
JAVA:
sig = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
PHP:
$sig = hash_hmac('sha256', base64_encode($header) + "." + base64_decode($payload), $secret);
JWT 支持兩種簽名方式:
用戶登錄後,能夠把一些經常使用字段(用戶標識,是不是管理員,權限有哪些等等能夠公開的信息)用 JWT 編碼存儲在 Cookie 中,每次服務器讀取到 Cookie 後就能夠解析到當前用戶對應的信息,減少數據庫壓力。也能夠用 Authorization: Bearer <jwttoken>
的方式經過 HTTP Header 僅發送 JWT 的 Token。
發送請求時,Token 放在請求的 HTTP Header 中。另外,若是發生跨域,例如 www.xx.com
下發出到 api.xx.com
的請求,須要在服務端開啓 CORS(跨域資源共享):
Access-Control-Allow-Origin: *
下面的示例,基於 PHP 語言,CodeIgniter 框架。同時,省略了無關的 HTTP Header,重點分析 JWT 相關字段。
-----------------------------------------請求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/create_token HTTP/1.1
Host: jwt.com
...
-----------------------------------------響應的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Fri, 11 May 2018 02:27:19 GMT
Set-Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDU2MzksIm5iZiI6MTUyNjAwNTY5OSwiZXhwIjoxNTI2MDA5MjM5LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.MvYG6L71mM_AJj5FT4--RzCluIQ__nqgYSe9RTj8VCk; expires=Fri, 11-May-2018 04:27:19 GMT; Max-Age=7200; path=/
Content-Length: 1052
...
服務器端從 Cookie 中提取 jwt 這個字段後驗證簽名,若是經過驗證則認爲內容可靠,解析其中的內容並以此決定用戶登陸狀態、權限等:
-----------------------------------------請求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/ HTTP/1.1
Host: jwt.com
Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDU2MzksIm5iZiI6MTUyNjAwNTY5OSwiZXhwIjoxNTI2MDA5MjM5LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.MvYG6L71mM_AJj5FT4--RzCluIQ__nqgYSe9RTj8VCk
...
-----------------------------------------請求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/create_token HTTP/1.1
Host: jwt.com
...
-----------------------------------------響應的 HTTP Header-------------------------------------------
HTTP/1.1 200 OK
Date: Fri, 11 May 2018 02:35:19 GMT
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDc3NTQsIm5iZiI6MTUyNjAwNzgxNCwiZXhwIjoxNTI2MDExMzU0LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.CBsw7_rDC-GeJBob2JwCOITp7L80g_VT9KUtSVmKYKY
Host: jwt.com
-----------------------------------------請求的 HTTP Header-------------------------------------------
GET http://jwt.com/welcome/ HTTP/1.1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IjRmMWcyM2ExMmFhIn0.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHA6XC9cL2V4YW1wbGUub3JnIiwianRpIjoiNGYxZzIzYTEyYWEiLCJpYXQiOjE1MjYwMDc3NTQsIm5iZiI6MTUyNjAwNzgxNCwiZXhwIjoxNTI2MDExMzU0LCJ1c2VySUQiOjY2NjYsInVzZXJOYW1lIjoia2lrYSIsInVzZXJTZXgiOiJtIn0.CBsw7_rDC-GeJBob2JwCOITp7L80g_VT9KUtSVmKYKY
Host: jwt.com
後端服務器對這個 Authorization 進行判斷便可。
對於 PHP,可使用的 JWT 庫有 jwt、jwt-auth。這裏以第一個 jwt 爲例,具體操做請結合所使用語言及框架和安裝的 JWT 庫。
composer require lcobucci/jwt
注意,PHP 版本須要 5.5+,同時須要開啓 OpenSSL 擴展。
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
public function create_token() {
$builder = new Builder();
$signer = new Sha256();
// 設置簽發者
$builder->setIssuer('http://xx.com');
// 設置接收者
$builder->setAudience('http://xx.com');
// 設置 ID,能夠用來區分
$builder->setId('4f1g23a12aa', true);
// 設置簽發時間
$builder->setIssuedAt(time());
// 在 60 秒內該 token 沒法使用
$builder->setNotBefore(time() + 60);
// 設置過時時間位 2 小時
$builder->setExpiration(time() + 7200);
// 設置自定義的 payload 信息
$builder->set('userID', 6666);
$builder->set('userName', 'kika');
$builder->set('userSex', 'm');
// sha256 簽名,密鑰字符串能夠自定義
$builder->sign($signer, 'signatureString');
// 獲取生成的token
$token = $builder->getToken();
// 能夠經過 Cookie 傳輸
set_cookie('jwt', $token, 7200);
// 也能夠經過 HTTP Header 傳輸,在前端保存 token 後添加到 HTTP Header 便可:Authorization: Bearer xx.xx.xx
// 查看字段內容
$token = explode('.', $token);
echo base64_decode($token[0]).'<br/>';
echo base64_decode($token[1]).'<br/>';
}
把上面使用字符串加密的這一行:
$builder->sign($signer, 'signatureString');
替換爲使用密鑰文件加密便可,須要提供私鑰地址:
$builder->sign($signer, $keychain->getPrivateKey('私鑰地址'));
在每個請求頭裏加入 Authorization,並加上 Bearer:
fetch('api/user', {
headers: {
'Authorization': 'Bearer ' + token
}
})
經過 Cookie 傳輸 JWT 信息:
if ($token = get_cookie('jwt')) {
$rs = $this->verify_token($token);
if ($rs) {
echo 'you have right jwt<br />';
} else {
echo 'error<br />';
}
}
經過 HTTP Header 傳輸 JWT 信息:
$headers = apache_request_headers();
if (!empty($headers['Authorization']) && $token = $headers['Authorization']) {
$token = substr($token, strpos($token, 'Bearer ') + 7);
$rs = $this->verify_token($token);
if ($rs) {
echo 'you have right jwt from Authorization<br />';
} else {
echo 'error Authorization<br />';
}
}
直接從 $token
中獲取全部數據:
public function get_claims ($token) {
$parser = new Parser();
$parse = $parser->parse($token);
return $parse->getClaims();
}
也能夠獲取單條數據:
$parse->getClaim('aud');
內容比較多,另寫一篇,參考 這裏。