編者注:我相信鑑權應該是大部分 Web 服務必備的基礎功能之一。實現許可權驗證的方式有很多種,其中 JSON Web Token(即JWT)這種使用 Token 驗證的方式受到了越來越多開發者的喜愛。其相對於傳統的驗證方式來說會更為安全一點,而且相對而言由於加密串中就包含了許可權資訊,所以不需要額外的資料庫查詢。今天我們請來了 ThinkJS 的開發人員盧士傑同學給我們實戰講解下載 ThinkJS 中如何使用 JWT 許可權驗證服務。
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊。它提供基於JSON 格式的 Token 來做安全認證。
JWT 組成
JWT 由三部分組成,分別是 header(頭部),payload(載荷),signature(簽證) 這三部分以小數點連線起來。
本例中使用名為jwt-token的cookie來儲存JWT例如:
jwt-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0.WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8;
複製程式碼
其中:
部分 | 值 |
---|---|
header | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 |
payload | eyJuYW1lIjoibHVzaGlqaWUiLCJpYXQiOjE1MzI1OTUyNTUsImV4cCI6MTUzMjU5NTI3MH0 |
signature | WZ9_poToN9llFFUfkswcpTljRDjF4JfZcmqYS0JcKO8 |
header
header 是對型別和雜湊演算法進行base64Encode之後得到的。對於比例中的header進行base64Decode可以得到:
{
"alg":"HS256”,
"typ":”JWT"
}
複製程式碼
payload
payload 是對我們需要傳輸的資訊進行base64Encode之後得到的。對於本例中的payload進行base64Decode可以得到:
{
"name":"lushijie”,
"iat":1532595255, // JWT 釋出的時間
"exp”:1532595270 // JWT 過期的時間,15秒後過期
}
複製程式碼
本例中的iat, exp 是 koa-jwt 中的預設欄位,除此之外 JWT 標準中註冊的非強制使用的宣告還有 jti,iss等,有興趣的小夥伴可以檢視更多的相關標準。 由於 payload 可以在客戶端解碼獲得,所以不建議在 payload 中存放敏感資訊,例如使用者的密碼。
signature
signature 包含了 header,payload 和 金鑰,計算公式如下:
const encodedString = base64Encode(header) + "." + base64Encode(payload);
let signature = HMACSHA256(encodedString, '金鑰');
複製程式碼
這裡金鑰是儲存在服務端的,客戶端是不知道的。
JWT 驗證
對於驗證一個 JWT 是否有效也是比較簡單的,服務端根據前面介紹的計算方法計算出 signature,和要校驗的JWT中的 signature 部分進行對比就可以了,如果 signature 部分相等則是一個有效的 JWT。
JWT 在 ThinkJS 中的實踐
下面我們在 ThinkJS 中實現使用 JWT 實現只有在登入後才能訪問一個介面。 ThinkJS 相容 koa2 的所有middleware,那就找個現成的 jwt 外掛吧,這裡我們使用 koa-jwt 外掛。koa-jwt 程式碼沒有幾行,大家可以稍微讀一下,簡單易懂,接下來我們開始使用它~ 首先我們要在 ThinkJS 中配置 koa-jwt:
公共配置
/src/config/config.js:
module.exports = {
// ...
jwt: {
secret: 'lushijie-password',
cookie: 'jwt-token',
expire: 30 // 秒
},
}
複製程式碼
因為這三個引數在不同的位置會用到,為了統一管理我們提取到了公共的 config 中。
中介軟體配置
/src/config/middleware.js
const jwt = require('koa-jwt');
const isDev = think.env === 'development';
module.exports = [
// ...
{
handle: jwt,
// match(ctx) {
// return !/^\/index\/login/.test(ctx.path);
// },
options: {
cookie: think.config('jwt')['cookie'],
secret: think.config('jwt')['secret'],
passthrough: true
}
},
// payload 這裡配置因為本例中 jwt 並沒有用到 request 解析後的引數
];
複製程式碼
起初我想通過配置 match 引數來決定某個 URL 是否需要登入認證,後來發現這樣需要配置好多的正則,比較麻煩; 其次 koa-jwt 沒有提供無權訪問自定義錯誤的鉤子,所以放棄了 match 的方案。
這裡採用了 koa-jwt 提供的配置 passthrough: true
,這個引數讓我們不管許可權驗證通過與否都可以繼續執行後面的中介軟體,只是在當前的 ctx 上設定了 payload。
我們錯誤處理需要在 logic 層進行,而不應該在 controller 層,否則會出現以下問題:如果 logic 層有有引數校驗不通過同時無權訪問,會先報引數校驗不通過資訊,然後再報無權訪問,這顯然是不符合要求的。
擴充套件 think.Controller
這裡我們對 think.Controller 做了擴充套件,這裡沒有對 think.Logic 上進行擴充套件是因為 think.Logic 繼承自 think.Controller。
/src/extend/controller.js
const jsonwebtoken = require('jsonwebtoken');
module.exports = {
authFail() {
return this.fail('JWT 驗證失敗');
},
checkAuth(target, name, descriptor) {
const action = descriptor.value;
descriptor.value = function() {
console.log(this.ctx.state.user);
const userName = this.ctx.state.user && this.ctx.state.user.name;
if (!userName) {
return this.authFail();
}
this.updateAuth(userName);
return action.apply(this, arguments);
}
return descriptor;
},
updateAuth(userName) {
const userInfo = {
name: userName
};
const {secret, cookie, expire} = this.config('jwt');
const token = jsonwebtoken.sign(userInfo, secret, {expiresIn: expire});
this.cookie(cookie, token);
return token;
}
}
複製程式碼
其中 authFail 是 JWT 驗證失敗的操作;updateAuth 是更新 JWT,此處使用 jsonwebtoken 生成 JWT 並種 cookie;checkAuth 使用了 decorator 方式實現,當然你也可以使用你喜歡的方式。
此處使用 cookie 的方式記錄生成的JWT, 當初也可以採用別的方式儲存,koa-jwt 提供了 getToken 讓我們能夠自由的獲取 JWT, 此處不再詳述。
controller 業務邏輯
/src/controller/jwt1.js
const userList = {
lushijie: '123123',
xiaoming: '456456'
};
module.exports = class extends think.Controller {
async userAction() {
const userInfo = this.ctx.state.user;
if (userInfo) {
return this.success(userInfo);
} else {
return this.fail('獲取使用者資訊失敗');
}
}
loginAction() {
const {name, password} = this.get();
if (userList[name] && password === userList[name]) {
const token = this.updateAuth(name);
return this.success(token);
} else {
return this.fail('登入失敗');
}
}
logoutAction() {
this.updateAuth(null);
return this.success('退出登入成功');
}
}
複製程式碼
jwt1 這個簡單的 controller 包含了三個簡單的功能,登入、退出與獲取使用者資訊,其中獲取使用者資訊要求必須登入之後才可以訪問。 這裡的登入只是進行了一個簡單的模擬,真實專案中的使用者驗證會比這個複雜一些,原理是一致的。
Logic 許可權驗證
/src/logic/jwt1.js
const {checkAuth} = think.Controller.prototype;
module.exports = class extends think.Logic {
@checkAuth
userAction(){
// 正常的引數驗證邏輯
}
}
複製程式碼
這樣一個驗證就完成了! 如果該 Logic 中的所有 action 都需要進行驗證,只需要給 __before 加 decorator 就可以了,其他的 action 就不用加了!