經過 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' }
];
健身房 SaaS 認證架構
📱 LINE LIFF 💻 Web/App 📞 SMS OTP
│ │ │
├─── LINE Login ────┤ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────┐
│ AWS API Gateway + Lambda │
│ (認證協調服務) │
└─────────────┬───────────────┬───────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ AWS Cognito │ │ LINE Platform │
│ User Pool │ │ Provider │
│ (統一身份管理) │ │ (第三方認證) │
└─────────────────────┘ └──────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Tenant Database │
│ (每家健身房獨立用戶資料) │
└─────────────────────────────────────────────────┘
// 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
}
});
}
}
// 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;
}
// 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;
}
// 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);
}
}
// 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
});
}
});
}
// 每月活躍用戶 (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)
}
}
};
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' // 每租戶獨立審計日誌
}
};
// 認證系統監控設定
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 多元認證架構的完整實作: