關於 Token 認證機制,這裡不做更多解釋。不清楚的可以看我的這篇文章:Web開發中常見的認證機制
GitHub 地址:koa-jwt-sample
所需庫
- bcrypt – 用於加密密碼
- koa-jwt – jwt 中介軟體
- jsonwebtoken – 用於生成token下發給瀏覽器,在 koa2 以後的版本不再提供 jsonwebtoken 的方法,所以需要另行安裝。
實現思路
整個方案實現的流程和思路很清晰,大致分為下面幾步:
- 自定義 401 攔截中介軟體,用於攔截 token 不存在或者失效的情況
- 配置 koa-jwt
- 註冊實現
- 登入實現
執行專案
該專案需要你已經裝好 mongodb,並啟動。關於 mongodb 的配置見 config/index.js
。
npm run start
該專案提供了三個 api
- /api/register
- /api/login
- /api/users
其中 /api/register
和 /api/login
為 public api,無需token就能訪問。/users
則為 private api,需要傳入正確的 token 才能訪問。
自定義 401 handler
使用了 koa-jwt
中介軟體後,如果沒有token,或者token失效,該中介軟體會給出對應的錯誤資訊。如果沒有自定義中介軟體的話,會直接將 koa-jwt
暴露的錯誤資訊直接返回給使用者。
// server/middlewares/errorHandle.js
export default errorHandle = (ctx, next) => {
return next().catch((err) => {
if (err.status === 401) {
ctx.status = 401;
ctx.body = {
error: err.originalError ? err.originalError.message : err.message,
};
} else {
throw err;
}
});
}
然後在 index.js
中使用該中介軟體
app
.use(errorHandle)
使用 koa-jwt
在 index.js
中加入 koa-jwt
中介軟體。
const secert = `jwt_secret`
app
.use(jwt({
secret,
}).unless({
path: [//register/, //login/],
}))
其中 secret
是用於加密的key,不侷限於字串,也可以是一個檔案。
// https://github.com/koajs/jwt#token-verification-exceptions
var publicKey = fs.readFileSync(`/path/to/public.pub`);
app.use(jwt({ secret: publicKey }));
unless()
用於設定哪些 api 是不需要通過 token 驗證的。也就是我們通常說的 public api,無需登入就能訪問的 api。在這個例子中,設定了 /register
和 /login
兩個 api 無需 token 檢查。
在使用 koa-jwt
後,所有的路由(除了 unless()
設定的路由除外)都會檢查 Header
首部中的 token,是否存在、是否有效。只有正確之後才能正確的訪問。
註冊實現
註冊很簡單,這裡只是簡單的將密碼加密,將資訊存入資料庫。實際專案中,還需要對使用者輸入的欄位進行驗證。
/**
* you can register with
* curl -X POST http://localhost:3200/api/register -H `cache-control: no-cache` -H `content-type: application/x-www-form-urlencoded` -d `username=superman2&password=123456`
*/
async register(ctx) {
const { body } = ctx.request;
try {
if (!body.username || !body.password) {
ctx.status = 400;
ctx.body = {
error: `expected an object with username, password but got: ${body}`,
}
return;
}
body.password = await bcrypt.hash(body.password, 5)
let user = await User.find({ username: body.username });
if (!user.length) {
const newUser = new User(body);
user = await newUser.save();
ctx.status = 200;
ctx.body = {
message: `註冊成功`,
user,
}
} else {
ctx.status = 406;
ctx.body = {
message: `使用者名稱已經存在`,
}
}
} catch (error) {
ctx.throw(500)
}
}
登入實現
使用者輸入使用者名稱和密碼登入,如果使用者名稱和密碼正確的話,使用 jsonwebtoken.sign()
生成 token,並返回給客戶端。客戶端將token儲存在本地儲存,在每次的 HTTP 請求中,都將 token 新增在 HTTP Header Authorazition: Bearer token
中。然後後端每次去驗證該token的正確與否。只有token正確後才能訪問到對應的資源。
/** you can login with
* curl -X POST http://localhost:3200/api/login/ -H `cache-control: no-cache` -H `content-type: application/x-www-form-urlencoded` -d `username=superman2&password=123456`
*/
async login(ctx) {
const { body } = ctx.request
try {
const user = await User.findOne({ username: body.username });
if (!user) {
ctx.status = 401
ctx.body = {
message: `使用者名稱錯誤`,
}
return;
}
// 匹配密碼是否相等
if (await bcrypt.compare(body.password, user.password)) {
ctx.status = 200
ctx.body = {
message: `登入成功`,
user: user.userInfo,
// 生成 token 返回給客戶端
token: jsonwebtoken.sign({
data: user,
// 設定 token 過期時間
exp: Math.floor(Date.now() / 1000) + (60 * 60), // 60 seconds * 60 minutes = 1 hour
}, secret),
}
} else {
ctx.status = 401
ctx.body = {
message: `密碼錯誤`,
}
}
} catch (error) {
ctx.throw(500)
}
}
需要注意的是,在使用 jsonwebtoken.sign()
時,需要傳入的 secret
引數,這裡的 secret
必須要與 前面設定 jwt()
中的 secret
一致。
更多關於 jsonwebtoken
的方法,可見:https://github.com/auth0/node-jsonwebtoken
在登入後,拿著返回的 token,這時候去訪問 /api/users
,就能正確獲得使用者列表。
curl -X GET http://localhost:3200/api/users -H `authorization: Bearer token` -H `cache-control: no-cache`