一、四種鑑權方式
目前我們常用的鑑權有四種:
- HTTP Basic Authentication
- session-cookie
- Token 驗證
- OAuth(開放授權)
二、HTTP Basic Authentication
這種授權方式是瀏覽器遵守http協議實現的基本授權方式,HTTP協議進行通訊的過程中,HTTP協議定義了基本認證允許HTTP伺服器對客戶端進行使用者身份證的方法。
認證過程:
1、 客戶端向伺服器請求資料,請求的內容可能是一個網頁或者是一個ajax非同步請求,此時,假設客戶端尚未被驗證,則客戶端提供如下請求至伺服器:
Get /index.html HTTP/1.0
Host:www.google.com
複製程式碼
2、 伺服器向客戶端傳送驗證請求程式碼401,(WWW-Authenticate: Basic realm=”google.com”這句話是關鍵,如果沒有客戶端不會彈出使用者名稱和密碼輸入介面)伺服器返回的資料大抵如下:
HTTP/1.0 401 Unauthorised
Server: SokEvo/1.0
WWW-Authenticate: Basic realm=”google.com”
Content-Type: text/html
Content-Length: xxx
複製程式碼
3、 當符合http1.0或1.1規範的客戶端(如IE,FIREFOX)收到401返回值時,將自動彈出一個登入視窗,要求使用者輸入使用者名稱和密碼。
4、 使用者輸入使用者名稱和密碼後,將使用者名稱及密碼以BASE64加密方式加密,並將密文放入前一條請求資訊中,則客戶端傳送的第一條請求資訊則變成如下內容:
Get /index.html HTTP/1.0
Host:www.google.com
Authorization: Basic d2FuZzp3YW5n
複製程式碼
注:d2FuZzp3YW5n
表示加密後的使用者名稱及密碼(使用者名稱:密碼 然後通過base64加密,加密過程是瀏覽器預設的行為,不需要我們人為加密,我們只需要輸入使用者名稱密碼即可)
5、 伺服器收到上述請求資訊後,將 Authorization
欄位後的使用者資訊取出、解密,將解密後的使用者名稱及密碼與使用者資料庫進行比較驗證,如使用者名稱及密碼正確,伺服器則根據請求,將所請求資源傳送給客戶端
效果:
客戶端未未認證的時候,會彈出使用者名稱密碼輸入框,這個時候請求時屬於 pending
狀態,當使用者輸入使用者名稱密碼的時候客戶端會再次傳送帶 Authentication
頭的請求。
server.js
let express = require("express");
let app = express();
app.use(express.static(__dirname+'/public'));
app.get("/Authentication_base",function(req,res){
console.log('req.headers.authorization:',req.headers)
if(!req.headers.authorization){
res.set({
'WWW-Authenticate':'Basic realm="wang"'
});
res.status(401).end();
}else{
let base64 = req.headers.authorization.split(" ")[1];
let userPass = new Buffer(base64, 'base64').toString().split(":");
let user = userPass[0];
let pass = userPass[1];
if(user=="wang"&&pass="wang"){
res.end("OK");
}else{
res.status(401).end();
}
}
})
app.listen(9090)
複製程式碼
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>HTTP Basic Authentication</title>
</head>
<body>
<div></div>
<script src="js/jquery-3.2.1.js"></script>
<script>
$(function(){
send('./Authentication_base');
})
var send = function(url){
$.ajax({
url : url,
method : 'GET',
});
}
</script>
</body>
</html>
複製程式碼
優點: 基本認證的一個優點是基本上所有流行的網頁瀏覽器都支援基本認證。基本認證很少在可公開訪問的網際網路網站上使用,有時候會在小的私有系統中使用(如路由器網頁管理介面)。後來的機制HTTP摘要認證是為替代基本認證而開發的,允許金鑰以相對安全的方式在不安全的通道上傳輸。 程式設計師和系統管理員有時會在可信網路環境中使用基本認證,使用Telnet或其他明文網路協議工具手動地測試Web伺服器。這是一個麻煩的過程,但是網路上傳輸的內容是人可讀的,以便進行診斷。
缺點:
雖然基本認證非常容易實現,但該方案建立在以下的假設的基礎上,即:客戶端和伺服器主機之間的連線是安全可信的。特別是,如果沒有使用SSL/TLS
這樣的傳輸層安全的協議,那麼以明文傳輸的金鑰和口令很容易被攔截。該方案也同樣沒有對伺服器返回的資訊提供保護。
現存的瀏覽器儲存認證資訊直到標籤頁或瀏覽器被關閉,或者使用者清除歷史記錄。HTTP沒有為伺服器提供一種方法指示客戶端丟棄這些被快取的金鑰。這意味著伺服器端在使用者不關閉瀏覽器的情況下,並沒有一種有效的方法來讓使用者登出。
三、session-cookie
3.1 cookie
Http協議是一個無狀態的協議,伺服器不會知道到底是哪一臺瀏覽器訪問了它,因此需要一個標識用來讓伺服器區分不同的瀏覽器。cookie
就是這個管理伺服器與客戶端之間狀態的標識。
cookie
的原理是,瀏覽器第一次向伺服器傳送請求時,伺服器在 response
頭部設定 Set-Cookie
欄位,瀏覽器收到響應就會設定 cookie
並儲存,在下一次該瀏覽器向伺服器傳送請求時,就會在 request
頭部自動帶上 Cookie
欄位,伺服器端收到該 cookie
用以區分不同的瀏覽器。當然,這個 cookie
與某個使用者的對應關係應該在第一次訪問時就存在伺服器端,這時就需要 session
了。
const http = require('http')
http.createServer((req, res) => {
if (req.url === '/favicon.ico') {
return
} else {
res.setHeader('Set-Cookie', 'name=zhunny')
res.end('Hello Cookie')
}
}).listen(3000)
複製程式碼
3.2 session
session
是會話的意思,瀏覽器第一次訪問服務端,服務端就會建立一次會話,在會話中儲存標識該瀏覽器的資訊。它與 cookie
的區別就是 session
是快取在服務端的,cookie
則是快取在客戶端,他們都由服務端生成,為了彌補 Http
協議無狀態的缺陷。
3.3 session-cookie認證
- 伺服器在接受客戶端首次訪問時在伺服器端建立seesion,然後儲存seesion(我們可以將seesion儲存在 記憶體中,也可以儲存在redis中,推薦使用後者),然後給這個session生成一個唯一的標識字串,然後在 響應頭中種下這個唯一標識字串。
- 簽名。這一步通過祕鑰對sid進行簽名處理,避免客戶端修改sid。(非必需步驟)
- 瀏覽器中收到請求響應的時候會解析響應頭,然後將sid儲存在本地cookie中,瀏覽器在下次http請求的請求頭中會帶上該域名下的cookie資訊。
- 伺服器在接受客戶端請求時會去解析請求頭cookie中的sid,然後根據這個sid去找伺服器端儲存的該客戶端的session,然後判斷該請求是否合法。
const http = require('http')
//此時session存在記憶體中
const session = {}
http.createServer((req, res) => {
const sessionKey = 'sid'
if (req.url === '/favicon.ico') {
return
} else {
const cookie = req.headers.cookie
//再次訪問,對sid請求進行認證
if (cookie && cookie.indexOf(sessionKey) > -1) {
res.end('Come Back')
}
//首次訪問,生成sid,儲存在伺服器端
else {
const sid = (Math.random() * 9999999).toFixed()
res.setHeader('Set-Cookie', `${sessionKey}=${sid}`)
session[sid] = { name: 'zhunny' }
res.end('Hello Cookie')
}
}
}).listen(3000)
複製程式碼
3.4 redis
redis是一個鍵值伺服器,可以專門放session的鍵值對。如何在koa中使用session:
const koa = require('koa')
const app = new koa()
const session = require('koa-session')
const redisStore = require('koa-redis')
const redis = require('redis')
const redisClient = redis.createClient(6379, 'localhost')
const wrapper = require('co-redis')
const client = wrapper(redisClient)
//加密sessionid
app.keys = ['session secret']
const SESS_CONFIG = {
key: 'kbb:sess',
//此時讓session儲存在redis中
store: redisStore({ client })
}
app.use(session(SESS_CONFIG, app))
app.use(ctx => {
//檢視redis中的內容
redisClient.keys('*', (errr, keys) => {
console.log('keys:', keys)
keys.forEach(key => {
redisClient.get(key, (err, val) => {
console.log(val)
})
})
})
if (ctx.path === '/favicon.ico') return
let n = ctx.session.count || 0
ctx.session.count = ++n
ctx.body = `第${n}次訪問`
})
app.listen(3000)
複製程式碼
3.5 使用者登入認證
使用session-cookie做登入認證時,登入時儲存session,退出登入時刪除session,而其他的需要登入後才能操作的介面需要提前驗證是否存在session,存在才能跳轉頁面,不存在則回到登入頁面。
在koa中做一個驗證的中介軟體,在需要驗證的介面中使用該中介軟體。
//前端程式碼
async login() {
await axios.post('/login', {
username: this.username,
password: this.password
})
},
async logout() {
await axios.post('/logout')
},
async getUser() {
await axios.get('/getUser')
}
複製程式碼
//中介軟體 auth.js
module.exports = async (ctx, next) => {
if (!ctx.session.userinfo) {
ctx.body = {
ok: 0,
message: "使用者未登入" };
} else {
await next();
} };
//需要驗證的介面
router.get('/getUser', require('auth'), async (ctx) => {
ctx.body = {
message: "獲取資料成功",
userinfo: ctx.session.userinfo
}
})
//登入
router.post('/login', async (ctx) => {
const {
body
} = ctx.request
console.log('body', body)
//設定session
ctx.session.userinfo = body.username;
ctx.body = {
message: "登入成功"
}
})
//登出
router.post('/logout', async (ctx) => {
//設定session
delete ctx.session.userinfo
ctx.body = {
message: "登出系統"
}
})
複製程式碼
四、Token
token
是一個令牌,瀏覽器第一次訪問服務端時會簽發一張令牌,之後瀏覽器每次攜帶這張令牌訪問服務端就會認證該令牌是否有效,只要服務端可以解密該令牌,就說明請求是合法的,令牌中包含的使用者資訊還可以區分不同身份的使用者。一般 token
由使用者資訊、時間戳和由 hash
演算法加密的簽名構成。
4.1 Token認證流程
- 客戶端使用使用者名稱跟密碼請求登入
- 服務端收到請求,去驗證使用者名稱與密碼
- 驗證成功後,服務端會簽發一個
Token
,再把這個Token
傳送給客戶端 - 客戶端收到
Token
以後可以把它儲存起來,比如放在Cookie
裡或者Local Storage
裡 - 客戶端每次向服務端請求資源的時候需要帶著服務端簽發的
Token
- 服務端收到請求,然後去驗證客戶端請求裡面帶著的
Token
(request頭部新增Authorization),如果驗證成功,就向客戶端返回請求的資料 ,如果不成功返回401錯誤碼,鑑權失敗。
4.2 Token和session的區別
session-cookie
的缺點:
(1)認證方式侷限於在瀏覽器中使用,cookie
是瀏覽器端的機制,如果在app端就無法使用 cookie
。
(2)為了滿足全域性一致性,我們最好把 session
儲存在 redis
中做持久化,而在分散式環境下,我們可能需要在每個伺服器上都備份,佔用了大量的儲存空間。
(3)在不是 Https
協議下使用 cookie
,容易受到 CSRF 跨站點請求偽造攻擊。
token的缺點:
(1)加密解密消耗使得 token
認證比 session-cookie
更消耗效能。
(2)token
比 sessionId
大,更佔頻寬。
兩者對比,它們的區別顯而易見:
(1)token
認證不侷限於 cookie
,這樣就使得這種認證方式可以支援多種客戶端,而不僅是瀏覽器。且不受同源策略的影響。
(2)不使用 cookie
就可以規避CSRF攻擊。
(3)token
不需要儲存,token
中已包含了使用者資訊,伺服器端變成無狀態,伺服器端只需要根據定義的規則校驗這個 token
是否合法就行。這也使得 token
的可擴充套件性更強。
4.3 JWT(JSON Web Token)
基於 token
的解決方案有許多,常用的是JWT
,JWT
的原理是,伺服器認證以後,生成一個 JSON
物件,這個 JSON
物件肯定不能裸傳給使用者,那誰都可以篡改這個物件傳送請求。因此這個 JSON
物件會被伺服器端簽名加密後返回給使用者,返回的內容就是一張令牌,以後使用者每次訪問伺服器端就帶著這張令牌。
這個 JSON
物件可能包含的內容就是使用者的資訊,使用者的身份以及令牌的過期時間。
4.3.1 JWT的組成部分
在該網站JWT,可以解碼或編碼一個JWT。一個JWT形如:
它由三部分組成:Header(頭部)、Payload(負載)、Signature(簽名)。
- Header部分是一個JSON物件,描述JWT的後設資料。一般描述資訊為該Token的加密演算法以及Token的型別。{"alg": "HS256","typ": "JWT"}的意思就是,該token使用HS256加密,token型別是JWT。這個部分基本相當於明文,它將這個JSON物件做了一個Base64轉碼,變成一個字串。Base64編碼解碼是有演算法的,解碼過程是可逆的。頭部資訊預設攜帶著兩個欄位。
- Payload 部分也是一個 JSON 物件,用來存放實際需要傳遞的資料。有7個官方欄位,還可以在這個部分定義私有欄位。一般存放使用者名稱、使用者身份以及一些JWT的描述欄位。它也只是做了一個Base64編碼,因此肯定不能在其中存放祕密資訊,比如說登入密碼之類的。
- Signature是對前面兩個部分的簽名,防止資料篡改,如果前面兩段資訊被人修改了傳送給伺服器端,此時伺服器端是可利用簽名來驗證資訊的正確性的。簽名需要金鑰,金鑰是伺服器端儲存的,使用者不知道。算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字串,每個部分之間用"點"(.)分隔,就可以返回給使用者。
4.3.2 JWT的特點
- JWT 預設是不加密,但也是可以加密的。生成原始 Token 以後,可以用金鑰再加密一次。
- JWT 不加密的情況下,不能將祕密資料寫入 JWT。
- JWT 不僅可以用於認證,也可以用於交換資訊。有效使用 JWT,可以降低伺服器查詢資料庫的次數。
- JWT 的最大缺點是,由於伺服器不儲存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的許可權。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。
- JWT 本身包含了認證資訊,一旦洩露,任何人都可以獲得該令牌的所有許可權。為了減少盜用,JWT 的有效期應該設定得比較短。對於一些比較重要的許可權,使用時應該再次對使用者進行認證。
- 為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用 HTTPS 協議傳輸。
4.3.3 JWT驗證使用者登入
//前端程式碼
//axios的請求攔截器,在每個request請求頭上加JWT認證資訊
axios.interceptors.request.use(
config => {
const token = window.localStorage.getItem("token");
if (token) {
// 判斷是否存在token,如果存在的話,則每個http header都加上token
// Bearer是JWT的認證頭部資訊
config.headers.common["Authorization"] = "Bearer " + token;
}
return config;
},
err => {
return Promise.reject(err);
}
);
//登入方法:在將後端返回的JWT存入localStorage
async login() {
const res = await axios.post("/login-token", {
username: this.username,
password: this.password
});
localStorage.setItem("token", res.data.token);
},
//登出方法:刪除JWT
async logout() {
localStorage.removeItem("token");
},
async getUser() {
await axios.get("/getUser-token");
}
複製程式碼
//後端程式碼
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
//用來簽名的金鑰
const secret = "it's a secret";
router.post("/login-token", async ctx => {
const { body } = ctx.request;
//登入邏輯,略,即查詢資料庫,若該使用者和密碼合法,即將其資訊生成一個JWT令牌傳給使用者
const userinfo = body.username;
ctx.body = {
message: "登入成功",
user: userinfo,
// 生成 token 返回給客戶端
token: jwt.sign(
{
data: userinfo,
// 設定 token 過期時間,一小時後,秒為單位
exp: Math.floor(Date.now() / 1000) + 60 * 60
},
secret
)
};
});
//jwtAuth這個中介軟體會拿著金鑰解析JWT是否合法。
//並且把JWT中的payload的資訊解析後放到state中,ctx.state用於中介軟體的傳值。
router.get(
"/getUser-token",
jwtAuth({
secret
}),
async ctx => {
// 驗證通過,state.user
console.log(ctx.state.user);
ctx.body = {
message: "獲取資料成功",
userinfo: ctx.state.user.data
};
}
)
//這種密碼學的方式使得token不需要儲存,只要服務端能拿著金鑰解析出使用者資訊,就說明該使用者是合法的。
//若要更進一步的許可權驗證,需要判斷解析出的使用者身份是管理員還是普通使用者。
複製程式碼
五、OAuth(開放授權)
OAuth(Open Authorization)是一個開放標準,允許使用者授權第三方網站訪問他們儲存在另外的服務提供者上的資訊,而不需要將使用者名稱和密碼提供給第三方網站或分享他們資料的所有內容,為了保護使用者資料的安全和隱私,第三方網站訪問使用者資料前都需要顯式的向使用者徵求授權。我們常見的提供OAuth認證服務的廠商有支付寶,QQ,微信。
OAuth協議又有1.0和2.0兩個版本。相比較1.0版,2.0版整個授權驗證流程更簡單更安全,也是目前最主要的使用者身份驗證和授權方式。
關於OAuth相關文章,可以檢視 OAuth 2.0 的一個簡單解釋、理解OAuth 2.0、OAuth 2.0 的四種方式
5.1 OAuth認證流程
OAuth就是一種授權機制。資料的所有者告訴系統,同意授權第三方應用進入系統,獲取這些資料。系統從而產生一個短期的進入令牌(token),用來代替密碼,供第三方應用使用。
OAuth有四種獲取令牌的方式,不管哪一種授權方式,第三方應用申請令牌之前,都必須先到系統備案,說明自己的身份,然後會拿到兩個身份識別碼:客戶端 ID(client ID)和客戶端金鑰(client secret)。這是為了防止令牌被濫用,沒有備案過的第三方應用,是不會拿到令牌的。
在前後端分離的情境下,我們常使用授權碼方式,指的是第三方應用先申請一個授權碼,然後再用該碼獲取令牌。
5.2 GitHub第三方登入示例
我們用例子來理清授權碼方式的流程。
- 在GitHub中備案第三方應用,拿到屬於它的客戶端ID和客戶端金鑰。
在github-settings-developer settings
中建立一個OAuth App。並填寫相關內容。填寫完成後Github會給你一個客戶端ID和客戶端金鑰。
- 此時在你的第三方網站就可以提供一個Github登入連結,使用者點選該連結後會跳轉到Github。這一步拿著客戶端ID向Github請求授權碼code。
const config = {
client_id: '28926186082164bbea8f',
client_secret: '07c4fdae1d5ca458dae3345b6d77a0add5a785ca'
}
router.get('/github/login', async (ctx) => {
var dataStr = (new Date()).valueOf();
//重定向到認證介面,並配置引數
var path = "https://github.com/login/oauth/authorize";
path += '?client_id=' + config.client_id;
//轉發到授權伺服器
ctx.redirect(path);
})
複製程式碼
- 使用者跳轉到Github,輸入Github的使用者名稱密碼,表示使用者同意使用Github身份登入第三方網站。此時就會帶著授權碼code跳回第三方網站。跳回的地址在建立該OAuth時已經設定好了。http://localhost:3000/github/callback
- 第三方網站收到授權碼,就可以拿著授權碼、客戶端ID和客戶端金鑰去向Github請求access_token令牌。
- Github收到請求,向第三方網站頒發令牌。
- 第三方網站收到令牌,就可以暫時擁有Github一些請求的許可權,比如說拿到使用者資訊,拿到這個使用者資訊之後就可以構建自己第三方網站的token,做相關的鑑權操作。
router.get('/github/callback', async (ctx) => {
console.log('callback..')
const code = ctx.query.code;
const params = {
client_id: config.client_id,
client_secret: config.client_secret,
code: code
}
let res = await axios.post('https://github.com/login/oauth/access_token', params)
const access_token = querystring.parse(res.data).access_token
res = await axios.get('https://api.github.com/user?access_token=' + access_token)
console.log('userAccess:', res.data)
ctx.body = `
<h1>Hello ${res.data.login}</h1>
<img src="${res.data.avatar_url}" alt=""/>
`
})
複製程式碼