iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
Build on AWS

30 天將工作室 SaaS 產品部署起來系列 第 12

Day 12:30天部署SaaS產品到AWS-多元認證架構:LINE Login + AWS Cognito 整合實作

  • 分享至 

  • xImage
  •  

前情提要

經過 Day 11 的多租戶資料庫架構建立,我們已經為每家健身房建立了獨立的資料庫環境。今天我們要實作多元認證系統,支援LINE Login 整合,同時保持 Email/Password 和 Phone OTP 等傳統認證方式的彈性選擇。

認證選擇分析

健身房用戶特性

// 用戶角色與認證偏好分析
interface UserAuthPreference {
  role: 'member' | 'trainer' | 'staff' | 'manager' | 'owner';
  primaryMethod: 'line' | 'phone' | 'email';
  securityLevel: 'basic' | 'medium' | 'high';
}

const authPreferences: UserAuthPreference[] = [
  // 學員:便利性優先,LINE 使用率最高
  { role: 'member', primaryMethod: 'line', securityLevel: 'basic' },

  // 教練:LINE + Phone ,靈活度高
  { role: 'trainer', primaryMethod: 'line', securityLevel: 'medium' },

  // 櫃檯人員:Phone OTP,快速上手
  { role: 'staff', primaryMethod: 'phone', securityLevel: 'medium' },

  // 管理者:Email + OTP 雙因子,安全性優先
  { role: 'manager', primaryMethod: 'email', securityLevel: 'high' },

  // 健身房老闆:最高安全等級
  { role: 'owner', primaryMethod: 'email', securityLevel: 'high' }
];

認證架構設計原則

  1. 彈性優先: 不同健身房可選擇不同認證組合
  2. 安全分級: 管理員 vs 一般用戶不同安全要求
  3. 本土化: LINE Login 深度整合
  4. 向下兼容: 支援傳統 Email/Password

🏗️ AWS Cognito + LINE Login 整合架構

整體認證流程圖

                    健身房 SaaS 認證架構

    📱 LINE LIFF        💻 Web/App         📞 SMS OTP
         │                   │                  │
         ├─── LINE Login ────┤                  │
         │                   │                  │
         ▼                   ▼                  ▼
    ┌─────────────────────────────────────────────────┐
    │           AWS API Gateway + Lambda              │
    │              (認證協調服務)                      │
    └─────────────┬───────────────┬───────────────────┘
                  │               │
                  ▼               ▼
    ┌─────────────────────┐  ┌──────────────────┐
    │   AWS Cognito       │  │   LINE Platform  │
    │  User Pool          │  │   Provider       │
    │  (統一身份管理)       │  │   (第三方認證)    │
    └─────────────────────┘  └──────────────────┘
                  │
                  ▼
    ┌─────────────────────────────────────────────────┐
    │              Tenant Database                    │
    │        (每家健身房獨立用戶資料)                   │
    └─────────────────────────────────────────────────┘

🔑 AWS Cognito 設定與 LINE Provider 整合

1. Cognito User Pool 建立

// infra/cdk/lib/auth-stack.ts
import { UserPool, UserPoolClient, UserPoolIdentityProviderOidc } from 'aws-cdk-lib/aws-cognito';

export class AuthStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // 主要 User Pool - 支援多種認證方式
    const userPool = new UserPool(this, 'KyoSystemUserPool', {
      userPoolName: 'kyo-system-users',

      // 彈性登入選項
      signInAliases: {
        email: true,
        phone: true,
        username: false  // 使用 email/phone 作為主要識別
      },

      // 密碼政策 - 管理員更嚴格
      passwordPolicy: {
        minLength: 8,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: false  // 降低用戶負擔
      },

      // MFA 設定 - 管理員必須開啟
      mfa: MfaSecondFactor.OPTIONAL,
      mfaSecondFactor: {
        sms: true,
        otp: true
      },

      // 自定義屬性 - 租戶隔離
      standardAttributes: {
        email: { required: false, mutable: true },
        phoneNumber: { required: false, mutable: true },
        givenName: { required: false, mutable: true },
        familyName: { required: false, mutable: true }
      },

      customAttributes: {
        // 租戶 ID - 關鍵的隔離機制
        tenant_id: new StringAttribute({ minLen: 1, maxLen: 50, mutable: false }),

        // LINE 用戶 ID
        line_user_id: new StringAttribute({ minLen: 1, maxLen: 100, mutable: true }),

        // 用戶角色
        role: new StringAttribute({ minLen: 1, maxLen: 20, mutable: true }),

        // 健身房內部會員編號
        member_code: new StringAttribute({ minLen: 1, maxLen: 20, mutable: true })
      }
    });

    // LINE Login Provider 設定
    const lineProvider = new UserPoolIdentityProviderOidc(this, 'LineProvider', {
      userPool,
      name: 'LINE',
      clientId: process.env.LINE_CHANNEL_ID!,
      clientSecret: process.env.LINE_CHANNEL_SECRET!,

      // LINE Login 端點
      issuerUrl: 'https://api.line.me',

      // OAuth 2.0 設定
      scopes: ['openid', 'profile', 'email'],

      // 屬性對應 - LINE 用戶資料 -> Cognito
      attributeMapping: {
        email: ProviderAttribute.other('email'),
        givenName: ProviderAttribute.other('name'),
        custom: {
          line_user_id: ProviderAttribute.other('sub')  // LINE User ID
        }
      }
    });

    // App Client 設定 - 支援多種認證流程
    const userPoolClient = new UserPoolClient(this, 'KyoSystemClient', {
      userPool,

      // 認證流程
      authFlows: {
        userPassword: true,      // Email/Password
        userSrp: true,          // SRP 安全認證
        custom: true,           // 自定義認證 (Phone OTP)
        adminUserPassword: true  // 管理員認證
      },

      // OAuth 設定 - LINE Login 需要
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
          implicitCodeGrant: false  // 安全考量
        },

        // 支援的 Provider
        supportedIdentityProviders: [
          UserPoolClientIdentityProvider.COGNITO,
          UserPoolClientIdentityProvider.custom('LINE')
        ],

        // 回調 URL - 多環境支援
        callbackUrls: [
          'https://kyo-system.com/auth/callback',           // 生產環境
          'https://staging.kyo-system.com/auth/callback',   // 測試環境
          'http://localhost:3000/auth/callback',            // 開發環境
          'https://liff.line.me/callback'                   // LINE LIFF
        ],

        logoutUrls: [
          'https://kyo-system.com/auth/logout',
          'https://staging.kyo-system.com/auth/logout',
          'http://localhost:3000/auth/logout'
        ]
      },

      // Token 設定
      accessTokenValidity: Duration.hours(1),      // 短期 Access Token
      refreshTokenValidity: Duration.days(30),     // 長期 Refresh Token
      idTokenValidity: Duration.hours(1)
    });

    // User Pool Domain - 託管認證頁面
    const userPoolDomain = new UserPoolDomain(this, 'KyoSystemDomain', {
      userPool,
      cognitoDomain: {
        domainPrefix: 'kyo-system-auth'  // https://kyo-system-auth.auth.ap-northeast-1.amazoncognito.com
      }
    });
  }
}

📱 LINE Login 整合實作

1. LINE LIFF 應用程式設定

// packages/kyo-core/src/line/liff-service.ts
export class LiffService {
  private liffId: string;
  private channelId: string;

  constructor() {
    this.liffId = process.env.LINE_LIFF_ID!;
    this.channelId = process.env.LINE_CHANNEL_ID!;
  }

  // LIFF 初始化
  async initLiff(): Promise<boolean> {
    try {
      await liff.init({ liffId: this.liffId });

      if (liff.isLoggedIn()) {
        return true;
      }

      return false;
    } catch (error) {
      console.error('LIFF init failed:', error);
      return false;
    }
  }

  // LINE Login 執行
  async lineLogin(): Promise<LineLoginResult> {
    try {
      if (!liff.isLoggedIn()) {
        // 觸發 LINE Login 流程
        liff.login({
          redirectUri: window.location.href
        });
        return { success: false, redirecting: true };
      }

      // 取得 LINE 用戶資料
      const profile = await liff.getProfile();
      const idToken = liff.getIDToken();

      return {
        success: true,
        profile: {
          userId: profile.userId,
          displayName: profile.displayName,
          pictureUrl: profile.pictureUrl,
          statusMessage: profile.statusMessage
        },
        idToken
      };
    } catch (error) {
      console.error('LINE login failed:', error);
      return { success: false, error: error.message };
    }
  }

  // LINE 用戶資料同步到 Cognito
  async syncToCognito(lineProfile: LineProfile, tenantId: string): Promise<CognitoSyncResult> {
    try {
      const authService = new AuthService();

      // 檢查用戶是否已存在
      const existingUser = await authService.getUserByLineId(lineProfile.userId, tenantId);

      if (existingUser) {
        // 更新現有用戶資料
        return await authService.updateUserFromLine(existingUser.username, lineProfile);
      } else {
        // 建立新用戶
        return await authService.createUserFromLine(lineProfile, tenantId);
      }
    } catch (error) {
      console.error('Sync to Cognito failed:', error);
      throw error;
    }
  }
}

interface LineLoginResult {
  success: boolean;
  redirecting?: boolean;
  profile?: LineProfile;
  idToken?: string;
  error?: string;
}

interface LineProfile {
  userId: string;
  displayName: string;
  pictureUrl?: string;
  statusMessage?: string;
}

2. Cognito + LINE 認證協調服務

// apps/kyo-otp-service/src/auth/cognito-line-service.ts
import { CognitoIdentityProviderClient, AdminCreateUserCommand, AdminSetUserPasswordCommand } from '@aws-sdk/client-cognito-identity-provider';

export class CognitoLineService {
  private cognitoClient: CognitoIdentityProviderClient;
  private userPoolId: string;

  constructor() {
    this.cognitoClient = new CognitoIdentityProviderClient({
      region: process.env.AWS_REGION || 'ap-northeast-1'
    });
    this.userPoolId = process.env.COGNITO_USER_POOL_ID!;
  }

  // 從 LINE Profile 建立 Cognito 用戶
  async createUserFromLine(lineProfile: LineProfile, tenantId: string, role: string = 'member'): Promise<CognitoUser> {
    try {
      // 生成唯一的用戶名 (LINE User ID + Tenant ID)
      const username = `line_${lineProfile.userId}_${tenantId}`;

      const createUserCommand = new AdminCreateUserCommand({
        UserPoolId: this.userPoolId,
        Username: username,
        UserAttributes: [
          { Name: 'given_name', Value: lineProfile.displayName },
          { Name: 'custom:tenant_id', Value: tenantId },
          { Name: 'custom:line_user_id', Value: lineProfile.userId },
          { Name: 'custom:role', Value: role },
          { Name: 'email_verified', Value: 'false' },
          { Name: 'phone_number_verified', Value: 'false' }
        ],
        MessageAction: 'SUPPRESS',  // 不發送歡迎郵件
        TemporaryPassword: this.generateSecurePassword()
      });

      const result = await this.cognitoClient.send(createUserCommand);

      // 設定永久密碼 (用戶不會直接使用,但 Cognito 要求)
      await this.setUserPermanentPassword(username);

      // 同步到租戶資料庫
      await this.syncUserToTenantDatabase({
        cognitoUsername: username,
        lineUserId: lineProfile.userId,
        displayName: lineProfile.displayName,
        pictureUrl: lineProfile.pictureUrl,
        tenantId,
        role
      });

      return {
        username,
        cognitoUserId: result.User?.Username!,
        tenantId,
        role
      };
    } catch (error) {
      console.error('Create user from LINE failed:', error);
      throw error;
    }
  }

  // LINE Token 驗證與 Cognito JWT 交換
  async exchangeLineTokenForCognito(idToken: string, tenantId: string): Promise<CognitoTokens> {
    try {
      // 驗證 LINE ID Token
      const lineProfile = await this.verifyLineIdToken(idToken);

      // 檢查或建立 Cognito 用戶
      let cognitoUser = await this.findCognitoUserByLine(lineProfile.userId, tenantId);

      if (!cognitoUser) {
        cognitoUser = await this.createUserFromLine(lineProfile, tenantId);
      }

      // 產生 Cognito JWT Token
      const tokens = await this.generateCognitoTokens(cognitoUser.username);

      return tokens;
    } catch (error) {
      console.error('Exchange LINE token failed:', error);
      throw error;
    }
  }

  // 多租戶用戶查詢 - 安全隔離
  private async findCognitoUserByLine(lineUserId: string, tenantId: string): Promise<CognitoUser | null> {
    try {
      const listUsersCommand = new ListUsersCommand({
        UserPoolId: this.userPoolId,
        Filter: `custom:line_user_id = "${lineUserId}" AND custom:tenant_id = "${tenantId}"`
      });

      const result = await this.cognitoClient.send(listUsersCommand);

      if (result.Users && result.Users.length > 0) {
        const user = result.Users[0];
        return {
          username: user.Username!,
          cognitoUserId: user.Username!,
          tenantId,
          role: this.getAttribute(user.Attributes, 'custom:role') || 'member'
        };
      }

      return null;
    } catch (error) {
      console.error('Find user by LINE ID failed:', error);
      return null;
    }
  }

  // 同步用戶資料到租戶資料庫
  private async syncUserToTenantDatabase(userData: UserSyncData): Promise<void> {
    const tenantDbService = new TenantDatabaseService();
    const tenantConnection = await tenantDbService.getTenantConnection(userData.tenantId);

    try {
      // 插入或更新用戶資料
      await tenantConnection.query(`
        INSERT INTO users (
          cognito_username, line_user_id, display_name,
          picture_url, role, created_at, updated_at
        ) VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
        ON CONFLICT (cognito_username)
        DO UPDATE SET
          display_name = EXCLUDED.display_name,
          picture_url = EXCLUDED.picture_url,
          updated_at = NOW()
      `, [
        userData.cognitoUsername,
        userData.lineUserId,
        userData.displayName,
        userData.pictureUrl,
        userData.role
      ]);
    } finally {
      await tenantConnection.end();
    }
  }

  private generateSecurePassword(): string {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%';
    let password = '';
    for (let i = 0; i < 12; i++) {
      password += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return password;
  }
}

interface UserSyncData {
  cognitoUsername: string;
  lineUserId: string;
  displayName: string;
  pictureUrl?: string;
  tenantId: string;
  role: string;
}

🔐 Phone OTP 認證整合

Cognito Custom Authentication Flow

// apps/kyo-otp-service/src/auth/custom-auth-flow.ts
export class CustomAuthFlow {
  private cognitoClient: CognitoIdentityProviderClient;
  private otpService: IOtpService;

  constructor() {
    this.cognitoClient = new CognitoIdentityProviderClient({
      region: process.env.AWS_REGION || 'ap-northeast-1'
    });
    this.otpService = createOtpServiceFromEnv();
  }

  // 發起 Phone OTP 認證
  async initiatePhoneAuth(phoneNumber: string, tenantId: string): Promise<AuthChallenge> {
    try {
      // 檢查用戶是否存在
      const username = `phone_${phoneNumber.replace('+', '')}_${tenantId}`;

      const initiateAuthCommand = new InitiateAuthCommand({
        ClientId: process.env.COGNITO_CLIENT_ID!,
        AuthFlow: 'CUSTOM_AUTH',
        AuthParameters: {
          USERNAME: username,
          CHALLENGE_NAME: 'CUSTOM_CHALLENGE'
        }
      });

      const result = await this.cognitoClient.send(initiateAuthCommand);

      if (result.ChallengeName === 'CUSTOM_CHALLENGE') {
        // 發送 OTP 簡訊
        const otpResult = await this.otpService.send({
          phone: phoneNumber,
          template: 'auth_login',
          params: {
            gym_name: await this.getTenantName(tenantId)
          }
        });

        return {
          challengeName: 'CUSTOM_CHALLENGE',
          session: result.Session!,
          challengeParameters: {
            PHONE_NUMBER: phoneNumber,
            TENANT_ID: tenantId
          }
        };
      }

      throw new Error('Unexpected challenge type');
    } catch (error) {
      // 如果用戶不存在,先建立用戶再重試
      if (error.name === 'UserNotFoundException') {
        await this.createPhoneUser(phoneNumber, tenantId);
        return await this.initiatePhoneAuth(phoneNumber, tenantId);
      }

      throw error;
    }
  }

  // 驗證 OTP 並完成認證
  async respondToAuthChallenge(session: string, otpCode: string, phoneNumber: string): Promise<AuthResult> {
    try {
      const respondCommand = new RespondToAuthChallengeCommand({
        ClientId: process.env.COGNITO_CLIENT_ID!,
        ChallengeName: 'CUSTOM_CHALLENGE',
        Session: session,
        ChallengeResponses: {
          USERNAME: `phone_${phoneNumber.replace('+', '')}_${tenantId}`,
          ANSWER: otpCode
        }
      });

      const result = await this.cognitoClient.send(respondCommand);

      if (result.AuthenticationResult) {
        return {
          accessToken: result.AuthenticationResult.AccessToken!,
          refreshToken: result.AuthenticationResult.RefreshToken!,
          idToken: result.AuthenticationResult.IdToken!,
          expiresIn: result.AuthenticationResult.ExpiresIn!
        };
      }

      throw new Error('Authentication failed');
    } catch (error) {
      console.error('OTP verification failed:', error);
      throw error;
    }
  }

  // 建立手機號碼用戶
  private async createPhoneUser(phoneNumber: string, tenantId: string): Promise<void> {
    const username = `phone_${phoneNumber.replace('+', '')}_${tenantId}`;

    const createUserCommand = new AdminCreateUserCommand({
      UserPoolId: process.env.COGNITO_USER_POOL_ID!,
      Username: username,
      UserAttributes: [
        { Name: 'phone_number', Value: phoneNumber },
        { Name: 'custom:tenant_id', Value: tenantId },
        { Name: 'custom:role', Value: 'member' },
        { Name: 'phone_number_verified', Value: 'true' }
      ],
      MessageAction: 'SUPPRESS'
    });

    await this.cognitoClient.send(createUserCommand);
  }
}

認證統一 API 端點

Fastify API 認證路由整合

// apps/kyo-otp-service/src/routes/auth.ts
import { FastifyInstance } from 'fastify';

export async function authRoutes(fastify: FastifyInstance) {
  const liffService = new LiffService();
  const cognitoLineService = new CognitoLineService();
  const customAuthFlow = new CustomAuthFlow();

  // LINE Login 認證端點
  fastify.post('/auth/line/login', async (request, reply) => {
    const { idToken, tenantId } = request.body as { idToken: string; tenantId: string };

    try {
      const tokens = await cognitoLineService.exchangeLineTokenForCognito(idToken, tenantId);

      return reply.code(200).send({
        success: true,
        tokens,
        provider: 'line'
      });
    } catch (error) {
      return reply.code(401).send({
        success: false,
        error: 'LINE authentication failed',
        message: error.message
      });
    }
  });

  // Phone OTP 認證發起
  fastify.post('/auth/phone/initiate', async (request, reply) => {
    const { phoneNumber, tenantId } = request.body as { phoneNumber: string; tenantId: string };

    try {
      const challenge = await customAuthFlow.initiatePhoneAuth(phoneNumber, tenantId);

      return reply.code(200).send({
        success: true,
        challengeName: challenge.challengeName,
        session: challenge.session
      });
    } catch (error) {
      return reply.code(400).send({
        success: false,
        error: 'Phone authentication initiation failed',
        message: error.message
      });
    }
  });

  // Phone OTP 認證驗證
  fastify.post('/auth/phone/verify', async (request, reply) => {
    const { session, otpCode, phoneNumber } = request.body as {
      session: string;
      otpCode: string;
      phoneNumber: string;
    };

    try {
      const authResult = await customAuthFlow.respondToAuthChallenge(session, otpCode, phoneNumber);

      return reply.code(200).send({
        success: true,
        tokens: authResult,
        provider: 'phone'
      });
    } catch (error) {
      return reply.code(401).send({
        success: false,
        error: 'OTP verification failed',
        message: error.message
      });
    }
  });

  // Email/Password 傳統認證
  fastify.post('/auth/email/login', async (request, reply) => {
    const { email, password, tenantId } = request.body as {
      email: string;
      password: string;
      tenantId: string;
    };

    try {
      const initiateAuthCommand = new InitiateAuthCommand({
        ClientId: process.env.COGNITO_CLIENT_ID!,
        AuthFlow: 'USER_PASSWORD_AUTH',
        AuthParameters: {
          USERNAME: `email_${email}_${tenantId}`,
          PASSWORD: password
        }
      });

      const result = await cognitoClient.send(initiateAuthCommand);

      if (result.AuthenticationResult) {
        return reply.code(200).send({
          success: true,
          tokens: {
            accessToken: result.AuthenticationResult.AccessToken!,
            refreshToken: result.AuthenticationResult.RefreshToken!,
            idToken: result.AuthenticationResult.IdToken!
          },
          provider: 'email'
        });
      }

      throw new Error('Authentication failed');
    } catch (error) {
      return reply.code(401).send({
        success: false,
        error: 'Email authentication failed',
        message: error.message
      });
    }
  });

  // Token 刷新端點
  fastify.post('/auth/refresh', async (request, reply) => {
    const { refreshToken } = request.body as { refreshToken: string };

    try {
      const refreshCommand = new InitiateAuthCommand({
        ClientId: process.env.COGNITO_CLIENT_ID!,
        AuthFlow: 'REFRESH_TOKEN_AUTH',
        AuthParameters: {
          REFRESH_TOKEN: refreshToken
        }
      });

      const result = await cognitoClient.send(refreshCommand);

      return reply.code(200).send({
        success: true,
        tokens: {
          accessToken: result.AuthenticationResult?.AccessToken!,
          idToken: result.AuthenticationResult?.IdToken!
        }
      });
    } catch (error) {
      return reply.code(401).send({
        success: false,
        error: 'Token refresh failed',
        message: error.message
      });
    }
  });
}

💰 成本分析與效益評估

AWS Cognito 成本結構

// 每月活躍用戶 (MAU) 成本分析
const cognitoCostAnalysis = {
  // 免費額度
  freeTier: {
    mau: 50000,
    cost: 0
  },

  // 付費階層
  paidTier: {
    // $0.0055 per MAU (前 50K 後)
    perMAU: 0.0055,

    // LINE Login 等第三方 Provider
    federatedUsers: {
      // $0.015 per MAU
      perMAU: 0.015
    }
  },

  // 範例計算:中型健身房 (1000 會員)
  mediumGym: {
    totalMembers: 1000,
    activeRate: 0.6,  // 60% 月活躍率
    mau: 600,

    // 認證方式分布
    authMethods: {
      line: 400,      // 67% 使用 LINE Login
      phone: 150,     // 25% 使用 Phone OTP
      email: 50       // 8% 使用 Email/Password
    },

    // 月費用計算
    monthlyCost: {
      // LINE Login 用戶:400 * $0.015 = $6
      lineUsers: 400 * 0.015,

      // Phone/Email 用戶:200 * $0 (免費額度內)
      traditionalUsers: 0,

      total: 6  // $6/月
    }
  },

  // 大型連鎖 (50 家分店,總計 20K 會員)
  largeChain: {
    totalMembers: 20000,
    activeRate: 0.5,
    mau: 10000,

    authMethods: {
      line: 7000,     // 70% LINE Login
      phone: 2000,    // 20% Phone OTP
      email: 1000     // 10% Email/Password
    },

    monthlyCost: {
      // LINE Login:7000 * $0.015 = $105
      lineUsers: 7000 * 0.015,

      // 傳統認證:3000 * $0 (免費額度內)
      traditionalUsers: 0,

      total: 105  // $105/月 (平均每家分店 $2.1)
    }
  }
};

ROI 效益分析

const authSystemROI = {
  // 自建 vs AWS Cognito 對比
  buildVsBuy: {
    // 自建認證系統成本
    selfBuild: {
      developmentTime: '3-6 個月',
      developerCost: 200000,  // $200K 開發成本
      maintenanceCost: 50000, // 年維護 $50K
      securityRisk: 'HIGH',   // 安全風險高
      complianceEffort: 'HIGH' // 合規工作量大
    },

    // AWS Cognito 解決方案
    awsCognito: {
      setupTime: '1-2 週',
      monthlyCost: 105,      // 大型連鎖 $105/月
      annualCost: 1260,      // $1,260/年
      securityRisk: 'LOW',   // AWS 等級安全
      complianceEffort: 'LOW' // SOC 2, GDPR 等預配置
    },

    // 投資回報分析
    roi: {
      costSaving: 198740,    // 首年節省 $198,740
      timeToMarket: '4-5 個月提前上線',
      riskReduction: '顯著降低安全與合規風險',
      scalability: '無痛擴展到任何規模'
    }
  },

  // 業務價值
  businessValue: {
    userExperience: {
      lineIntegration: 'LINE 原生體驗,降低註冊阻力',
      multipleOptions: '多種認證方式,滿足不同用戶需求',
      seamlessLogin: '單次登入,全平台通用'
    },

    operationalEfficiency: {
      reducedSupport: '自助認證,減少客服負擔',
      automatedManagement: '用戶管理自動化',
      tenantIsolation: '完全的多租戶隔離'
    },

    technicalAdvantages: {
      globalScale: 'AWS 全球基礎設施',
      highAvailability: '99.9% 可用性 SLA',
      securityCompliance: '企業級安全標準'
    }
  }
};

🔒 安全考量與最佳實踐

多層次安全防護

// 安全設定檢查清單
const securityChecklist = {
  // Cognito 安全設定
  cognitoSecurity: {
    mfaPolicy: {
      admins: 'REQUIRED',      // 管理員強制 MFA
      users: 'OPTIONAL'        // 一般用戶可選
    },

    passwordPolicy: {
      complexity: 'STRONG',
      rotation: '90_DAYS',     // 管理員密碼 90 天輪換
      reusePolicy: 'NO_REUSE'
    },

    sessionManagement: {
      accessTokenTTL: '1_HOUR',
      refreshTokenTTL: '30_DAYS',
      idTokenTTL: '1_HOUR'
    }
  },

  // LINE Login 安全
  lineSecurity: {
    channelSecret: 'SECRETS_MANAGER',  // 密鑰存在 Secrets Manager
    tokenValidation: 'STRICT',         // 嚴格驗證 LINE Token
    userDataMinimization: 'ENABLED'    // 最小化用戶資料收集
  },

  // API 安全
  apiSecurity: {
    rateLimiting: {
      authEndpoints: '5_requests_per_minute',
      otpEndpoints: '3_requests_per_minute'
    },

    inputValidation: 'ZOD_SCHEMAS',
    outputSanitization: 'ENABLED',
    auditLogging: 'COMPREHENSIVE'
  },

  // 租戶隔離安全
  tenantSecurity: {
    dataIsolation: 'DATABASE_LEVEL',   // 資料庫級別隔離
    accessControl: 'TENANT_SCOPED',    // 租戶範圍存取控制
    auditTrail: 'PER_TENANT'           // 每租戶獨立審計日誌
  }
};

📊 監控與追蹤

CloudWatch 認證指標

// 認證系統監控設定
const authMonitoring = {
  // 核心指標
  coreMetrics: {
    loginSuccess: {
      metric: 'Auth/LoginSuccess',
      dimensions: ['Provider', 'TenantId'],
      alarmThreshold: '<95%'
    },

    loginLatency: {
      metric: 'Auth/LoginLatency',
      dimensions: ['Provider'],
      alarmThreshold: '>3000ms'
    },

    otpDelivery: {
      metric: 'Auth/OTPDelivery',
      dimensions: ['Provider'],
      alarmThreshold: '<98%'
    }
  },

  // 安全指標
  securityMetrics: {
    failedLogins: {
      metric: 'Auth/FailedLogins',
      dimensions: ['Provider', 'TenantId'],
      alarmThreshold: '>10_per_minute'
    },

    suspiciousActivity: {
      metric: 'Auth/SuspiciousActivity',
      dimensions: ['IP', 'UserAgent'],
      alarmThreshold: '>5_per_hour'
    }
  },

  // 業務指標
  businessMetrics: {
    newUserRegistration: {
      metric: 'Auth/NewUsers',
      dimensions: ['Provider', 'TenantId'],
      tracking: 'DAILY'
    },

    providerPreference: {
      metric: 'Auth/ProviderUsage',
      dimensions: ['Provider', 'TenantId'],
      tracking: 'WEEKLY'
    }
  }
};

總結

今天我們完成了健身房 SaaS 多元認證架構的完整實作:

核心功能

  1. LINE Login 深度整合: 透過 LIFF 提供原生 LINE 體驗
  2. AWS Cognito 統一管理: 企業級身份認證與授權
  3. 多租戶安全隔離: 每家健身房完全獨立的認證體驗
  4. 彈性認證組合: LINE/Phone/Email 三種方式自由搭配

💰 成本效益

  • 大型連鎖健身房: 每月僅 $105 認證成本 (20K 會員)
  • 中小型健身房: 每月 $6 以下 (基本在免費額度內)
  • 開發效率: 比自建節省 4-5 個月開發時間

🔒 安全保障

  • 企業級安全: AWS SOC 2, GDPR 合規
  • 多層防護: MFA + 速率限制 + 審計日誌
  • 租戶隔離: 完全的資料與認證隔離

🚀 技術優勢

  • 本地化優勢: LINE 深度整合,適合台灣市場
  • 全球擴展: AWS 基礎設施支援國際化
  • 無痛維護: 託管服務,免運維負擔

上一篇
Day 11: 30天部署SaaS產品到AWS-Database per Tenant 多租戶資料庫架構
下一篇
Day 13: 30天部署SaaS產品到AWS-S3 + CloudFront 檔案管理 - LINE Login 頭像儲存
系列文
30 天將工作室 SaaS 產品部署起來19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言