11 KiB
11 KiB
安全性實作指南
🔒 安全性總覽
DramaLing 遵循 OWASP 安全標準,實施多層防護策略。
🛡️ 認證與授權
1. NextAuth 配置
// lib/auth.ts
import { NextAuthOptions } from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import GoogleProvider from 'next-auth/providers/google'
import { createClient } from '@supabase/supabase-js'
import bcrypt from 'bcryptjs'
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Missing credentials')
}
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
const { data: user } = await supabase
.from('users')
.select('*')
.eq('email', credentials.email)
.single()
if (!user || !await bcrypt.compare(credentials.password, user.password)) {
throw new Error('Invalid credentials')
}
return {
id: user.id,
email: user.email,
name: user.name,
}
}
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
})
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
jwt: {
secret: process.env.NEXTAUTH_SECRET,
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.userId = user.id
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.userId as string
}
return session
},
},
}
2. 密碼安全
// utils/password.ts
import bcrypt from 'bcryptjs'
import { z } from 'zod'
// 密碼驗證規則
export const passwordSchema = z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character')
// 密碼雜湊
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12)
}
// 密碼驗證
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash)
}
🔐 API 安全
1. Rate Limiting
// middleware/rateLimit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds
})
export async function rateLimitMiddleware(req: Request) {
const ip = req.headers.get('x-forwarded-for') ?? 'anonymous'
const { success, limit, reset, remaining } = await ratelimit.limit(ip)
if (!success) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': new Date(reset).toISOString(),
},
})
}
return null
}
2. CORS 配置
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: process.env.ALLOWED_ORIGIN || '*' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,DELETE,PATCH,POST,PUT' },
{ key: 'Access-Control-Allow-Headers', value: 'Authorization, Content-Type' },
],
},
]
},
}
3. API 路由保護
// app/api/flashcards/route.ts
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { rateLimitMiddleware } from '@/middleware/rateLimit'
export async function POST(req: Request) {
// Rate limiting
const rateLimitResponse = await rateLimitMiddleware(req)
if (rateLimitResponse) return rateLimitResponse
// 認證檢查
const session = await getServerSession(authOptions)
if (!session) {
return new Response('Unauthorized', { status: 401 })
}
// 輸入驗證
const body = await req.json()
const validation = flashcardSchema.safeParse(body)
if (!validation.success) {
return new Response(JSON.stringify({ errors: validation.error.errors }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// 處理請求...
}
🧹 輸入驗證與消毒
1. XSS 防護
// utils/sanitize.ts
import DOMPurify from 'isomorphic-dompurify'
export function sanitizeHTML(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target'],
})
}
// 使用範例
export function FlashcardContent({ content }: { content: string }) {
return (
<div
dangerouslySetInnerHTML={{
__html: sanitizeHTML(content)
}}
/>
)
}
2. SQL Injection 防護
// 使用參數化查詢(Supabase/Prisma 自動處理)
// ❌ 錯誤:直接字串串接
const query = `SELECT * FROM users WHERE email = '${email}'`
// ✅ 正確:使用參數化查詢
const { data } = await supabase
.from('users')
.select('*')
.eq('email', email) // 自動參數化
3. 檔案上傳安全
// utils/fileUpload.ts
import { z } from 'zod'
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp']
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
export const fileSchema = z.object({
name: z.string(),
size: z.number().max(MAX_FILE_SIZE, 'File too large'),
type: z.enum(ALLOWED_FILE_TYPES as [string, ...string[]]),
})
export async function validateFile(file: File) {
// 檢查檔案類型
if (!ALLOWED_FILE_TYPES.includes(file.type)) {
throw new Error('Invalid file type')
}
// 檢查檔案大小
if (file.size > MAX_FILE_SIZE) {
throw new Error('File too large')
}
// 檢查檔案內容(Magic Number)
const buffer = await file.arrayBuffer()
const bytes = new Uint8Array(buffer)
const signatures = {
jpeg: [0xFF, 0xD8, 0xFF],
png: [0x89, 0x50, 0x4E, 0x47],
}
// 驗證檔案簽名...
return true
}
🔑 環境變數安全
1. 環境變數分離
# .env.local (開發環境)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# .env.production (生產環境)
NEXT_PUBLIC_APP_URL=https://dramaling.com
# .env.vault (加密儲存敏感資料)
SUPABASE_SERVICE_ROLE_KEY=encrypted:xxx
2. Secrets 管理
// utils/secrets.ts
export function getSecret(key: string): string {
const value = process.env[key]
if (!value) {
throw new Error(`Missing required environment variable: ${key}`)
}
// 生產環境檢查
if (process.env.NODE_ENV === 'production') {
if (value.includes('test') || value.includes('example')) {
throw new Error(`Invalid production value for ${key}`)
}
}
return value
}
🛑 錯誤處理安全
1. 安全的錯誤訊息
// utils/errorHandler.ts
export function sanitizeError(error: unknown): { message: string; code: string } {
// 生產環境:隱藏詳細錯誤
if (process.env.NODE_ENV === 'production') {
console.error('Internal error:', error) // 記錄到伺服器日誌
return {
message: 'An error occurred. Please try again later.',
code: 'INTERNAL_ERROR',
}
}
// 開發環境:顯示詳細錯誤
if (error instanceof Error) {
return {
message: error.message,
code: 'DEV_ERROR',
}
}
return {
message: 'Unknown error',
code: 'UNKNOWN',
}
}
🔍 安全標頭
// middleware.ts
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
// Content Security Policy
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://apis.google.com; " +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
"font-src 'self' https://fonts.gstatic.com; " +
"img-src 'self' data: https: blob:; " +
"connect-src 'self' https://*.supabase.co https://generativelanguage.googleapis.com;"
)
return response
}
📊 安全監控
1. 審計日誌
// utils/audit.ts
interface AuditLog {
userId: string
action: string
resource: string
timestamp: Date
ip?: string
userAgent?: string
}
export async function logAuditEvent(event: AuditLog) {
await supabase.from('audit_logs').insert({
...event,
timestamp: new Date().toISOString(),
})
}
// 使用範例
await logAuditEvent({
userId: session.user.id,
action: 'DELETE_FLASHCARD',
resource: `flashcard:${flashcardId}`,
ip: request.headers.get('x-forwarded-for'),
userAgent: request.headers.get('user-agent'),
})
2. 異常檢測
// utils/security/anomaly.ts
export async function detectAnomalies(userId: string) {
// 檢查異常登入模式
const recentLogins = await getRecentLogins(userId)
// 檢查異常 API 使用
const apiUsage = await getAPIUsage(userId)
if (apiUsage.count > 1000) {
await flagAccount(userId, 'EXCESSIVE_API_USAGE')
}
// 檢查異常數據存取
const dataAccess = await getDataAccessPatterns(userId)
if (dataAccess.uniqueIPs > 5) {
await flagAccount(userId, 'MULTIPLE_IP_ACCESS')
}
}
✅ 安全檢查清單
開發階段
- 所有 API 路由都有認證檢查
- 所有用戶輸入都經過驗證
- 敏感數據都已加密
- 實施 Rate Limiting
- 設置安全標頭
- 錯誤訊息不洩露敏感資訊
部署前
- 移除所有 console.log
- 更新所有依賴到最新安全版本
- 執行安全掃描(npm audit)
- 設置 HTTPS
- 配置防火牆規則
- 啟用監控和警報
定期檢查
- 每週檢查安全日誌
- 每月更新依賴
- 每季進行安全審計
- 定期備份數據
- 測試災難恢復流程
🚨 事件響應計劃
安全事件處理流程
- 檢測 - 監控系統發現異常
- 評估 - 確定影響範圍
- 隔離 - 限制受影響系統
- 修復 - 修補漏洞
- 恢復 - 恢復正常運作
- 檢討 - 事後分析改進
緊急聯絡
- 安全團隊:security@dramaling.com
- 24/7 監控:monitor@dramaling.com
- 法律顧問:legal@dramaling.com