在 Day 18 我們完成了前端效能優化,今天我們要建立前端安全防護體系。對於健身房 SaaS 系統,安全性至關重要 - 從會員個資保護到金流交易,我們將實作全面的前端安全機制,包括 XSS/CSRF 防護、JWT 安全處理、Content Security Policy 配置等企業級安全方案。
// src/utils/SecuritySanitizer.ts
import DOMPurify from 'dompurify';
export class SecuritySanitizer {
// HTML 內容淨化
static sanitizeHTML(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title', 'alt'],
FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
});
}
// URL 參數淨化
static sanitizeURL(url: string): string {
try {
const urlObj = new URL(url);
// 只允許 HTTPS 和相對路徑
if (urlObj.protocol !== 'https:' && urlObj.protocol !== 'http:') {
throw new Error('Invalid protocol');
}
// 檢查是否為可信任的域名
const trustedDomains = ['api.kyo-saas.com', 'localhost'];
if (!trustedDomains.includes(urlObj.hostname)) {
throw new Error('Untrusted domain');
}
return urlObj.toString();
} catch (error) {
console.warn('Invalid URL detected:', url);
return '/';
}
}
// 用戶輸入淨化
static sanitizeInput(input: string): string {
return input
.replace(/[<>'"&]/g, (match) => {
const htmlEntities: Record<string, string> = {
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'&': '&'
};
return htmlEntities[match];
})
.trim();
}
// JSON 回應淨化
static sanitizeAPIResponse(response: any): any {
if (typeof response === 'string') {
return this.sanitizeInput(response);
}
if (Array.isArray(response)) {
return response.map(item => this.sanitizeAPIResponse(item));
}
if (typeof response === 'object' && response !== null) {
const sanitized: any = {};
for (const [key, value] of Object.entries(response)) {
sanitized[this.sanitizeInput(key)] = this.sanitizeAPIResponse(value);
}
return sanitized;
}
return response;
}
}
// React 組件中的安全使用
export const SafeHTML: React.FC<{ content: string; className?: string }> = ({
content,
className
}) => {
const safeContent = SecuritySanitizer.sanitizeHTML(content);
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: safeContent }}
/>
);
};
// src/utils/CSRFProtection.ts
export class CSRFProtection {
private static readonly CSRF_TOKEN_KEY = 'csrf_token';
private static readonly CSRF_HEADER = 'X-CSRF-Token';
// 產生 CSRF Token
static generateToken(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// 儲存 CSRF Token
static storeToken(token: string): void {
sessionStorage.setItem(this.CSRF_TOKEN_KEY, token);
// 同時設置到 meta tag 供伺服器驗證
let metaTag = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement;
if (!metaTag) {
metaTag = document.createElement('meta');
metaTag.name = 'csrf-token';
document.head.appendChild(metaTag);
}
metaTag.content = token;
}
// 取得 CSRF Token
static getToken(): string | null {
return sessionStorage.getItem(this.CSRF_TOKEN_KEY);
}
// 驗證 Referer Header
static validateReferer(allowedOrigins: string[]): boolean {
const referer = document.referrer;
if (!referer) return false;
try {
const refererURL = new URL(referer);
return allowedOrigins.some(origin => {
const originURL = new URL(origin);
return refererURL.origin === originURL.origin;
});
} catch {
return false;
}
}
// Double Submit Cookie 模式
static setupDoubleSubmitCookie(): void {
const token = this.generateToken();
// 設置 Cookie (HttpOnly=false 讓前端可讀取)
document.cookie = `csrf_token=${token}; Secure; SameSite=Strict; Path=/`;
// 同時儲存到 localStorage 供 AJAX 請求使用
this.storeToken(token);
}
// 驗證 Double Submit
static validateDoubleSubmit(): boolean {
const cookieToken = this.getCookieValue('csrf_token');
const storageToken = this.getToken();
return cookieToken && storageToken && cookieToken === storageToken;
}
private static getCookieValue(name: string): string | null {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split('=');
if (cookieName === name) {
return cookieValue;
}
}
return null;
}
}
// Axios 攔截器整合
import axios, { AxiosRequestConfig } from 'axios';
axios.interceptors.request.use((config: AxiosRequestConfig) => {
// 只對非 GET 請求添加 CSRF 保護
if (config.method && !['get', 'head', 'options'].includes(config.method.toLowerCase())) {
const csrfToken = CSRFProtection.getToken();
if (csrfToken) {
config.headers = config.headers || {};
config.headers['X-CSRF-Token'] = csrfToken;
}
// 驗證 Double Submit
if (!CSRFProtection.validateDoubleSubmit()) {
console.warn('CSRF token validation failed');
return Promise.reject(new Error('CSRF protection failed'));
}
}
return config;
});
// src/utils/ContentSecurityPolicy.ts
export class ContentSecurityPolicy {
private static readonly CSP_NONCE_KEY = 'csp_nonce';
// 產生 Nonce
static generateNonce(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array));
}
// 設置 CSP Nonce
static setupNonce(): string {
const nonce = this.generateNonce();
sessionStorage.setItem(this.CSP_NONCE_KEY, nonce);
return nonce;
}
// 取得 CSP Nonce
static getNonce(): string | null {
return sessionStorage.getItem(this.CSP_NONCE_KEY);
}
// 動態建立 CSP 策略
static buildCSPPolicy(environment: 'development' | 'production'): string {
const nonce = this.getNonce();
const basePolicy = {
'default-src': ["'self'"],
'script-src': [
"'self'",
nonce ? `'nonce-${nonce}'` : null,
environment === 'development' ? "'unsafe-eval'" : null,
'https://cdnjs.cloudflare.com',
'https://www.google-analytics.com'
].filter(Boolean),
'style-src': [
"'self'",
"'unsafe-inline'", // CSS-in-JS 需要
'https://fonts.googleapis.com'
],
'font-src': [
"'self'",
'https://fonts.gstatic.com',
'data:'
],
'img-src': [
"'self'",
'data:',
'blob:',
'https://images.kyo-saas.com',
'https://cdn.kyo-saas.com'
],
'connect-src': [
"'self'",
'https://api.kyo-saas.com',
'wss://api.kyo-saas.com',
environment === 'development' ? 'ws://localhost:*' : null,
environment === 'development' ? 'http://localhost:*' : null
].filter(Boolean),
'frame-src': [
"'none'"
],
'object-src': [
"'none'"
],
'base-uri': [
"'self'"
],
'form-action': [
"'self'"
],
'frame-ancestors': [
"'none'"
],
'upgrade-insecure-requests': environment === 'production' ? [] : null
};
// 建構 CSP 字串
const cspString = Object.entries(basePolicy)
.filter(([_, value]) => value !== null)
.map(([directive, sources]) => {
if (Array.isArray(sources) && sources.length > 0) {
return `${directive} ${sources.join(' ')}`;
} else if (sources === []) {
return directive;
}
return null;
})
.filter(Boolean)
.join('; ');
return cspString;
}
// 設置 CSP Meta Tag
static setCSPMetaTag(environment: 'development' | 'production'): void {
const cspPolicy = this.buildCSPPolicy(environment);
let metaTag = document.querySelector('meta[http-equiv="Content-Security-Policy"]') as HTMLMetaElement;
if (!metaTag) {
metaTag = document.createElement('meta');
metaTag.httpEquiv = 'Content-Security-Policy';
document.head.appendChild(metaTag);
}
metaTag.content = cspPolicy;
}
// CSP 違規報告處理
static setupViolationReporting(): void {
document.addEventListener('securitypolicyviolation', (event) => {
const violation = {
documentURI: event.documentURI,
violatedDirective: event.violatedDirective,
blockedURI: event.blockedURI,
lineNumber: event.lineNumber,
columnNumber: event.columnNumber,
sourceFile: event.sourceFile,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// 發送違規報告到後端
this.reportViolation(violation);
});
}
private static async reportViolation(violation: any): Promise<void> {
try {
await fetch('/api/security/csp-violation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(violation)
});
} catch (error) {
console.error('Failed to report CSP violation:', error);
}
}
}
// 在應用程式啟動時初始化 CSP
export const initializeCSP = () => {
const environment = process.env.NODE_ENV as 'development' | 'production';
// 設置 Nonce
const nonce = ContentSecurityPolicy.setupNonce();
// 設置 CSP 策略
ContentSecurityPolicy.setCSPMetaTag(environment);
// 設置違規報告
ContentSecurityPolicy.setupViolationReporting();
return nonce;
};
// src/utils/SecureTokenManager.ts
interface TokenData {
accessToken: string;
refreshToken: string;
expiresAt: number;
tokenType: string;
}
export class SecureTokenManager {
private static readonly ACCESS_TOKEN_KEY = 'access_token';
private static readonly REFRESH_TOKEN_KEY = 'refresh_token';
private static readonly TOKEN_EXPIRY_KEY = 'token_expiry';
private static readonly ENCRYPTION_KEY = 'user_session_key';
// 加密 Token
private static async encryptToken(token: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(token);
// 使用 Web Crypto API 進行加密
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(this.getEncryptionKey()),
{ name: 'AES-GCM' },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
);
// 將 IV 和加密數據組合
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv);
combined.set(new Uint8Array(encrypted), iv.length);
return btoa(String.fromCharCode(...combined));
}
// 解密 Token
private static async decryptToken(encryptedToken: string): Promise<string> {
try {
const combined = new Uint8Array(
atob(encryptedToken).split('').map(char => char.charCodeAt(0))
);
const iv = combined.slice(0, 12);
const encrypted = combined.slice(12);
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(this.getEncryptionKey()),
{ name: 'AES-GCM' },
false,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
encrypted
);
return new TextDecoder().decode(decrypted);
} catch (error) {
console.error('Token decryption failed:', error);
this.clearTokens();
throw new Error('Invalid token');
}
}
// 產生加密金鑰 (基於用戶會話)
private static getEncryptionKey(): string {
let key = sessionStorage.getItem(this.ENCRYPTION_KEY);
if (!key) {
key = this.generateRandomKey();
sessionStorage.setItem(this.ENCRYPTION_KEY, key);
}
return key;
}
private static generateRandomKey(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// 儲存 Token (加密)
static async storeTokens(tokenData: TokenData): Promise<void> {
try {
const encryptedAccessToken = await this.encryptToken(tokenData.accessToken);
const encryptedRefreshToken = await this.encryptToken(tokenData.refreshToken);
// 使用 sessionStorage 而非 localStorage 增加安全性
sessionStorage.setItem(this.ACCESS_TOKEN_KEY, encryptedAccessToken);
sessionStorage.setItem(this.REFRESH_TOKEN_KEY, encryptedRefreshToken);
sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, tokenData.expiresAt.toString());
// 設置自動清理
this.scheduleTokenCleanup(tokenData.expiresAt);
} catch (error) {
console.error('Failed to store tokens:', error);
throw new Error('Token storage failed');
}
}
// 取得 Access Token
static async getAccessToken(): Promise<string | null> {
try {
const encryptedToken = sessionStorage.getItem(this.ACCESS_TOKEN_KEY);
if (!encryptedToken) return null;
// 檢查是否過期
if (this.isTokenExpired()) {
await this.refreshTokens();
return this.getAccessToken();
}
return await this.decryptToken(encryptedToken);
} catch (error) {
console.error('Failed to get access token:', error);
return null;
}
}
// 取得 Refresh Token
static async getRefreshToken(): Promise<string | null> {
try {
const encryptedToken = sessionStorage.getItem(this.REFRESH_TOKEN_KEY);
if (!encryptedToken) return null;
return await this.decryptToken(encryptedToken);
} catch (error) {
console.error('Failed to get refresh token:', error);
return null;
}
}
// 檢查 Token 是否過期
static isTokenExpired(): boolean {
const expiryTime = sessionStorage.getItem(this.TOKEN_EXPIRY_KEY);
if (!expiryTime) return true;
const now = Date.now();
const expiry = parseInt(expiryTime, 10);
// 提前 5 分鐘視為過期,確保有足夠時間刷新
return now >= (expiry - 5 * 60 * 1000);
}
// 刷新 Token
static async refreshTokens(): Promise<boolean> {
try {
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
this.clearTokens();
return false;
}
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
this.clearTokens();
return false;
}
const newTokenData = await response.json();
await this.storeTokens(newTokenData);
return true;
} catch (error) {
console.error('Token refresh failed:', error);
this.clearTokens();
return false;
}
}
// 清除所有 Token
static clearTokens(): void {
sessionStorage.removeItem(this.ACCESS_TOKEN_KEY);
sessionStorage.removeItem(this.REFRESH_TOKEN_KEY);
sessionStorage.removeItem(this.TOKEN_EXPIRY_KEY);
sessionStorage.removeItem(this.ENCRYPTION_KEY);
}
// 排程 Token 清理
private static scheduleTokenCleanup(expiresAt: number): void {
const now = Date.now();
const delay = expiresAt - now;
if (delay > 0) {
setTimeout(() => {
this.clearTokens();
}, delay);
}
}
// JWT Payload 解析 (不驗證簽名,僅用於前端邏輯)
static parseJWTPayload(token: string): any {
try {
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid JWT format');
}
const payload = parts[1];
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
} catch (error) {
console.error('JWT parse error:', error);
return null;
}
}
// 取得用戶資訊 (從 JWT)
static async getUserInfo(): Promise<any> {
const token = await this.getAccessToken();
if (!token) return null;
const payload = this.parseJWTPayload(token);
return payload ? {
userId: payload.sub,
email: payload.email,
roles: payload.roles || [],
gymId: payload.gymId,
permissions: payload.permissions || []
} : null;
}
}
// src/utils/PermissionManager.ts
export enum Role {
SUPER_ADMIN = 'super_admin',
GYM_OWNER = 'gym_owner',
GYM_MANAGER = 'gym_manager',
TRAINER = 'trainer',
MEMBER = 'member'
}
export enum Permission {
// 健身房管理
MANAGE_GYM = 'manage_gym',
VIEW_GYM_ANALYTICS = 'view_gym_analytics',
// 會員管理
MANAGE_MEMBERS = 'manage_members',
VIEW_MEMBERS = 'view_members',
// 課程管理
MANAGE_COURSES = 'manage_courses',
VIEW_COURSES = 'view_courses',
// 財務管理
MANAGE_BILLING = 'manage_billing',
VIEW_BILLING = 'view_billing',
// 系統管理
MANAGE_USERS = 'manage_users',
SYSTEM_CONFIG = 'system_config'
}
export class PermissionManager {
private static rolePermissions: Record<Role, Permission[]> = {
[Role.SUPER_ADMIN]: [
Permission.MANAGE_GYM,
Permission.VIEW_GYM_ANALYTICS,
Permission.MANAGE_MEMBERS,
Permission.VIEW_MEMBERS,
Permission.MANAGE_COURSES,
Permission.VIEW_COURSES,
Permission.MANAGE_BILLING,
Permission.VIEW_BILLING,
Permission.MANAGE_USERS,
Permission.SYSTEM_CONFIG
],
[Role.GYM_OWNER]: [
Permission.MANAGE_GYM,
Permission.VIEW_GYM_ANALYTICS,
Permission.MANAGE_MEMBERS,
Permission.VIEW_MEMBERS,
Permission.MANAGE_COURSES,
Permission.VIEW_COURSES,
Permission.MANAGE_BILLING,
Permission.VIEW_BILLING,
Permission.MANAGE_USERS
],
[Role.GYM_MANAGER]: [
Permission.VIEW_GYM_ANALYTICS,
Permission.MANAGE_MEMBERS,
Permission.VIEW_MEMBERS,
Permission.MANAGE_COURSES,
Permission.VIEW_COURSES,
Permission.VIEW_BILLING
],
[Role.TRAINER]: [
Permission.VIEW_MEMBERS,
Permission.MANAGE_COURSES,
Permission.VIEW_COURSES
],
[Role.MEMBER]: [
Permission.VIEW_COURSES
]
};
// 檢查用戶是否有特定權限
static async hasPermission(permission: Permission): Promise<boolean> {
try {
const userInfo = await SecureTokenManager.getUserInfo();
if (!userInfo) return false;
// 直接檢查權限列表
if (userInfo.permissions?.includes(permission)) {
return true;
}
// 根據角色檢查權限
if (userInfo.roles?.length > 0) {
return userInfo.roles.some((role: Role) =>
this.rolePermissions[role]?.includes(permission)
);
}
return false;
} catch (error) {
console.error('Permission check failed:', error);
return false;
}
}
// 檢查用戶是否有任一權限
static async hasAnyPermission(permissions: Permission[]): Promise<boolean> {
for (const permission of permissions) {
if (await this.hasPermission(permission)) {
return true;
}
}
return false;
}
// 檢查用戶是否有所有權限
static async hasAllPermissions(permissions: Permission[]): Promise<boolean> {
for (const permission of permissions) {
if (!(await this.hasPermission(permission))) {
return false;
}
}
return true;
}
// 檢查用戶角色
static async hasRole(role: Role): Promise<boolean> {
try {
const userInfo = await SecureTokenManager.getUserInfo();
return userInfo?.roles?.includes(role) || false;
} catch (error) {
console.error('Role check failed:', error);
return false;
}
}
// 取得用戶所有權限
static async getUserPermissions(): Promise<Permission[]> {
try {
const userInfo = await SecureTokenManager.getUserInfo();
if (!userInfo) return [];
const permissions = new Set<Permission>();
// 加入直接權限
if (userInfo.permissions) {
userInfo.permissions.forEach((p: Permission) => permissions.add(p));
}
// 加入角色權限
if (userInfo.roles) {
userInfo.roles.forEach((role: Role) => {
this.rolePermissions[role]?.forEach(p => permissions.add(p));
});
}
return Array.from(permissions);
} catch (error) {
console.error('Failed to get user permissions:', error);
return [];
}
}
}
// 權限檢查 Hook
import { useState, useEffect } from 'react';
export const usePermission = (permission: Permission) => {
const [hasPermission, setHasPermission] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const checkPermission = async () => {
setLoading(true);
try {
const result = await PermissionManager.hasPermission(permission);
setHasPermission(result);
} catch (error) {
console.error('Permission check error:', error);
setHasPermission(false);
} finally {
setLoading(false);
}
};
checkPermission();
}, [permission]);
return { hasPermission, loading };
};
// 多權限檢查 Hook
export const usePermissions = (permissions: Permission[]) => {
const [permissionMap, setPermissionMap] = useState<Record<Permission, boolean>>({});
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const checkPermissions = async () => {
setLoading(true);
try {
const results: Record<Permission, boolean> = {};
await Promise.all(
permissions.map(async (permission) => {
results[permission] = await PermissionManager.hasPermission(permission);
})
);
setPermissionMap(results);
} catch (error) {
console.error('Permissions check error:', error);
setPermissionMap({});
} finally {
setLoading(false);
}
};
checkPermissions();
}, [permissions]);
return { permissionMap, loading };
};
// src/components/RouteGuard.tsx
import React, { useEffect, useState } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { Permission, PermissionManager, Role } from '@/utils/PermissionManager';
import { SecureTokenManager } from '@/utils/SecureTokenManager';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
interface RouteGuardProps {
children: React.ReactNode;
requiredPermissions?: Permission[];
requiredRoles?: Role[];
requireAny?: boolean; // 是否只需滿足任一條件
redirectTo?: string;
}
export const RouteGuard: React.FC<RouteGuardProps> = ({
children,
requiredPermissions = [],
requiredRoles = [],
requireAny = false,
redirectTo = '/login'
}) => {
const [loading, setLoading] = useState(true);
const [hasAccess, setHasAccess] = useState(false);
const location = useLocation();
useEffect(() => {
const checkAccess = async () => {
setLoading(true);
try {
// 檢查是否已登入
const token = await SecureTokenManager.getAccessToken();
if (!token) {
setHasAccess(false);
setLoading(false);
return;
}
// 如果沒有設定權限要求,則允許已登入用戶訪問
if (requiredPermissions.length === 0 && requiredRoles.length === 0) {
setHasAccess(true);
setLoading(false);
return;
}
let permissionCheck = false;
let roleCheck = false;
// 檢查權限
if (requiredPermissions.length > 0) {
if (requireAny) {
permissionCheck = await PermissionManager.hasAnyPermission(requiredPermissions);
} else {
permissionCheck = await PermissionManager.hasAllPermissions(requiredPermissions);
}
} else {
permissionCheck = true; // 沒有權限要求則視為通過
}
// 檢查角色
if (requiredRoles.length > 0) {
if (requireAny) {
roleCheck = await Promise.all(
requiredRoles.map(role => PermissionManager.hasRole(role))
).then(results => results.some(result => result));
} else {
roleCheck = await Promise.all(
requiredRoles.map(role => PermissionManager.hasRole(role))
).then(results => results.every(result => result));
}
} else {
roleCheck = true; // 沒有角色要求則視為通過
}
// 根據 requireAny 決定最終結果
if (requireAny) {
setHasAccess(permissionCheck || roleCheck);
} else {
setHasAccess(permissionCheck && roleCheck);
}
} catch (error) {
console.error('Route guard check failed:', error);
setHasAccess(false);
} finally {
setLoading(false);
}
};
checkAccess();
}, [requiredPermissions, requiredRoles, requireAny, location.pathname]);
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner />
</div>
);
}
if (!hasAccess) {
return <Navigate to={redirectTo} state={{ from: location }} replace />;
}
return <>{children}</>;
};
// 條件性渲染組件
interface ConditionalRenderProps {
children: React.ReactNode;
permission?: Permission;
role?: Role;
permissions?: Permission[];
roles?: Role[];
requireAny?: boolean;
fallback?: React.ReactNode;
}
export const ConditionalRender: React.FC<ConditionalRenderProps> = ({
children,
permission,
role,
permissions = [],
roles = [],
requireAny = false,
fallback = null
}) => {
const [hasAccess, setHasAccess] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const checkAccess = async () => {
try {
let permissionCheck = true;
let roleCheck = true;
// 單一權限檢查
if (permission) {
permissionCheck = await PermissionManager.hasPermission(permission);
}
// 多權限檢查
if (permissions.length > 0) {
if (requireAny) {
permissionCheck = await PermissionManager.hasAnyPermission(permissions);
} else {
permissionCheck = await PermissionManager.hasAllPermissions(permissions);
}
}
// 單一角色檢查
if (role) {
roleCheck = await PermissionManager.hasRole(role);
}
// 多角色檢查
if (roles.length > 0) {
if (requireAny) {
roleCheck = await Promise.all(
roles.map(r => PermissionManager.hasRole(r))
).then(results => results.some(result => result));
} else {
roleCheck = await Promise.all(
roles.map(r => PermissionManager.hasRole(r))
).then(results => results.every(result => result));
}
}
setHasAccess(permissionCheck && roleCheck);
} catch (error) {
console.error('Conditional render check failed:', error);
setHasAccess(false);
} finally {
setLoading(false);
}
};
checkAccess();
}, [permission, role, permissions, roles, requireAny]);
if (loading) {
return null; // 或者返回載入狀態
}
return hasAccess ? <>{children}</> : <>{fallback}</>;
};
// src/utils/SecureAPIClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { SecureTokenManager } from './SecureTokenManager';
import { CSRFProtection } from './CSRFProtection';
import { SecuritySanitizer } from './SecuritySanitizer';
export class SecureAPIClient {
private static instance: SecureAPIClient;
private axiosInstance: AxiosInstance;
private readonly baseURL: string;
constructor() {
this.baseURL = process.env.REACT_APP_API_BASE_URL || 'https://api.kyo-saas.com';
this.axiosInstance = this.createSecureInstance();
this.setupInterceptors();
}
static getInstance(): SecureAPIClient {
if (!SecureAPIClient.instance) {
SecureAPIClient.instance = new SecureAPIClient();
}
return SecureAPIClient.instance;
}
private createSecureInstance(): AxiosInstance {
return axios.create({
baseURL: this.baseURL,
timeout: 30000,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
}
});
}
private setupInterceptors(): void {
// 請求攔截器
this.axiosInstance.interceptors.request.use(
async (config: AxiosRequestConfig) => {
// 添加認證 Token
const token = await SecureTokenManager.getAccessToken();
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
// 添加 CSRF 保護
if (config.method && !['get', 'head', 'options'].includes(config.method.toLowerCase())) {
const csrfToken = CSRFProtection.getToken();
if (csrfToken) {
config.headers = config.headers || {};
config.headers['X-CSRF-Token'] = csrfToken;
}
}
// 添加請求簽名
if (config.data) {
const signature = await this.signRequest(config.data);
config.headers = config.headers || {};
config.headers['X-Request-Signature'] = signature;
}
// 添加時間戳防重放攻擊
config.headers = config.headers || {};
config.headers['X-Timestamp'] = Date.now().toString();
// 淨化請求數據
if (config.data) {
config.data = SecuritySanitizer.sanitizeAPIResponse(config.data);
}
return config;
},
(error) => {
console.error('Request interceptor error:', error);
return Promise.reject(error);
}
);
// 回應攔截器
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
// 淨化回應數據
if (response.data) {
response.data = SecuritySanitizer.sanitizeAPIResponse(response.data);
}
return response;
},
async (error) => {
const originalRequest = error.config;
// Token 過期處理
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshSuccess = await SecureTokenManager.refreshTokens();
if (refreshSuccess) {
const newToken = await SecureTokenManager.getAccessToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return this.axiosInstance(originalRequest);
} else {
// 刷新失敗,重定向到登入頁面
window.location.href = '/login';
return Promise.reject(error);
}
}
// CSRF 錯誤處理
if (error.response?.status === 403 && error.response?.data?.error === 'CSRF token mismatch') {
CSRFProtection.setupDoubleSubmitCookie();
return this.axiosInstance(originalRequest);
}
// 記錄安全相關錯誤
this.logSecurityError(error);
return Promise.reject(error);
}
);
}
// 請求簽名
private async signRequest(data: any): Promise<string> {
const payload = JSON.stringify(data);
const encoder = new TextEncoder();
const payloadBytes = encoder.encode(payload);
// 使用 HMAC-SHA256 簽名
const key = await this.getSigningKey();
const signature = await crypto.subtle.sign(
'HMAC',
key,
payloadBytes
);
return Array.from(new Uint8Array(signature))
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}
private async getSigningKey(): Promise<CryptoKey> {
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(this.getClientSecret()),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
return keyMaterial;
}
private getClientSecret(): string {
// 在實際應用中,這應該是從安全的來源獲取
return process.env.REACT_APP_CLIENT_SECRET || 'default-secret';
}
// 記錄安全錯誤
private logSecurityError(error: any): void {
const securityError = {
timestamp: new Date().toISOString(),
url: error.config?.url,
method: error.config?.method,
status: error.response?.status,
message: error.message,
userAgent: navigator.userAgent,
referer: document.referrer
};
// 發送到安全監控系統
this.reportSecurityIncident(securityError);
}
private async reportSecurityIncident(incident: any): Promise<void> {
try {
await fetch('/api/security/incident', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(incident)
});
} catch (error) {
console.error('Failed to report security incident:', error);
}
}
// 公開 API 方法
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.get<T>(url, config);
return response.data;
}
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.post<T>(url, data, config);
return response.data;
}
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.put<T>(url, data, config);
return response.data;
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.axiosInstance.delete<T>(url, config);
return response.data;
}
}
// API Client 單例
export const apiClient = SecureAPIClient.getInstance();
// src/security/SecurityManager.ts
import { ContentSecurityPolicy, initializeCSP } from '@/utils/ContentSecurityPolicy';
import { CSRFProtection } from '@/utils/CSRFProtection';
import { SecureTokenManager } from '@/utils/SecureTokenManager';
export class SecurityManager {
static async initialize(): Promise<void> {
try {
console.log('Initializing security systems...');
// 1. 初始化 CSP
const nonce = initializeCSP();
console.log('CSP initialized with nonce:', nonce.substring(0, 8) + '...');
// 2. 設置 CSRF 保護
CSRFProtection.setupDoubleSubmitCookie();
console.log('CSRF protection enabled');
// 3. 驗證現有 Token
const isValidToken = await this.validateExistingSession();
console.log('Token validation:', isValidToken ? 'valid' : 'invalid');
// 4. 設置安全事件監聽
this.setupSecurityEventListeners();
console.log('Security event listeners configured');
// 5. 定期安全檢查
this.startSecurityMonitoring();
console.log('Security monitoring started');
console.log('Security initialization complete');
} catch (error) {
console.error('Security initialization failed:', error);
throw error;
}
}
private static async validateExistingSession(): Promise<boolean> {
try {
const token = await SecureTokenManager.getAccessToken();
if (!token) return false;
const userInfo = await SecureTokenManager.getUserInfo();
return !!userInfo;
} catch (error) {
console.error('Session validation failed:', error);
SecureTokenManager.clearTokens();
return false;
}
}
private static setupSecurityEventListeners(): void {
// 監聽頁面隱藏事件,清理敏感資料
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 頁面隱藏時清理敏感 DOM 內容
this.clearSensitiveData();
}
});
// 監聽開發者工具
let devtools = false;
setInterval(() => {
if (!devtools && (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200)) {
devtools = true;
console.warn('Developer tools detected');
// 可以選擇在生產環境中採取額外安全措施
}
}, 500);
// 監聽複製事件,防止敏感資料外洩
document.addEventListener('copy', (event) => {
const selection = window.getSelection()?.toString();
if (selection && this.containsSensitiveData(selection)) {
event.preventDefault();
console.warn('Sensitive data copy prevented');
}
});
}
private static clearSensitiveData(): void {
// 清理表單中的敏感資料
const sensitiveInputs = document.querySelectorAll('input[type="password"], input[data-sensitive]');
sensitiveInputs.forEach((input: Element) => {
(input as HTMLInputElement).value = '';
});
}
private static containsSensitiveData(text: string): boolean {
const sensitivePatterns = [
/\b\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b/, // 信用卡號
/\b\d{3}-\d{2}-\d{4}\b/, // 社會安全號碼格式
/password/i,
/token/i,
/secret/i
];
return sensitivePatterns.some(pattern => pattern.test(text));
}
private static startSecurityMonitoring(): void {
// 每 5 分鐘檢查一次安全狀態
setInterval(async () => {
try {
// 檢查 Token 有效性
const tokenValid = await this.validateExistingSession();
if (!tokenValid) {
console.warn('Invalid session detected, redirecting to login');
window.location.href = '/login';
}
// 檢查 CSRF Token
if (!CSRFProtection.validateDoubleSubmit()) {
console.warn('CSRF validation failed, refreshing token');
CSRFProtection.setupDoubleSubmitCookie();
}
} catch (error) {
console.error('Security monitoring error:', error);
}
}, 5 * 60 * 1000);
}
static async cleanup(): Promise<void> {
console.log('Cleaning up security systems...');
SecureTokenManager.clearTokens();
CSRFProtection.generateToken(); // 重新產生新的 CSRF Token
console.log('Security cleanup complete');
}
}
今天我們建立了全面的前端安全防護體系: