最近研究了下基於token的身份驗證,並將這種機制整合在個人專案中。現在很多網站的認證方式都從傳統的seesion+cookie轉向token校驗。對比傳統的校驗方式,token確實有更好的擴充套件性與安全性。
傳統的session+cookie身份驗證
由於HTTP是無狀態的,它並不記錄使用者的身份。使用者將賬號與密碼傳送給伺服器後,後臺通過校驗,但是並沒有記錄狀態,於是下一次使用者的請求仍然需要校驗身份。為了解決這一問題,需要在服務端生成一條包含使用者身份的記錄,也就是session,再將這條記錄傳送給使用者並儲存在使用者本地,即cookie。接下來使用者的請求都會帶上這條cookie,若客戶端的cookie與服務端的session能對應上,則說明使用者身份驗證通過。
token身份校驗
流程大致如下:
- 第一次請求時,使用者傳送賬號與密碼
- 後臺校驗通過,則會生成一個有時效性的token,再將此token傳送給使用者
- 使用者獲得token後,將此token儲存在本地,一般儲存在localstorage或cookie
- 之後的每次請求都會將此token新增在請求頭裡,所有需要校驗身份的介面都會被校驗token,若token解析後的資料包含使用者身份資訊,則身份驗證通過。
對比傳統的校驗方式,token校驗有如下優勢:
- 在基於token的認證,token通過請求頭傳輸,而不是把認證資訊儲存在session或者cookie中。這意味著無狀態。你可以從任意一種可以傳送HTTP請求的終端向伺服器傳送請求。
- 可以避免CSRF攻擊
- 當在應用中進行 session的讀,寫或者刪除操作時,會有一個檔案操作發生在作業系統的temp 資料夾下,至少在第一次時。假設有多臺伺服器並且 session 在第一臺服務上建立。當你再次傳送請求並且這個請求落在另一臺伺服器上,session 資訊並不存在並且會獲得一個“未認證”的響應。我知道,你可以通過一個粘性 session 解決這個問題。然而,在基於 token 的認證中,這個問題很自然就被解決了。沒有粘性 session 的問題,因為在每個傳送到伺服器的請求中這個請求的 token 都會被攔截。
下面介紹一下利用node+jwt(jwt教程)搭建簡易的token身份校驗
示例
當使用者第一次登入時,提交賬號與密碼至伺服器,伺服器校驗通過,則生成對應的token,程式碼如下:
const fs = require(`fs`);
const path = require(`path`);
const jwt = require(`jsonwebtoken`);
//生成token的方法
function generateToken(data){
let created = Math.floor(Date.now() / 1000);
let cert = fs.readFileSync(path.join(__dirname, `../config/pri.pem`));//私鑰
let token = jwt.sign({
data,
exp: created + 3600 * 24
}, cert, {algorithm: `RS256`});
return token;
}
//登入介面
router.post(`/oa/login`, async (ctx, next) => {
let data = ctx.request.body;
let {name, password} = data;
let sql = `SELECT uid FROM t_user WHERE name=? and password=? and is_delete=0`, value = [name, md5(password)];
await db.query(sql, value).then(res => {
if (res && res.length > 0) {
let val = res[0];
let uid = val[`uid`];
let token = generateToken({uid});
ctx.body = {
...Tips[0], data: {token}
}
} else {
ctx.body = Tips[1006];
}
}).catch(e => {
ctx.body = Tips[1002];
});
});
複製程式碼
使用者通過校驗將獲取到的token存放在本地:
store.set(`loginedtoken`,token);//store為外掛
複製程式碼
之後客戶端請求需要驗證身份的介面,都會將token放在請求頭裡傳遞給服務端:
service.interceptors.request.use(config => {
let params = config.params || {};
let loginedtoken = store.get(`loginedtoken`);
let time = Date.now();
let {headers} = config;
headers = {...headers,loginedtoken};
params = {...params,_:time};
config = {...config,params,headers};
return config;
}, error => {
Promise.reject(error);
})
複製程式碼
服務端對所有需要登入的介面均攔截token並校驗合法性。
function verifyToken(token){
let cert = fs.readFileSync(path.join(__dirname, `../config/pub.pem`));//公鑰
try{
let result = jwt.verify(token, cert, {algorithms: [`RS256`]}) || {};
let {exp = 0} = result,current = Math.floor(Date.now()/1000);
if(current <= exp){
res = result.data || {};
}
}catch(e){
}
return res;
}
app.use(async(ctx, next) => {
let {url = ``} = ctx;
if(url.indexOf(`/user/`) > -1){//需要校驗登入態
let header = ctx.request.header;
let {loginedtoken} = header;
if (loginedtoken) {
let result = verifyToken(loginedtoken);
let {uid} = result;
if(uid){
ctx.state = {uid};
await next();
}else{
return ctx.body = Tips[1005];
}
} else {
return ctx.body = Tips[1005];
}
}else{
await next();
}
});
複製程式碼
本示例使用的公鑰與私鑰可自己生成,操作如下:
- 開啟命令列工具,輸入openssl,開啟openssl;
- 生成私鑰:
genrsa -out rsa_private_key.pem 2048
- 生成公鑰:
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem