iT邦幫忙

2023 iThome 鐵人賽

0
自我挑戰組

用ChatGPT詠唱來完成工作與點亮前後端技能樹系列 第 46

【Day46】ChatGPT請教教我:NestJS!(五)- JWT 身份驗證!Guards!

  • 分享至 

  • xImage
  •  

這個章節要來討論的是以下內容

  1. 什麼是JWT (JSON Web Token)
  2. JWT的驗證原理與流程、淺談hash雜湊
  3. 用NodeJS撰寫JWT驗證邏輯
  4. 搭配NestJS實作JWT身份驗證,設置Guards
  5. 設定.evn檔來設置私鑰
  6. JWT的驗證機制和其他驗證機制的不同處

GoGo!

JWT (JSON Web Token)

先來看看「JWT」是什麼的介紹
https://ithelp.ithome.com.tw/upload/images/20231029/20135750xDj2gi3dew.png

嗯,當然可以為你解釋 JWT。JWT 的全名是「JSON Web Token」,它是一種用來在網路上安全地傳輸資訊的方式。

簡單來說,JWT 是一個簡潔且安全的方式,讓我們可以在不同的系統之間傳遞資訊,並確保資訊的真實性和完整性。這在前端工程師的工作中經常會用到,特別是在處理身份驗證和授權的情境。

一個 JWT 主要由三部分組成:

  1. 標頭 (Header): 通常包含了該 Token 的類型以及使用的簽名算法。這部分通常是固定不變的。
  2. 有效載荷 (Payload): 包含了要傳遞的資訊,以 JSON 格式儲存。例如,可以包含使用者的識別資訊或者其他需要傳遞的資訊。
  3. 簽名 (Signature): 這部分使用了前兩部分的資訊,並加上一個密鑰來進行簽名。這可以確保在傳輸過程中 Token 沒有被篡改。

當收到一個 JWT 時,我們可以用相同的密鑰來驗證簽名,確保該 Token 的真實性。這讓我們可以信任這個 Token 中的資訊。

這在前端開發中特別有用,因為它讓我們可以在不同的系統之間傳遞資訊,同時又保持了安全性。

如果你需要更詳細的範例或程式碼,也可以告訴我哦!

ChatGPT只有簡單的說「是一個簡潔且安全的方式」
JWT到底長什麼樣子?怎麼個簡潔安全?
我們繼續問下去
https://ithelp.ithome.com.tw/upload/images/20231029/20135750q4BEASm7iN.png

當然可以,讓我用簡單清楚的方式來介紹 JWT 的樣子和運作原理。

JWT 的樣子:

一個 JWT 通常看起來像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

它由三個部分以點(.)分隔而成:

  1. 標頭 (Header): 通常包含了該 Token 的類型以及使用的簽名算法。這通常是固定的,以 JSON 格式表示。

    範例:

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  2. 有效載荷 (Payload): 包含了要傳遞的資訊,以 JSON 格式儲存。例如,可以包含使用者的識別資訊或者其他需要傳遞的資訊。

    範例:

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    
  3. 簽名 (Signature): 這部分使用了前兩部分的資訊,並加上一個密鑰來進行簽名,以確保 Token 的真實性。

    範例:

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret
    )
    

JWT 的運作原理:

  1. 建立 JWT:
    • 伺服器收到一個請求,需要生成一個 JWT 來回應客戶端。
    • 伺服器將需要傳遞的資訊放入 Payload 中,例如使用者識別資訊。
    • 伺服器使用特定的密鑰對 Header 和 Payload 進行簽名,生成簽名部分。
  2. 傳遞 JWT:
    • 伺服器將生成的 JWT 回傳給客戶端,客戶端可以將其保存在 Local Storage 或者 Cookie 中。
  3. 驗證 JWT:
    • 當客戶端發送請求時,將 JWT 放在請求的 Header 中(通常以 Authorization 標頭)。
    • 伺服器收到請求後,將收到的 JWT 進行解析,並使用相同的密鑰驗證簽名部分。
  4. 使用 JWT:
    • 如果簽名驗證成功,伺服器可以信任 JWT 中的資訊,例如使用者識別資訊,並執行相應的操作。

這樣,JWT 提供了一種安全且可靠的方式來在不同系統之間傳遞資訊,同時確保了資訊的真實性和完整性。如果你需要更多範例或者有任何問題,隨時告訴我哦!

上面的那串「JWT」,如果用 . 分隔開來,分出的三段內容就是這樣

  1. 標頭 (Header):

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    
  2. 有效載荷 (Payload):

    eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
    
  3. 簽名 (Signature):

    SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

根據簽名的製作方法,可以推斷知道前兩段是經過base64的 編碼(encode)獲得
在js中將編碼解開的方式是 atob('已編碼的字串') (ascii to binary)
所以我們簡單點擊F12,打開console來試看看
https://ithelp.ithome.com.tw/upload/images/20231029/201357505RUina0ere.png

果然前兩段都可以透過 atob() 還原,變成ChatGPT說的JSON內容
用 btoa (binary to ascii) 就是 ChatGPT說的 base64UrlEncode 步驟了

而最後一段「簽名」,則是把前兩個編碼字串用.組合之後
加上「密鑰(secret)」,放入「HMACSHA256」的處理

這個「HMACSHA256」是什麼?為什麼這樣就會「安全」?

簽名 (Signature)

https://ithelp.ithome.com.tw/upload/images/20231029/20135750NhR81wfgFx.png

透過將標頭和有效載荷與一個秘密(稱為 secret)傳遞給 HMACSHA256 函數,可以保證 JWT 的安全性的原因如下:

  1. 秘密 (Secret): 秘密是一個只有伺服器知道的隨機字串。它作為簽名的關鍵。因為只有伺服器知道這個秘密,其他人無法生成相同的簽名。
  2. HMACSHA256 函數: 這是一種基於哈希函數的加密方式,它將輸入資料(在這裡是標頭和有效載荷)與秘密結合起來,生成一個固定長度的雜湊值。這個雜湊值即為簽名。
  3. 保證資訊完整性: 簽名的存在保證了 JWT 的完整性。如果有人在傳輸過程中偷偷修改了標頭或有效載荷,簽名將無法驗證,伺服器會知道這個 JWT 已經被篡改。
  4. 驗證來源: 由於只有伺服器知道秘密,只有伺服器才能生成正確的簽名。這意味著當伺服器收到一個 JWT 時,它可以使用相同的秘密來重新計算簽名,並確保它與 JWT 中的簽名匹配。

總的來說,使用 HMACSHA256 函數與秘密可以確保 JWT 在傳輸過程中不被篡改,同時也可以驗證 JWT 的來源是否為可信任的伺服器。這樣就保證了 JWT 的安全性和可靠性。

獲得了一些關鍵字:「哈希函數 (Hash)」
但他沒有跟我們解釋這樣驗證的原理是什麼,只說可用相同的「secret」來重新計算簽名
我們再問清楚一點
https://ithelp.ithome.com.tw/upload/images/20231029/20135750ty7neUqbNE.png

簽名的驗證原理和哈希函數是密切相關的,讓我來簡單地解釋:

哈希函數:

哈希函數是一種數學運算,它可以將任意大小的輸入(訊息)轉換成固定大小的輸出(雜湊值)。這個過程是單向的,也就是說,可以從雜湊值反推出原始訊息幾乎是不可能的。

哈希函數的特性包括:

  1. 固定輸出: 無論輸入的訊息大小如何,哈希函數總是產生相同長度的雜湊值。
  2. 快速運算: 能夠在相對較短的時間內計算出雜湊值。
  3. 即時性: 對於相同的輸入,哈希函數總是生成相同的雜湊值。
  4. 碰撞耐受: 雖然理論上可能會有相同的雜湊值對應到不同的輸入,但實際上在強大的哈希函數中,這是非常困難的。

簽名的驗證原理:

在 JWT 的情境下,簽名的驗證原理如下:

  1. 伺服器接收到一個 JWT,並提取其中的標頭和有效載荷。
  2. 伺服器使用相同的密鑰以及相同的哈希函數(在這裡是 HMACSHA256)來重新計算簽名。
  3. 伺服器將重新計算的簽名與 JWT 中的簽名進行比較。
    • 如果兩者相符,這表示 JWT 沒有在傳輸過程中被篡改,並且伺服器可以信任其中的資訊。
    • 如果簽名不符,這表示 JWT 已經被篡改,伺服器會拒絕信任其中的資訊。

簽名的驗證原理基於哈希函數的特性,確保了在傳輸過程中 JWT 的完整性。只有知道密鑰的伺服器才能正確生成相應的簽名,進而驗證 JWT 的真實性。

所以,前面範例中,第三部分的簽名: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
是用某個我們不曉得內容的固定「密鑰」,搭配固定的hash函數,所生成的字串

因為「相同輸入可以生成相同雜湊」
所以只要對方偽造的JWT更改了任何一個部分,像是第二段的JSON資料
經過base64編碼再丟入hash,就會跑出不一樣的內容

而且這邊要注意的是,經過Hash產生的字串,我們並沒有要把它解開。能解開的是前面兩段base64
請ChatGPT再把流程圖畫一下
https://ithelp.ithome.com.tw/upload/images/20231029/201357507VfllsdFnz.png

當然可以!以下是使用 Mermaid 來表示JWT身份驗證機制的流程以及簽名驗證的流程:

graph TD;
    A[客戶端] -->|請求| B[伺服器]
    B -->|生成JWT| C[客戶端]
    C -->|攜帶JWT| B

    D[伺服器] -->|接收請求| E[客戶端]
    E -->|攜帶JWT| D
    D -->|解析JWT| F[取得資訊]
    F -->|進行身份驗證| G[驗證通過]
    G -->|回應| E

這是JWT身份驗證機制的流程:

  1. 客戶端向伺服器發送請求。
  2. 伺服器生成JWT並返回給客戶端。
  3. 客戶端攜帶JWT在未來的請求中。

在驗證的流程中:

  1. 伺服器接收到請求並從中提取JWT。
  2. 伺服器解析JWT,獲取其中的資訊。
  3. 進行身份驗證,確保使用者的身份有效。
  4. 驗證通過,伺服器回應客戶端。

接下來是簽名驗證的流程:

graph TD;
    A[伺服器] -->|收到JWT| B[解析JWT]
    B -->|取得標頭和有效載荷| C[重新計算簽名]
    C -->|取得儲存在JWT中的簽名| D[比較兩者]
    D -->|簽名相符| E[驗證通過]
    D -->|簽名不符| F[驗證失敗]

這是簽名驗證的流程:

  1. 伺服器收到JWT並解析它。
  2. 伺服器取得JWT中的標頭和有效載荷。
  3. 重新計算簽名,使用相同的密鑰和哈希函數。
  4. 取得儲存在JWT中的簽名。
  5. 比較重新計算的簽名與JWT中的簽名。
  6. 如果兩者相符,驗證通過;如果不符,驗證失敗。

這兩個流程描述了JWT的身份驗證機制以及簽名驗證的流程。如果你需要更多的解釋或範例,請隨時告訴我!

https://ithelp.ithome.com.tw/upload/images/20231029/20135750acpGw9xuZN.png
https://ithelp.ithome.com.tw/upload/images/20231029/20135750SrLf0JKG1X.png
Wow這個流程圖很可以

NodeJS程式碼範例:簽名與驗證

接下來就是請ChatGPT直接用程式碼示範一下要怎麼製作JWT的簽名與驗證了
https://ithelp.ithome.com.tw/upload/images/20231029/20135750FlBYbVjKhp.png

當然可以!以下是使用 Node.js 實作JWT的簽名與驗證機制的範例:

首先,你需要安裝 jsonwebtoken 套件,這是用於處理JWT的套件。

npm install jsonwebtoken

接著,你可以使用以下的程式碼來進行簽名與驗證:

簽名 (Sign):

const jwt = require('jsonwebtoken');

// 假設這是你的秘密
const secret = 'mySecretKey';

const payload = {
  userId: '123456',
  username: 'john_doe'
};

const token = jwt.sign(payload, secret, { expiresIn: '1h' });

console.log('生成的Token:', token);

驗證 (Verify):

const jwt = require('jsonwebtoken');

const secret = 'mySecretKey';

const token = '在這裡插入你的JWT';

jwt.verify(token, secret, (err, decoded) => {
  if (err) {
    console.error('驗證失敗:', err);
  } else {
    console.log('解碼後的資訊:', decoded);
  }
});

這個範例演示了如何使用 Node.js 和 jsonwebtoken 套件來進行JWT的簽名與驗證。請確保在實際應用中將 secret 替換為你自己的私鑰,並適當地處理錯誤。

如果你有任何進一步的問題或需要更多的說明,請隨時告訴我!

好…的,現在的套件實在很方便啊
直接用套件來處理,非常方便!我們等最後再請ChatGPT示範一個不用套件的版本

先繼續回到我們的主題,當想要在NestJS中處理JWT身份驗證的話要怎麼做呢

NestJS + JWT驗證 + Guards

https://ithelp.ithome.com.tw/upload/images/20231029/20135750oaZl6XoTGp.png

當然可以!以下是使用 NestJS 框架實作JWT機制的示範,包含取得JWT以及在接受請求時進行身份驗證:

首先,確保你已經建立了一個 NestJS 專案。如果尚未建立,你可以使用以下指令創建一個新的 NestJS 專案:

nest new project-name

接著,我們將會進行以下步驟:

步驟一:安裝相關套件

首先,我們需要安裝 jsonwebtoken 套件,這是用於處理JWT的套件:

npm install jsonwebtoken

步驟二:建立JWT Service

src 資料夾下創建一個 jwt.service.ts 檔案,並添加以下程式碼:

import { Injectable } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class JwtService {
  private readonly secretKey = 'mySecretKey'; // 這裡放置你的私鑰

  generateToken(payload: any): string {
    return jwt.sign(payload, this.secretKey, { expiresIn: '1h' });
  }

  verifyToken(token: string): any {
    try {
      return jwt.verify(token, this.secretKey);
    } catch (error) {
      throw new Error('Token 驗證失敗');
    }
  }
}

步驟三:建立AuthGuard

在 NestJS 中,我們可以使用 AuthGuard 來進行身份驗證。在 src 資料夾下創建一個 jwt.guard.ts 檔案,並添加以下程式碼:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { JwtService } from './jwt.service';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    if (token) {
      try {
        const decoded = this.jwtService.verifyToken(token);
        request.user = decoded;
        return true;
      } catch (error) {
        return false;
      }
    }

    return false;
  }
}

步驟四:使用AuthGuard進行身份驗證

在你的 Controller 中使用 JwtAuthGuard 來進行身份驗證。例如:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt.guard';

@Controller('protected')
export class ProtectedController {
  @Get()
  @UseGuards(JwtAuthGuard)
  getProtectedData(): string {
    return '這是受保護的資料';
  }
}

步驟五:生成並返回JWT

在你的 Controller 中使用 JwtService 來生成並返回JWT。例如:

import { Controller, Get } from '@nestjs/common';
import { JwtService } from './jwt.service';

@Controller('auth')
export class AuthController {
  constructor(private readonly jwtService: JwtService) {}

  @Get('login')
  login(): { access_token: string } {
    const token = this.jwtService.generateToken({ userId: '123456' });
    return { access_token: token };
  }
}

現在,你已經設置了一個基本的JWT機制,包括生成JWT和在接受請求時進行身份驗證。當客戶端訪問 auth/login 時,將生成一個JWT並返回給客戶端。當客戶端訪問 protected 路由時,會先經過 JwtAuthGuard 進行身份驗證。

請確保在實際應用中適當地處理錯誤和設定私鑰。如果你有任何進一步的問題或需要更多的說明,請隨時告訴我!

恩..雖然提供了程式碼
但是一來他放了兩個controller,但沒有對應模組引用,就預設注入了JwtService
二來也沒有一起提供app.module.ts要怎麼設置

不夠完整,為了更好測試和讓情況簡單化,請ChatGPT給更完整可以跑的程式碼
https://ithelp.ithome.com.tw/upload/images/20231029/20135750oVG5W4cmpE.png

當然可以!我們可以把 logingetProtectedData 先放在 AppController 裡面。

首先,讓我們在 app.controller.ts 中簡單實作這兩個路由:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt.guard';
import { JwtService } from './jwt.service';

@Controller()
export class AppController {
  constructor(private readonly jwtService: JwtService) {}

  @Get('login')
  login(): { access_token: string } {
    const token = this.jwtService.generateToken({ userId: '123456' });
    return { access_token: token };
  }

  @Get('protected')
  @UseGuards(JwtAuthGuard)
  getProtectedData(): string {
    return '這是受保護的資料';
  }
}

接下來,我們需要更新 app.module.ts 來設置我們的相依模組:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { JwtAuthGuard } from './jwt.guard';
import { JwtService } from './jwt.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [JwtAuthGuard, JwtService],
})
export class AppModule {}

這樣我們就在 AppModule 中註冊了我們的 AppControllerJwtAuthGuard 以及 JwtService

現在,你可以使用 npm run start 啟動你的 NestJS 應用,並在瀏覽器或 Postman 中訪問 http://localhost:3000/loginhttp://localhost:3000/protected 來測試JWT的機制了。

如果你有任何進一步的問題或需要更多的說明,請隨時告訴我!

好耶夠完整了,照著上面的做之後
先來看看那個 'protected' 的GET方法,http://localhost:3000/protected
如果沒有攜帶任何JWT資料的時候長什麼樣子
https://ithelp.ithome.com.tw/upload/images/20231029/20135750ClqXXyNIS1.png

果然直接得到了403
接著我們要一下JWT: http://localhost:3000/login
https://ithelp.ithome.com.tw/upload/images/20231029/20135750wyDYMdmXLY.png

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJpYXQiOjE2OTg0NzQ0NDgsImV4cCI6MTY5ODQ3ODA0OH0.e9oMjkyJdWtUmIPIx3UqOHZwl76MRqurfzb5Txs3r1I

一樣拆解一下前兩個header與payload的部分
https://ithelp.ithome.com.tw/upload/images/20231029/201357507fywEmg6xN.png

注意的是,程式碼中有有效期限的部分
return jwt.sign(payload, this.secretKey, { expiresIn: '1h' });
簽出來的payload也就帶有一小時的有效期限

拿到了之後要怎麼測試呢?

如果是前端使用Axios的話,就可以輕鬆在header中夾帶JWT
如果身為後端要自己測試的話該怎麼辦呢?
https://ithelp.ithome.com.tw/upload/images/20231029/201357501lAAMu5nHT.png

要在請求中帶著JWT token來測試 http://localhost:3000/protected,你可以在請求的標頭(headers)中添加 Authorization 標頭,值為 Bearer <token>,其中 <token> 是你生成的JWT token。

例如,使用curl來模擬一個帶有JWT token的請求:

curl -X GET http://localhost:3000/protected -H "Authorization: Bearer <your_token>"

請確保將 <your_token> 替換為你實際生成的JWT token。

如果你使用的是Postman或其他API測試工具,可以在請求的標頭中手動添加 Authorization 標頭,並將值設為 Bearer <your_token>

這樣就可以在請求中攜帶JWT token進行測試了。如果有任何進一步的問題或需要更多的說明,請隨時告訴我!

OK這兩種我們都試看看

  1. 用curl :打開終端機,把JWT組合一下,變成這樣

    curl -X GET http://localhost:3000/protected -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTYiLCJpYXQiOjE2OTg0NzQ0NDgsImV4cCI6MTY5ODQ3ODA0OH0.e9oMjkyJdWtUmIPIx3UqOHZwl76MRqurfzb5Txs3r1I"
    

    https://ithelp.ithome.com.tw/upload/images/20231029/20135750HeuNbKrJtU.png

  2. 用postman看看:記得選到Auth的欄位,類型照著ChatGPT說的選 Bearer Token
    https://ithelp.ithome.com.tw/upload/images/20231029/20135750uu5ncjdmSo.png

透過這樣的方式設定Authorization,就可以正常的請求了!
啊.然後Postman我沒有規劃要做成一篇來介紹
但是下一篇會介紹其他更方便前後端測試的方式,就簡單說一下而已

evn 環境變數存取

再回顧一下 JwtService 的程式碼

  private readonly secretKey = 'mySecretKey'; // 這裡放置你的私鑰

這邊的話就是把你的私鑰放在程式碼中
這麼一來,如果把這段程式碼推上git等地方,你的密鑰就被知道了

就如同你能夠製作簽名&驗證,關鍵都是這個密鑰
被別人知道的話,別人就能偽造JWT傳送了

所以該怎麼辦才好呢?
https://ithelp.ithome.com.tw/upload/images/20231029/20135750tSir7vauBX.png

當你需要保護你的私鑰(例如在版本控制中不公開)時,你可以使用環境變數來存儲私鑰。

以下是一個使用環境變數來保護私鑰的方法:

步驟一:在專案中建立 .env 檔案

在專案的根目錄中,建立一個名為 .env 的檔案,並在裡面添加私鑰:

SECRET_KEY=mySecretKey

步驟二:使用 dotenv 套件

安裝 dotenv 套件來載入 .env 檔案中的環境變數:

npm install dotenv

在你的 main.ts(或 main.js) 檔案中加入以下程式碼:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as dotenv from 'dotenv';

async function bootstrap() {
  dotenv.config(); // 讀取 .env 檔案中的環境變數
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

步驟三:在 JwtService 中使用環境變數

JwtService 中,取得環境變數的私鑰:

import { Injectable } from '@nestjs/common';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class JwtService {
  private readonly secretKey = process.env.SECRET_KEY; // 使用環境變數中的私鑰

  generateToken(payload: any): string {
    return jwt.sign(payload, this.secretKey, { expiresIn: '1h' });
  }

  verifyToken(token: string): any {
    try {
      return jwt.verify(token, this.secretKey);
    } catch (error) {
      throw new Error('Token 驗證失敗');
    }
  }
}

步驟四:將 .env 加入 .gitignore

確保你的 .env 檔案被加入到你的 .gitignore 檔案中,這樣它就不會被提交到版本控制中:

.env

這樣你的私鑰就不會被公開在版本控制中,而是以環境變數的方式來存取。當你部署到不同的環境時,只需在該環境中設定相對應的環境變數即可。

照著做之後,設定成功!一樣可以跑、可以驗證和讀取
https://ithelp.ithome.com.tw/upload/images/20231029/20135750enBZQtErPa.png

實作完之後,我們再來了解一些和JWT相關的其他問題

其他問題:Bearer

https://ithelp.ithome.com.tw/upload/images/20231029/201357508fy63KHWs1.png

Bearer 是一種身份驗證機制中的一個認證方式,通常用於 HTTP 的標頭(headers)中,以識別傳送的資訊的類型。在 JWT 中,通常在使用 JWT token 進行身份驗證時,會將 token 放在 Authorization 標頭中,並以 Bearer 作為前綴。

這是一個範例 Authorization 標頭:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

這裡的 Bearer 表示這是一個使用 Bearer 認證方式的 token。

為什麼需要Bearer?

  1. 明確的指示身份驗證方式Bearer 告訴服務器,這是一個使用 Bearer token 身份驗證的請求。
  2. 區分不同的身份驗證方式:在 HTTP 標頭中可以有多種不同的身份驗證方式(例如 Basic、Digest、Bearer 等)。使用 Bearer 前綴可以清楚地指示正在使用的是 Bearer token。

總的來說,Bearer 是一種標準的方式,用於指示請求中的 token 是屬於 Bearer token 身份驗證方式。這樣伺服器就能夠正確地處理這個請求,並使用對應的驗證機制來進行身份驗證。

其他問題:不倚賴套件實作簽名與驗證邏輯

https://ithelp.ithome.com.tw/upload/images/20231029/20135750hkqcTfilrY.png

當然可以,你可以手動實作JWT的簽名和驗證機制,而不依賴 jsonwebtoken 套件。以下是一個簡單的範例,展示如何在Node.js中手動實作JWT的簽名和驗證。

簽名 (Sign):

const crypto = require('crypto');

const secret = 'mySecretKey';
const payload = {
  userId: '123456',
  username: 'john_doe'
};

const header = {
  alg: 'HS256', // HMACSHA256 簽名算法
  typ: 'JWT',
};

// 將標頭和有效載荷轉為Base64編碼
const base64Header = Buffer.from(JSON.stringify(header)).toString('base64');
const base64Payload = Buffer.from(JSON.stringify(payload)).toString('base64');

// 將標頭和有效載荷以.相連
const dataToSign = `${base64Header}.${base64Payload}`;

// 使用HMACSHA256算法簽名
const signature = crypto.createHmac('sha256', secret).update(dataToSign).digest('base64');

// 最後,將標頭、有效載荷和簽名組合成JWT
const token = `${base64Header}.${base64Payload}.${signature}`;

console.log('生成的Token:', token);

驗證 (Verify):

const crypto = require('crypto');

const secret = 'mySecretKey';

const token = '在這裡插入你的JWT';

const [header, payload, signature] = token.split('.');
const dataToSign = `${header}.${payload}`;

// 使用HMACSHA256算法驗證簽名
const expectedSignature = crypto.createHmac('sha256', secret).update(dataToSign).digest('base64');

if (expectedSignature === signature) {
  // 簽名驗證成功
  console.log('簽名驗證成功');
  const decodedPayload = JSON.parse(Buffer.from(payload, 'base64').toString());
  console.log('解碼後的資訊:', decodedPayload);
} else {
  // 簽名驗證失敗
  console.error('簽名驗證失敗');
}

這個範例演示了如何手動實作JWT的簽名和驗證,而不依賴 jsonwebtoken 套件。請確保在實際應用中將 secret 替換為你自己的私鑰,並適當地處理錯誤。

這邊就可以看到一開始繪製的驗證流程圖,確實就是拿token中的payload內容,再用自己的密鑰簽一次,然後確認看看是不是簽出來跟JWT token中的 signature 一樣

payload被動過,或者signature是用不同私鑰偽造的,驗證時簽出來的signature 比對就會失敗

其他身份驗證機制

好的,我們已經徹底了解JWT的運作流程與機制後,來了解一下其他驗證方式
https://ithelp.ithome.com.tw/upload/images/20231029/201357509SbCopeE0N.png

如果不使用JWT這種特定的驗證機制,你可以考慮以下幾種常見的身份驗證方式:

  1. 基本身份驗證(Basic Authentication)
    • 在每個請求的標頭中包含使用者名稱和密碼的Base64編碼。伺服器將解碼它們,並進行身份驗證。
    • 這種方式不是最安全的,因為Base64編碼可以相對容易地被解碼,所以通常搭配使用HTTPS。
  2. Digest 身份驗證(Digest Authentication)
    • 這是基於基本身份驗證的一種更安全的選擇。它使用一種加密的挑戰-回應方式,使攻擊者更難破解使用者的密碼。
  3. OAuth 2.0
    • OAuth 2.0 是一種開放標準,它允許第三方應用獲取受保護的資源(如用戶資料)的訪問權限。
    • 它支援多種流程,包括授權碼流程、隱式流程、密碼憑證流程等。
  4. OpenID Connect
    • OpenID Connect 是建立在OAuth 2.0之上的標準,用於身份驗證和授權。
    • 它允許用戶使用現有的帳戶(如Google、Facebook等)來登入其他網站,而不需要在這些網站上創建新的帳戶。
  5. Session-based 身份驗證
    • 通過在伺服器上保持關於用戶的狀態來實現,每當用戶登入時,伺服器會創建一個唯一的識別碼(通常是Session ID),並將其存儲在Cookie中。
    • 伺服器在後續請求中使用Session ID來識別用戶。

以上提到的是一些常見的身份驗證方式,每種方式都有其優缺點,選擇適合你的應用場景的方式非常重要。請注意,不同的場景可能需要不同的身份驗證方式,並且安全性應該是你考慮的首要因素。

JWT 優缺點

https://ithelp.ithome.com.tw/upload/images/20231029/20135750gqxYg3LPyi.png

JWT(JSON Web Tokens)相較於其他驗證方式具有一些優點和優勢:

優點:

  1. 輕量且可攜帶:JWT 是一個輕量且簡單的標準,可以輕易在網絡間傳輸。
  2. 跨平台和語言:由於 JWT 是基於JSON的開放標準,因此它在各種語言和平台上都有支持。
  3. 無需在伺服器上保留狀態:每個 JWT 都包含了足夠的資訊來驗證用戶的身份,因此伺服器無需在內部保留與用戶相關的狀態。
  4. 簽名驗證:JWT可以使用簽名來保護內容免受篡改,確保它來自可信的來源。
  5. 可以加密:除了簽名之外,JWT還可以使用加密機制,以保護內容不被第三方獲取。
  6. 可擴展:JWT的標頭允許添加自定義的屬性,這使得它可以被擴展以滿足特定需求。

缺點:

  1. 無法撤回:一旦生成了JWT,它的有效期開始運行,無法撤回或取消。
  2. 資料量較大:相對於一些其他驗證方式(如基本身份驗證),JWT可能會較大,尤其當內容較多時。
  3. 無法單獨撤銷單個Token:在傳統的基於Session的驗證中,伺服器可以單獨撤銷特定的Session,但對於JWT,伺服器無法單獨撤銷單個Token,除非更改密鑰或等待其過期。
  4. 不適合保管重要信息:儘管JWT可以簽名和加密,但它不適合用於保管敏感信息,因為這些信息可以被解碼。

總的來說,JWT提供了一種方便和靈活的方式來實現驗證,特別是在無狀態的API或分散式系統中。然而,它也有一些缺點,並且在特定情況下可能不適用。因此,在選擇驗證方式時,你應該根據你的應用場景和安全需求來進行選擇。

https://ithelp.ithome.com.tw/upload/images/20231029/20135750XaCco8iMK7.png

當使用JWT驗證機制時,有一些重要的事情需要特別注意:

  1. 保護私鑰
    • 確保你的私鑰保密且安全存放。不要將私鑰直接放在程式碼中或公開在版本控制系統中。
  2. 適當設置過期時間
    • 在生成JWT時,請為其設定一個適當的過期時間(exp)。這樣可以確保即使token被截取,也不會永遠有效。
  3. 不要在token中存儲敏感信息
    • 避免將敏感信息直接存儲在JWT中,即使它已經經過簽名或加密。最好只將非敏感的基本信息放在token中。
  4. 適當處理Refresh Token
    • 如果你使用Refresh Token來延長JWT的有效期,請確保這個機制是安全的,並定期檢查Refresh Token的有效性。
  5. 檢查簽名
    • 在驗證JWT時,確保簽名是有效的。只有當簽名驗證通過時,才能信任這個token。
  6. 保護免受中間人攻擊
    • 如果你使用HTTP而不是HTTPS,請注意可能會受到中間人攻擊。最好總是使用HTTPS。
  7. 適當處理過期token
    • 當JWT過期時,你需要適當地處理它。你可以選擇延長token的有效期(使用Refresh Token),或者要求用戶重新登入。
  8. 審查應用程式中的權限和許可權
    • 確保只有授予了相應權限的用戶可以訪問相應的資源。
  9. 記錄和監控
    • 監控你的應用程式,以偵測異常活動或異常的登入嘗試。設置適當的記錄以協助追蹤和調查問題。
  10. 避免重複使用token
    • 當你發現一個token被多次使用,這可能表示有人正在嘗試進行重放攻擊。確保每個token只能被使用一次。

總的來說,使用JWT時,請確保你瞭解並實施了相應的安全最佳實踐,以確保你的應用程式免受潛在的安全威脅。

統整重點

https://ithelp.ithome.com.tw/upload/images/20231029/20135750n09aP5ralp.png

綜合以上對話內容,以下是使用JWT(JSON Web Tokens)時的重點:

重點:

  1. JWT 簡介
    • JWT 是一種用於在網路上安全傳輸資訊的開放標準,通常用於驗證用戶的身份。
  2. JWT 結構
    • JWT 由三部分組成:標頭(Header)、有效載荷(Payload)和簽名(Signature)。
  3. 簽名驗證原理
    • JWT 通過將標頭和有效載荷進行簽名,來確保資訊的完整性和可信度。
  4. 私鑰保密
    • 確保私鑰保密且安全存放,避免直接放在程式碼或公開在版本控制系統中。
  5. 適當設置過期時間
    • 為JWT設定適當的過期時間,以保護資訊的有效性。
  6. 不要在token中存儲敏感信息
    • 避免將敏感信息直接存儲在JWT中,最好只放基本的非敏感信息。
  7. HTTPS 使用
    • 使用HTTPS 來保護免受中間人攻擊。
  8. 適當處理 Refresh Token
    • 如果使用Refresh Token來延長JWT的有效期,需確保安全,並定期檢查Refresh Token的有效性。
  9. 監控與記錄
    • 監控應用程式,記錄異常活動或異常的登入嘗試,協助解決問題。
  10. 避免重複使用token
    • 確保每個token只能被使用一次,避免重放攻擊。

結語:

使用JWT作為身份驗證機制是一種方便且靈活的方式,特別適用於無狀態的API或分散式系統。然而,它也有一些需要注意的地方,特別是在保護私鑰、適當設置過期時間等方面。請根據你的應用場景和安全需求來選擇合適的身份驗證方式。當適當地實施和監控時,JWT能夠提供一個有效的身份驗證解決方案。

結語

本次對話內容記錄在這邊:
https://chat.openai.com/share/6ea45d2e-1b24-4b13-b241-43c4ec8d00c8

對於REST API的實行上,JWT是無狀態的這一點非常適合
不需要在伺服端保留資料,而JWT機制整體運作起來難度也相當低
整篇看下來確實是簡潔又安全的方式

我自己工作上也是從Session-based 身份驗證,在近一年轉為JWT驗證的方式

不過,我想起好像多年前,曾經有一些混蛋在FB做了一個「心理測驗」的遊戲
大概是要你打開F12,輸入指令或是把cookie的內容複製下來貼給他
然後這個「心理測驗」就會告訴你「測試結果」
再來,這些參與遊戲的人,就會被盜了,發給親友病毒連結或個資外洩等等

想想這種手法真的很混蛋。

像JWT身份驗證的方式,通常也都是把Token放在cookie或是localStorage
像localStorage 的話,
使用 localStorage.setItem('token','JWT token') 就可以儲存token
接著使用localStorage.getItem('token') 就可以取得剛剛設定的token

只要拿到了Token,對伺服器來說,就是合格的使用者
因此就能不用輸入任何帳號密碼,直接使用這個token使用你的帳號

當時實際的運作邏輯和詐騙原理我並不是很清楚(那時候我只是一個孩子啊)
不過目前的Facebook打開F12的話,也會在console中顯示這樣的訊息
https://ithelp.ithome.com.tw/upload/images/20231029/20135750DVICwqPW7r.png
說得很好沒錯,住手,你們這些麻瓜 (X)

好的!以上就是有關JWT的介紹,以及實作在nestJS上的範例
下一篇!要來介紹方便前後端測試API的神套件: swagger
測試app就不用使用postman或是ChatGPT說得直接用curl 去發送請求

詳細就下一篇再介紹了

啊對了題外話,ChatGPT對我的機器人驗證,後來變得寬鬆了
一來是驗證的遊戲變簡單了(或是人類解起來比較簡單)
也沒有每一題都問我了!大概放置一段時間才問我一次
https://ithelp.ithome.com.tw/upload/images/20231029/20135750wIJUMx0qK0.png
開心開心,這個簡單跟快很多

就這樣!

我是喜歡使用JWT驗證的前端工程師一宵三筵,我們明天見~


上一篇
【Day45】ChatGPT請教教我:NestJS!(四)- Controller & Middleware!
下一篇
【Day47】ChatGPT請教教我:NestJS!(六)- API測試兼文件!swagger!
系列文
用ChatGPT詠唱來完成工作與點亮前後端技能樹56
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
CCCCC
iT邦新手 3 級 ‧ 2023-12-05 08:51:32

結語從nestjs變成nextjs了 哈

啊真的欸
感謝糾正typo
兩個拼字真的好像寫到錯亂去

我要留言

立即登入留言