iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 22

[今晚我想來點 Express 佐 MVC 分層架構] DAY 22 - 實作帳戶機制 (下)

前一篇完成了註冊機制,但在註冊完成時,應該要有個媒介讓我們能夠使用該帳戶,以該帳戶的名義進行操作,而不是取得整個帳戶資料,那要如何產生所謂的媒介又同時享有該帳戶的基本資訊呢?這時候 JsonWebToken (JWT) 會是很好的選擇。

JWT 是什麼?

JWT 是一種新的 token 設計方法,在 token 中附帶使用者資訊,降低伺服器請求資料庫的頻率,不過在JWT 中不建議存放敏感資訊,例如:信用卡卡號、身分證字號等,可以存放ID、使用者名稱等資訊,為什麼呢?因為存放的資訊並 無加密 ,而是以 Base64 的方式呈現。

一個標準的 JWT 格式如下:

ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9.ew0KICAiaWQiOiAiMSIsDQogICJ1c2VybmFtZSI6ICJIQU8iLA0KICAiZW1haWwiOiAiaGVsbG93d29ybGRAZXhhbXBsZS5jb20iDQp9.T468GEOrM7E2cBab30R4681FVEfxjoYZg14DAS7HgQ

仔細看這串字有 . 做為分隔,共切成三個部分:
第一段「ew0KICAiYWxnIjogIkhTMjU2IiwNCiAgInR5cCI6ICJKV1QiDQp9」為 標頭,內容為「加密方式」與「定義類型」,下方為解碼後的內容:

{
  "alg": "HS256",
  "typ": "JWT"
}

第二段「ew0KICAiaWQiOiAiMSIsDQogICJ1c2VybmFtZSI6ICJIQU8iLA0KICAiZW1haWwiOiAiaGVsbG93d29ybGRAZXhhbXBsZS5jb20iDQp9」為 內容,用來存放使用者的基本資訊,下方為解碼後的內容:

{
  "id": "1",
  "username": "HAO",
  "email": "hellowworld@example.com"
}

第三段「T468GEOrM7E2cBab30R4681FVEfxjoYZg14DAS7HgQ」為 簽章,用來驗證是否遭竄改。

實作 JWT

先安裝 JWT 的套件:

npm install jsonwebtoken

以及定義檔:

npm install @types/jsonwebtoken --save-dev

新增環境變數

由於 JWT 需要使用一組 key 進行簽章,所以添加至環境變數以方便取用:

JWT_SIGN=YOUR_JWT_SIGN

修改 Service

LocalAuthService 中匯入 jsonwebtoken:

import JWT from 'jsonwebtoken';

並新增 generateJWT 方法來產生合法的 JWT,這邊我設定有效期為 7 天:

public generateJWT(user: LocalAuthDocument): string {
  const expiry = new Date();
  expiry.setDate(expiry.getDate() + 7);
  return JWT.sign({
    _id: user._id,
    username: user.username,
    exp: expiry.getTime() / 1000
  }, (process.env.JWT_SIGN as string));
}

修改 Controller

現在要讓 LocalAuthController 中的 signup 回傳 JWT,所以要進行修改:

public async signup(req: Request): Promise<ResponseObject> {
  const { username, password, email } = req.body;
  const user = await this.localAuthSvc.addUser(username, password, email);
  const token = this.localAuthSvc.generateJWT(user);
  return this.formatResponse(token, HttpStatus.CREATED);
}

透過 Postman 再註冊一次,會看到 data 為 JWT:
https://ithelp.ithome.com.tw/upload/images/20200908/20119338pApou8ybSH.png

登入驗證機制

登入驗證可以很簡單也可以很複雜,如果今天只使用本地帳戶的方式,那恭喜你非常幸福,但如果今天有本地帳戶、Google、Facebook 等登入方式的話,就會變得較為複雜,這時候可以用一些工具來輔助我們,使流程變得較有一致性,在管理上會輕鬆許多,那是哪個工具呢?就是很熱門的 passport

什麼是 passport?

passport 是一個帳戶驗證機制的熱門套件,它提供上百種驗證機制,像是:本地帳戶驗證、OAuth 的驗證等,比較特別的是它將 驗證流程驗證機制 分離, passport 本身只處理流程 ,透過其 策略(Strategy) 套件來完成驗證機制,如下圖所示:
https://ithelp.ithome.com.tw/upload/images/20200906/20119338EYsHSIqLMU.png

安裝 passport

透過 npm 進行安裝:

npm install passport

安裝定義檔:

npm install @types/passport --save-dev

初始化 passport

passport 是中介軟體,透過初始化在 Request 物件中添加相關配置,讓其他中介軟體得以使用,所以要在 App 中新增配置:

import passport from 'passport';
private setPassport(): void {
  passport.initialize();
}

並在 contructor 中使用:

constructor() {
  this.setEnvironment();
  this.setHelmet();
  this.setCors();
  this.setPassport();
  this.registerRoute();
}

實作本地帳戶驗證

passport 提供的驗證策略非常多,我們是以本地帳號作為開發範例,若對串接 Google 帳戶等有興趣,在看完本篇有了基本概念之後,再去學習會較容易喔!我們先安裝本地帳號驗證的策略:

npm install passport-local

安裝定義檔:

npm install @types/passport-local --save-dev

建立策略

local-auth.service.ts 中匯入需要用的模組:

import passport from 'passport';
import { Strategy, VerifyFunction } from 'passport-local';

LocalAuthService 中設計一個 getter 來取得策略:

public get Strategy() {
  return new Strategy(
    { session: false },
    this.verifyUserFlow()
  );
}

session 為是否啟用 session 的配置項目,因為我們用 JWT,就不啟用 session 囉

這時候會看到 verifyUserFlow() 這個方法,就是策略要執行的驗證函式:

private verifyUserFlow(): VerifyFunction {
  return (username: string, password: string, done) => {
    this.localAuthRepo.getUser({ username })
      .then(user => {
        const error = new Error();
        if ( !user ) {
          error.message = '查無此用戶';
          (error as any).status = HttpStatus.NOT_FOUND;
          return done(error);
        }
        if ( !this.verifyPassword(user, password) ) {
          error.message = '您輸入的密碼有誤';
          (error as any).status = HttpStatus.FORBIDDEN;
          return done(error);
        }
        return done(null, user);
      })
      .catch((err: Error) => done(err));
  }
}

上方出現的 verifyPassword 為驗證密碼的方法:

private verifyPassword(user: LocalAuthDocument, password: string): boolean {
  const pair = this.localAuthRepo.hashPassword(password, user.password.salt);
  return pair.hash === user.password.hash;
}

驗證函式前兩個參數即 usernamepassword,第三個參數為結束驗證流程用的函式 done,它採用 Error-First 的方式,所以 done 的第一個參數要傳入 錯誤資訊 ,第二個為 user 資訊,第三個為自訂選項。

執行策略

透過 passport 的 authenticate 方法處理走完驗證流程的結果,由於是採用本地帳戶的策略,所以要指定策略為 local,又因為 authenticate 是採用 callback 的方式而非 Promise,所以我們將它包在 Promise 中使用:

public authenticate(...args: any[]): Promise<string> {
  return new Promise((resolve, reject) => {
    passport.authenticate('local', (err: Error, user: LocalAuthDocument) => {
      if ( err ) {
        return reject(err);
      }
      const token = this.generateJWT(user);
      resolve(token);
    })(...args);
  });
}

可以看到傳入參數為 args,主要是把 RequestResponseNextFunction 帶入 authenticate 中。

套用策略

策略規劃完後就要將它與 Controller 做連結,所以在 LocalAuthController 新增 signin 方法並透過 passport 的 use 套用策略,接著再等待 authenticate 的結果:

public async signin(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<ResponseObject> {
  passport.use(this.localAuthSvc.Strategy);
  const token = await this.localAuthSvc.authenticate(req, res, next);
  return this.formatResponse(token, HttpStatus.OK);
}

新增路由

最後就是新增路由,修改 LocalAuthRoute

protected registerRoute(): void {
  this.router.post(
    '/signup',
    express.json(),
    this.responseHandler(this.controller.signup)
  );
  this.router.post(
    '/signin',
    express.json(),
    this.responseHandler(this.controller.signin)
  );
}

透過 Postman 進行登入,下圖分別為登入成功與失敗:
https://ithelp.ithome.com.tw/upload/images/20200909/20119338pTVRNNBOtr.png

https://ithelp.ithome.com.tw/upload/images/20200909/20119338LWuQDzMXUX.png

小結

今天的內容完整實現了登入驗證機制與 JWT,對於第一次接觸 passport 的人可能會比較難懂它的運作流程,所以我這邊整理了一份本地帳戶的驗證流程圖:
https://ithelp.ithome.com.tw/upload/images/20200909/20119338yhHLeC9z7o.png

最後還有一個部分要處理,就是當使用者要新增 Todo 的時候,應該要帶著 JWT 到後端才能確認身份,但現在不管有沒有帶 JWT 都可以新增 Todo,這時候我們就必須實作另一個很重要的概念 - Guard


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 21 - 實作帳戶機制 (上)
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 23 - Guard
系列文
今晚我想來點 Express 佐 MVC 分層架構30

尚未有邦友留言

立即登入留言