dramaling-vocab-learning/docs/04_technical/security.md

475 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 安全性實作指南
## 🔒 安全性總覽
DramaLing 遵循 OWASP 安全標準,實施多層防護策略。
## 🛡️ 認證與授權
### 1. NextAuth 配置
```typescript
// 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. 密碼安全
```typescript
// 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
```typescript
// 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 配置
```typescript
// 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 路由保護
```typescript
// 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 防護
```typescript
// 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 防護
```typescript
// 使用參數化查詢Supabase/Prisma 自動處理)
// ❌ 錯誤:直接字串串接
const query = `SELECT * FROM users WHERE email = '${email}'`
// ✅ 正確:使用參數化查詢
const { data } = await supabase
.from('users')
.select('*')
.eq('email', email) // 自動參數化
```
### 3. 檔案上傳安全
```typescript
// 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. 環境變數分離
```bash
# .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 管理
```typescript
// 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. 安全的錯誤訊息
```typescript
// 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',
}
}
```
## 🔍 安全標頭
```typescript
// 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. 審計日誌
```typescript
// 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. 異常檢測
```typescript
// 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
- [ ] 配置防火牆規則
- [ ] 啟用監控和警報
### 定期檢查
- [ ] 每週檢查安全日誌
- [ ] 每月更新依賴
- [ ] 每季進行安全審計
- [ ] 定期備份數據
- [ ] 測試災難恢復流程
## 🚨 事件響應計劃
### 安全事件處理流程
1. **檢測** - 監控系統發現異常
2. **評估** - 確定影響範圍
3. **隔離** - 限制受影響系統
4. **修復** - 修補漏洞
5. **恢復** - 恢復正常運作
6. **檢討** - 事後分析改進
### 緊急聯絡
- 安全團隊security@dramaling.com
- 24/7 監控monitor@dramaling.com
- 法律顧問legal@dramaling.com