iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0

前言

昨天我們完成了 Google Cloud Console 的設定,今天我們會用 Passport.js 這個身份驗證套件來實作 Google 登入。

什麼是 Passport.js?

Passport.js 是 Node.js 中最流行的身份驗證中介軟體,它的核心概念是:

  • 策略(Strategy)模式:每種登入方式(Google、Facebook、本地登入等)都是一個獨立的策略
  • 可插拔架構:需要什麼登入方式就安裝對應的套件
  • 統一介面:不管用哪種登入方式,使用方式都一致

為什麼要用 Passport?

  1. 省時間:不用自己實作複雜的 OAuth 流程
  2. 多種策略:支援 500+ 種登入方式(Google、Facebook、GitHub、Line...)
  3. 社群活躍:遇到問題很容易找到解決方案
  4. 彈性高:可以自訂驗證邏輯

OAuth 流程圖

sequenceDiagram
    participant U as 用戶
    participant F as 前端
    participant B as Tickeasy 後端
    participant G as Google OAuth
    U->>F: 點擊 "Google 登入"
    F->>B: GET /api/v1/auth/google
    B->>G: 重定向到 Google 授權頁面
    G->>U: 顯示授權頁面
    U->>G: 授權 Tickeasy 存取
    G->>B: 回調 /api/v1/auth/google/callback + code
    B->>G: 用 code 交換 access_token
    G->>B: 回傳 access_token + 用戶資料
    B->>B: 處理用戶資料 (創建/更新用戶)
    B->>B: 生成 JWT token
    B->>F: 重定向到前端 + JWT token
    F->>U: 登入成功

安裝套件

npm install passport passport-google-oauth20 jsonwebtoken
npm install --save-dev @types/passport @types/passport-google-oauth20 @types/jsonwebtoken

環境變數

# Google OAuth
GOOGLE_CLIENT_ID=你的用戶端編號
GOOGLE_CLIENT_SECRET=你的用戶端密碼
GOOGLE_CALLBACK_URL=http://localhost:3000/api/v1/auth/google/callback

# JWT
JWT_SECRET=your-secret-key
JWT_EXPIRES_IN=7d

# 前端網址
FRONTEND_URL=http://localhost:3001

實作

1. User Entity

// src/models/user.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @Column({ nullable: true, unique: true })
  googleId: string;

  @Column({ nullable: true })
  picture: string;

  @Column({ default: 'google' })
  provider: string;
}

2. JWT 工具

// src/utils/jwt.util.ts
import jwt from 'jsonwebtoken';

interface JwtPayload {
  id: string;
  email: string;
}

export class JwtUtil {
  static generateToken(payload: JwtPayload): string {
    return jwt.sign(payload, process.env.JWT_SECRET!, {
      expiresIn: process.env.JWT_EXPIRES_IN || '7d',
    });
  }

  static verifyToken(token: string): JwtPayload {
    return jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
  }
}

3. Passport 設定

// src/config/passport.ts
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { AppDataSource } from './database';
import { User } from '../models/user';

passport.use(
  new GoogleStrategy(
    {
      clientID: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      callbackURL: process.env.GOOGLE_CALLBACK_URL!,
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        const userRepository = AppDataSource.getRepository(User);
        const email = profile.emails?.[0]?.value;

        // 尋找或創建使用者
        let user = await userRepository.findOne({
          where: { googleId: profile.id },
        });

        if (!user && email) {
          user = userRepository.create({
            googleId: profile.id,
            email,
            name: profile.displayName,
            picture: profile.photos?.[0]?.value,
          });
          await userRepository.save(user);
        }

        done(null, user);
      } catch (error) {
        done(error as Error);
      }
    }
  )
);

export default passport;

重點說明

  • Strategy 的第一個參數是設定(Client ID、Secret、Callback URL)
  • 第二個參數是驗證函式,收到 Google 回傳的使用者資料後的處理邏輯
  • done(null, user) 告訴 Passport 驗證成功,並傳遞使用者資料

4. Controller

// src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import { JwtUtil } from '../utils/jwt.util';

export class AuthController {
  static googleCallback(req: Request, res: Response): void {
    if (!req.user) {
      return res.redirect(`${process.env.FRONTEND_URL}/login?error=auth_failed`);
    }

    // 生成 JWT
    const token = JwtUtil.generateToken({
      id: req.user.id,
      email: req.user.email,
    });

    // 帶著 token 導回前端
    res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
  }
}

5. Routes

// src/routes/auth.route.ts
import { Router } from 'express';
import passport from '../config/passport';
import { AuthController } from '../controllers/auth.controller';

const router = Router();

// 開始 Google 登入
router.get(
  '/google',
  passport.authenticate('google', {
    scope: ['profile', 'email'],
    session: false, // 使用 JWT,不需要 session
  })
);

// Google 回調
router.get(
  '/google/callback',
  passport.authenticate('google', { session: false }),
  AuthController.googleCallback
);

export default router;

重點說明

  • /google 路由會自動導向 Google 授權頁面
  • session: false 因為我們用 JWT,不需要 Session
  • scope 指定要取得的權限(個人資料、Email)

6. 主程式

// src/index.ts
import express from 'express';
import cors from 'cors';
import passport from './config/passport';
import authRoutes from './routes/auth.route';
import { AppDataSource } from './config/database';

const app = express();

app.use(express.json());
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
app.use(passport.initialize());

app.use('/api/v1/auth', authRoutes);

const startServer = async () => {
  await AppDataSource.initialize();
  app.listen(3000, () => {
    console.log('🚀 Server running on http://localhost:3000');
    console.log('📝 Google Login: http://localhost:3000/api/v1/auth/google');
  });
};

startServer();

使用流程

  1. 使用者訪問 http://localhost:3000/api/v1/auth/google
  2. Passport 導向 Google 授權頁面
  3. 使用者授權後,Google 導回 /api/v1/auth/google/callback
  4. Passport 自動處理 OAuth 流程,取得使用者資料
  5. 執行我們定義的驗證函式(創建或更新使用者)
  6. Controller 生成 JWT Token
  7. 導回前端並帶上 Token

Passport 核心概念

1. Strategy(策略)

每種登入方式都是一個 Strategy:

// Google Strategy
passport.use(new GoogleStrategy({...}, verifyCallback));

// Facebook Strategy
passport.use(new FacebookStrategy({...}, verifyCallback));

// 本地 Strategy(帳密登入)
passport.use(new LocalStrategy({...}, verifyCallback));

2. Verify Callback(驗證回調)

這是你定義的邏輯,決定如何處理第三方回傳的資料:

async (accessToken, refreshToken, profile, done) => {
  // 1. 從 profile 取得使用者資料
  // 2. 在資料庫中尋找或創建使用者
  // 3. 呼叫 done(null, user) 完成驗證
}

3. 認證流程

// 開始認證
passport.authenticate('google', { scope: [...] })

// Passport 會:
// 1. 導向 Google 授權頁面
// 2. 使用者授權後接收 callback
// 3. 用 code 交換 access_token
// 4. 取得使用者資料
// 5. 執行你的 verify callback
// 6. 將使用者資料掛到 req.user

上一篇
Day22 - Google 第三方登入
下一篇
Day24 - 信箱驗證
系列文
欸欸!! 這是我的學習筆記26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言