koa 實現 jwt 認證

超人666發表於2019-02-16

關於 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`

相關文章