Node.js 應用:Koa2 使用 JWT 進行鑒權

前言

在前後端分離的開發中,通過 Restful API 進行數據交互時,如果沒有對 API 進行保護,那麼別人就可以很容易地獲取並調用這些 API 進行操作。那麼服務器端要如何進行鑑權呢?

Json Web Token 簡稱為 JWT,它定義了一種用於簡潔、自包含的用於通信雙方之間以 JSON 對象的形式安全傳遞信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公鑰密鑰對進行簽名。

說得好像跟真的一樣,那麼到底要怎麼進行認證呢?

Node.js 應用:Koa2 使用 JWT 進行鑑權

首先用戶登錄時,輸入用戶名和密碼後請求服務器登錄接口,服務器驗證用戶名密碼正確後,生成token並返回給前端,前端存儲token,並在後面的請求中把token帶在請求頭中傳給服務器,服務器驗證token有效,返回正確數據。

既然服務器端使用 Koa2 框架進行開發,除了要使用到 jsonwebtoken 庫之外,還要使用一個 koa-jwt 中間件,該中間件針對 Koa 對 jsonwebtoken 進行了封裝,使用起來更加方便。下面就來看看是如何使用的。

生成token

這裡註冊了個 /login 的路由,用於用戶登錄時獲取token。

const router = require('koa-router')();

const jwt = require('jsonwebtoken');

const userModel = require('../models/userModel.js');

router.post('/login', async (ctx) => {

const data = ctx.request.body;

if(!data.name || !data.password){

return ctx.body = {

code: '000002',

data: null,

msg: '參數不合法'

}

}

const result = await userModel.findOne({

name: data.name,

password: data.password

})

if(result !== null){

const token = jwt.sign({

name: result.name,

_id: result._id

}, 'my_token', { expiresIn: '2h' });

return ctx.body = {

code: '000001',

data: token,

msg: '登錄成功'

}

}else{

return ctx.body = {

code: '000002',

data: null,

msg: '用戶名或密碼錯誤'

}

}

});

module.exports = router;

在驗證了用戶名密碼正確之後,調用 jsonwebtoken 的 sign() 方法來生成token,接收三個參數,第一個是載荷,用於編碼後存儲在 token 中的數據,也是驗證 token 後可以拿到的數據;第二個是密鑰,自己定義的,驗證的時候也是要相同的密鑰才能解碼;第三個是options,可以設置 token 的過期時間。

獲取token

接下來就是前端獲取 token,這裡是在 vue.js 中使用 axios 進行請求,請求成功之後拿到 token 保存到 localStorage 中。這裡登錄成功後,還把當前時間存了起來,除了判斷 token 是否存在之外,還可以再簡單的判斷一下當前 token 是否過期,如果過期,則跳登錄頁面

submit(){

axios.post('/login', {

name: this.username,

password: this.password

}).then(res => {

if(res.code === '000001'){

localStorage.setItem('token', res.data);

localStorage.setItem('token_exp', new Date().getTime());

this.$router.push('/');

}else{

alert(res.msg);

}

})

}

然後請求服務器端API的時候,把 token 帶在請求頭中傳給服務器進行驗證。每次請求都要獲取 localStorage 中的 token,這樣很麻煩,這裡使用了 axios 的請求攔截器,對每次請求都進行了取 token 放到 headers 中的操作。

axios.interceptors.request.use(config => {

const token = localStorage.getItem('token');

config.headers.common['Authorization'] = 'Bearer ' + token;

return config;

})

驗證token

通過 koa-jwt 中間件來進行驗證,用法也非常簡單

const koa = require('koa');

const koajwt = require('koa-jwt');

const app = new koa();

// 錯誤處理

app.use((ctx, next) => {

return next().catch((err) => {

if(err.status === 401){

ctx.status = 401;

ctx.body = 'Protected resource, use Authorization header to get access\n';

}else{

throw err;

}

})

})

app.use(koajwt({

secret: 'my_token'

}).unless({

path: [/\/user\/login/]

}));

通過 app.use 來調用該中間件,並傳入密鑰 {secret: 'my_token'},unless 可以指定哪些 URL 不需要進行 token 驗證。token 驗證失敗的時候會拋出401錯誤,因此需要添加錯誤處理,而且要放在 app.use(koajwt()) 之前,否則不執行。

如果請求時沒有token或者token過期,則會返回401。

解析koa-jwt

我們上面使用 jsonwebtoken 的 sign() 方法來生成 token 的,那麼 koa-jwt 做了些什麼幫我們來驗證 token。

resolvers/auth-header.js

module.exports = function resolveAuthorizationHeader(ctx, opts) {

if (!ctx.header || !ctx.header.authorization) {

return;

}

const parts = ctx.header.authorization.split(' ');

if (parts.length === 2) {

const scheme = parts[0];

const credentials = parts[1];

if (/^Bearer$/i.test(scheme)) {

return credentials;

}

}

if (!opts.passthrough) {

ctx.throw(401, 'Bad Authorization header format. Format is "Authorization: Bearer "');

}

};

在 auth-header.js 中,判斷請求頭中是否帶了 authorization,如果有,將 token 從 authorization 中分離出來。如果沒有 authorization,則代表了客戶端沒有傳 token 到服務器,這時候就拋出 401 錯誤狀態。

verify.js

const jwt = require('jsonwebtoken');

module.exports = (...args) => {

return new Promise((resolve, reject) => {

jwt.verify(...args, (error, decoded) => {

error ? reject(error) : resolve(decoded);

});

});

};

在 verify.js 中,使用 jsonwebtoken 提供的 verify() 方法進行驗證返回結果。jsonwebtoken 的 sign() 方法來生成 token 的,而 verify() 方法則是用來認證和解析 token。如果 token 無效,則會在此方法被驗證出來。

index.js

const decodedToken = await verify(token, secret, opts);

if (isRevoked) {

const tokenRevoked = await isRevoked(ctx, decodedToken, token);

if (tokenRevoked) {

throw new Error('Token revoked');

}

}

ctx.state[key] = decodedToken; // 這裡的key = 'user'

if (tokenKey) {

ctx.state[tokenKey] = token;

}

在 index.js 中,調用 verify.js 的方法進行驗證並解析 token,拿到上面進行 sign() 的數據 {name: result.name, _id: result._id},並賦值給 ctx.state.user,在控制器中便可以直接通過 ctx.state.user 拿到 name 和 _id。

安全性

  • 如果 JWT 的加密密鑰洩露的話,那麼就可以通過密鑰生成 token,隨意的請求 API 了。因此密鑰絕對不能存在前端代碼中,不然很容易就能被找到。
  • 在 HTTP 請求中,token 放在 header 中,中間者很容易可以通過抓包工具抓取到 header 裡的數據。而 HTTPS 即使能被抓包,但是它是加密傳輸的,所以也拿不到 token,就會相對安全了。

總結

這上面就是 jwt 基本的流程,這或許不是最完美的,但在大多數登錄中使用已經足夠了。

上面的代碼可能不夠具體,這裡使用 Koa + mongoose + vue.js 實現的一個例子 : jwt-demo,可以做為參考。


分享到:


相關文章: