Day 28:LIFF 會員註冊 - ID Token 驗證與資料庫整合
5.3k 字,約 5 分鐘
2025 iThome 鐵人賽:Line Bot × NestJS:30 天開發日記。 原文發表於 iThome,也可以在下方留言。
文章目錄
前言
今天的開發重點是建立會員卡系統,將 LIFF 前端與 NestJS 後端完整串接。
前端使用 Vue 建立註冊表單,搭配 vee-validator 驗證欄位的合法性。當用戶完成填寫並點擊註冊後,系統將 LIFF 取得的 ID Token 傳遞至後端。
後端接收 ID Token 後,結合 LIFF Channel ID 透過 LINE 驗證 API 進行身份驗證。驗證成功後解析 ID Token 中的用戶編號,並以此編號作為用戶資料表的主鍵,完成前後端串接的範例應用。所有資料庫操作都透過 Supabase 處理。明天將整合電子郵件驗證碼功能,強化註冊流程的安全性。
LIFF 前端會員卡流程
前端 LIFF 的部分主要拆分成三個頁面處理:
- 註冊頁面:使用 vee-validate 進行表單驗證,檢查欄位格式與必填項目。
- 驗證碼驗證頁面:透過電子郵件發送驗證碼,完成身份驗證。
- 完成註冊頁面:顯示註冊成功的會員資訊。
LINE 提供兩種主要的用戶資料驗證方式:
- 使用 ID Token 取得用戶資料
- 使用 Access Token 取得用戶資料
本範例使用的是第一種的方式,我們會透過 LIFF 將 IDtoken 傳遞至後端,流程如下:
- 前端透過 LIFF SDK 取得 ID Token
- 傳送 ID Token 至後端
- 後端向 LINE 平台驗證 ID Token
- 驗證成功後取得用戶資料
- 後端處理並回傳結果
Supabase 專案創建
Supabase 官方網站
Supabase 是一個開源的後端即服務(BaaS)平台,提供開發者快速建立應用程式所需的後端基礎設施。它整合了 PostgreSQL 資料庫、身份驗證、即時訂閱、儲存空間等功能,讓開發者無需從零搭建後端環境,即可專注於應用邏輯的開發。
為了讓開發流程更清晰,本專案後端採用 Supabase 的 PostgreSQL 服務。資料庫的建立、管理與操作都透過 Supabase 提供的管理介面和工具完成,簡化配置與維護的複雜度。
【 Free plan 】
- 最多 2 個活躍專案,專案一週未使用將自動暫停
- 每個專案提供 500 MB 資料庫空間,以及 1 GB 檔案儲存。
- 每月活躍使用者(MAU):上限 50,000 人
Step 1:創建專案
Database password的部分設定後要記得
Step 2:點選左側導覽列中的 Database
Step 3:進入 Database 介面後,點選 Create a new table 創建資料表
Step 4:創建兩張資料表,分別紀錄驗證碼及用戶資訊
id 使用 LINE User ID 作為唯一值,後端解析 ID Token 後取得。
Step 5:取得 Supabase 請求端點網址及服務金鑰
進入 Supabase 專案的 Settings > API 頁面,複製以下兩項資訊並設定於後端環境變數中:
1. Project URL(API 端點網址)
2. service_role key(服務金鑰)
Nest 後端伺服器
前置作業
安裝必要套件
- class-validator & class-transformer:用於請求 DTO 的驗證與資料轉換
- @supabase/supabase-js:Supabase 官方 JavaScript 客戶端函式庫
pnpm i class-validator
pnpm i class-transformer
pnpm i @supabase/supabase-js
環境變數(env)
- LINE_LOGIN_VERIFY_URL:LINE 官方 ID Token 驗證 API 端點
- LINE_LOGIN_CLIENT_ID:LINE Login 頻道 ID(非 Messaging API 頻道喔!!)
- SUPABASE_URL:Supabase 專案連線位址
- SUPABASE_SERVICE_ROLE_KEY:Supabase 服務端管理權杖
config 啟動驗證
/**
* 驗證模式
*/
const configSchema = Joi.object({
line: Joi.object({
loginChannelId: Joi.string().required(),
loginVerifyUrl: Joi.string().uri().required(),
}).required(),
supabase: Joi.object({
url: Joi.string().uri().required(),
serviceRoleKey: Joi.string().required(),
}).required(),
});
/**
* 驗證模組
*/
export default () => {
const config = {
line: {
loginChannelId: process.env.LINE_LOGIN_CLIENT_ID,
loginVerifyUrl: process.env.LINE_LOGIN_VERIFY_URL,
},
supabase: {
url: process.env.SUPABASE_URL,
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
},
};
const { error, value } = configSchema.validate(config, {
abortEarly: false,
});
if (error) throw new Error(`環境變數驗證錯誤: ${error.message}`);
return value;
};
LINE Login ID Token 驗證服務建立
請求注意事項:
- Content-Type:application/x-www-form-urlencoded(以表單格式發送請求資料)
- idToken:需由前端透過 LIFF SDK 的
liff.getIdToken()方法取得 JWT 格式的 Token,傳遞至後端後,透過 LINE 驗證 API 解析取得用戶資訊
ID Token 解析後主要欄位:
- sub:用戶的 LINE User ID(唯一識別碼)
- name:用戶的顯示名稱
- picture:用戶的頭像圖片網址
- aud:LINE Login Channel ID
- exp:Token 過期時間
// 略
@Injectable()
export class LineLoginService {
// 略
async verifyIDToken(idToken: string): Promise<TokenVerifyResponse> {
const formData = new URLSearchParams();
formData.append('id_token', idToken);
formData.append(
'client_id',
this.configService.getOrThrow<string>('line.channelId'),
);
const responseData = await firstValueFrom(
this.httpService
.post(this.LINE_LOGIN_VERIFY_URL, formData.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.pipe(
catchError((err: AxiosError) => {
return throwError(
() =>
new Error(
`LINE token verify API request failed: ${JSON.stringify(err.response?.data)}`,
),
);
}),
),
);
return responseData.data;
}
}
Supabase 資料庫服務建立
建立 Supabase 連線物件,供其他服務注入使用。為簡化操作,資料表未使用 RLS 機制,直接使用 Service Role Key 以管理者權限操作。
// 略
import { createClient, SupabaseClient } from '@supabase/supabase-js';
@Injectable()
export class SupabaseService {
private readonly client: SupabaseClient;
constructor(private readonly config: ConfigService) {
const url = this.config.getOrThrow<string>('supabase.url');
const key = this.config.getOrThrow<string>('supabase.serviceRoleKey');
this.client = createClient(url, key, { auth: { persistSession: false } });
}
get db() {
return this.client;
}
}
LINE 使用者註冊服務建立
負責處理用戶相關的 API 邏輯,包含用戶註冊功能。前端透過 LIFF 以 Ajax 方式向後端發送請求。
建立 DTO 資料傳輸物件
使用 class-validator 動態驗證請求及回覆資料,確保符合規定格式。
請求參數說明
- idToken:前端透過 LIFF SDK 的 liff.getIdToken() 取得
- name、phone、birthday、email:註冊表單填寫的用戶資料
user/dto/register-user.dto.ts(請求資料)
import {
IsEmail,
IsNotEmpty,
IsString,
Matches,
MinLength,
MaxLength,
} from 'class-validator';
export class RegisterUserDto {
@IsString()
@IsNotEmpty({ message: 'LINE ID Token 為必填項目' })
idToken: string;
@IsString()
@IsNotEmpty({ message: '姓名為必填項目' })
@MinLength(2, { message: '姓名至少需要 2 個字元' })
@MaxLength(20, { message: '姓名不能超過 20 個字元' })
name: string;
@IsString()
@IsNotEmpty({ message: '電話為必填項目' })
@Matches(/^09\d{8}$/, {
message: '請輸入正確的台灣手機號碼格式 (09xxxxxxxx)',
})
phone: string;
@IsString()
@IsNotEmpty({ message: '生日為必填項目' })
birthday: string;
@IsEmail({}, { message: '請輸入正確的電子信箱格式' })
@IsNotEmpty({ message: '電子信箱為必填項目' })
email: string;
}
在 Controller 透過 DTO 進行請求驗證與資料轉換
使用 ValidationPipe 搭配 DTO 自動驗證請求資料格式。
// 略
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('register')
@UsePipes(new ValidationPipe({ transform: true }))
async register(
@Body() registerUserDto: RegisterUserDto,
): Promise<UserResponseDto> {
return this.userService.register(registerUserDto);
}
}
建立用戶註冊 API 服務
line-login/line-login.service.ts
// 略
@Injectable()
export class LineLoginService {
// 略
async verifyIDToken(idToken: string): Promise<TokenVerifyResponse> {
const formData = new URLSearchParams();
formData.append('id_token', idToken);
formData.append('client_id', this.LINE_LOGIN_CLIENT_ID);
const responseData = await firstValueFrom(
this.httpService
.post(this.LINE_LOGIN_VERIFY_URL, formData.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.pipe(
catchError((err: AxiosError) => {
return throwError(
() =>
new Error(
`LINE token verify API request failed: ${JSON.stringify(err.response?.data)}`,
),
);
}),
),
);
return responseData.data;
}
}
本日結語
最後兩天打算完成一個 LIFF 串到後端資料庫的範例,了解後端解析 ID Token 的流程,並透過實作展示 LIFF 與後端的完整互動過程。
留言
留言使用 GitHub 帳號登入(Giscus),留言會存在 GitHub Discussions。