做 ip 防呆機制

This commit is contained in:
2025-08-01 13:17:32 +08:00
parent 2282eed9a1
commit a54ae31896
7 changed files with 1139 additions and 125 deletions

View File

@@ -1,23 +1,57 @@
import { NextRequest, NextResponse } from 'next/server'
import { getClientIp, isIpAllowed } from '@/lib/ip-utils'
import { getClientIp, isIpAllowed, getIpLocation, getDetailedIpInfo } from '@/lib/ip-utils'
// 強制動態渲染
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
try {
const clientIp = getClientIp(request)
// 使用詳細的IP檢測功能
const detailedInfo = getDetailedIpInfo(request);
const clientIp = detailedInfo.detectedIp;
const allowedIps = process.env.ALLOWED_IPS || ''
const enableIpWhitelist = process.env.ENABLE_IP_WHITELIST === 'true'
const isAllowed = enableIpWhitelist ? isIpAllowed(clientIp, allowedIps) : true
// 嘗試獲取地理位置信息
let locationInfo = null
if (clientIp && clientIp !== '127.0.0.1' && isPublicIp(clientIp)) {
try {
locationInfo = await getIpLocation(clientIp)
} catch (error) {
console.error('Error fetching location info:', error)
}
}
return NextResponse.json({
ip: clientIp,
isAllowed,
enableIpWhitelist,
allowedIps: enableIpWhitelist ? allowedIps.split(',').map(ip => ip.trim()) : [],
timestamp: new Date().toISOString()
timestamp: new Date().toISOString(),
debug: {
allIpSources: detailedInfo.ipSources,
allFoundIps: detailedInfo.allFoundIps,
isLocalDevelopment: detailedInfo.isLocalDevelopment,
localIp: detailedInfo.localIp,
environment: process.env.NODE_ENV,
host: request.headers.get('host'),
referer: request.headers.get('referer'),
userAgent: request.headers.get('user-agent'),
},
location: locationInfo,
// 本地開發環境的特殊信息
development: detailedInfo.isLocalDevelopment ? {
message: '本地開發環境 - IP檢測可能受限',
suggestions: [
'在生產環境中部署後IP檢測會更準確',
'可以使用 ngrok 或類似工具進行外部測試',
'檢查防火牆和網路配置',
'確認代理伺服器設置'
]
} : null
})
} catch (error) {
console.error('Error getting IP info:', error)
@@ -26,4 +60,20 @@ export async function GET(request: NextRequest) {
{ status: 500 }
)
}
}
// 檢查是否為公網IP的輔助函數
function isPublicIp(ip: string): boolean {
const privateRanges = [
/^10\./, // 10.0.0.0/8
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
/^192\.168\./, // 192.168.0.0/16
/^127\./, // 127.0.0.0/8
/^169\.254\./, // 169.254.0.0/16 (Link-local)
/^0\./, // 0.0.0.0/8
/^224\./, // 224.0.0.0/4 (Multicast)
/^240\./, // 240.0.0.0/4 (Reserved)
];
return !privateRanges.some(range => range.test(ip));
}

336
app/test/ip-debug/page.tsx Normal file
View File

@@ -0,0 +1,336 @@
'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Globe, Shield, MapPin, RefreshCw, AlertCircle, CheckCircle, Info, Lightbulb } from 'lucide-react'
interface IpDebugInfo {
ip: string
isAllowed: boolean
enableIpWhitelist: boolean
allowedIps: string[]
timestamp: string
debug: {
allIpSources: Record<string, string | null>
allFoundIps: string[]
isLocalDevelopment: boolean
localIp: string | null
environment: string
host: string | null
referer: string | null
userAgent: string | null
}
location: any
development: {
message: string
suggestions: string[]
} | null
}
export default function IpDebugPage() {
const [ipInfo, setIpInfo] = useState<IpDebugInfo | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchIpInfo = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch('/api/ip')
if (!response.ok) {
throw new Error('無法獲取IP信息')
}
const data = await response.json()
setIpInfo(data)
} catch (error) {
console.error("無法獲取IP信息:", error)
setError("無法獲取IP信息")
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchIpInfo()
}, [])
if (loading) {
return (
<div className="container mx-auto p-6 max-w-4xl">
<div className="flex items-center justify-center min-h-[400px]">
<div className="flex items-center gap-2">
<RefreshCw className="w-5 h-5 animate-spin" />
<span>...</span>
</div>
</div>
</div>
)
}
if (error || !ipInfo) {
return (
<div className="container mx-auto p-6 max-w-4xl">
<Card className="border-red-200 bg-red-50">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-red-800">
<AlertCircle className="w-5 h-5" />
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-red-700">{error || '無法獲取IP信息'}</p>
<Button onClick={fetchIpInfo} className="mt-4">
<RefreshCw className="w-4 h-4 mr-2" />
</Button>
</CardContent>
</Card>
</div>
)
}
return (
<div className="container mx-auto p-6 max-w-4xl space-y-6">
<div className="text-center">
<h1 className="text-3xl font-bold mb-2">IP 調</h1>
<p className="text-muted-foreground">IP檢測信息和調試數據</p>
</div>
{/* 本地開發環境提示 */}
{ipInfo.development && (
<Alert className="border-blue-200 bg-blue-50">
<Info className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-blue-800">
<div className="font-medium mb-2">{ipInfo.development.message}</div>
<div className="text-sm space-y-1">
{ipInfo.development.suggestions.map((suggestion, index) => (
<div key={index} className="flex items-start gap-2">
<Lightbulb className="w-3 h-3 mt-0.5 text-blue-500 flex-shrink-0" />
<span>{suggestion}</span>
</div>
))}
</div>
</AlertDescription>
</Alert>
)}
{/* 主要IP信息 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="w-5 h-5" />
IP
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-medium">IP:</span>
<Badge variant={ipInfo.ip === '127.0.0.1' ? 'destructive' : 'default'}>
{ipInfo.ip}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">:</span>
{ipInfo.isAllowed ? (
<Badge variant="default" className="flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
</Badge>
) : (
<Badge variant="destructive" className="flex items-center gap-1">
<AlertCircle className="w-3 h-3" />
</Badge>
)}
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Shield className="w-4 h-4" />
<span className="text-sm">IP白名單:</span>
<Badge variant={ipInfo.enableIpWhitelist ? 'default' : 'secondary'}>
{ipInfo.enableIpWhitelist ? '已啟用' : '已停用'}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">:</span>
<Badge variant="outline">{ipInfo.debug.environment}</Badge>
</div>
{ipInfo.debug.isLocalDevelopment && ipInfo.debug.localIp && (
<div className="flex items-center gap-2">
<span className="text-sm">IP:</span>
<Badge variant="outline">{ipInfo.debug.localIp}</Badge>
</div>
)}
</div>
{ipInfo.location && (
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span className="text-sm">:</span>
<span className="text-sm">
{ipInfo.location.city}, {ipInfo.location.country} ({ipInfo.location.isp})
</span>
</div>
)}
</CardContent>
</Card>
{/* 所有找到的IP */}
<Card>
<CardHeader>
<CardTitle>IP</CardTitle>
<CardDescription>IP地址</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{ipInfo.debug.allFoundIps.length > 0 ? (
ipInfo.debug.allFoundIps.map((ip, index) => (
<Badge
key={index}
variant={ip === ipInfo.ip ? 'default' : 'outline'}
className={ip === '127.0.0.1' ? 'border-red-300 text-red-700' : ''}
>
{ip} {ip === ipInfo.ip && '(已選擇)'}
</Badge>
))
) : (
<span className="text-muted-foreground">IP</span>
)}
</div>
</CardContent>
</Card>
{/* 允許的IP列表 */}
{ipInfo.enableIpWhitelist && (
<Card>
<CardHeader>
<CardTitle>IP地址</CardTitle>
<CardDescription>IP白名單</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{ipInfo.allowedIps.length > 0 ? (
ipInfo.allowedIps.map((ip, index) => (
<Badge key={index} variant="outline">
{ip}
</Badge>
))
) : (
<span className="text-muted-foreground">IP白名單</span>
)}
</div>
</CardContent>
</Card>
)}
{/* 調試信息 */}
<Card>
<CardHeader>
<CardTitle>調</CardTitle>
<CardDescription>IP來源和請求頭信息</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2">IP來源:</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{Object.entries(ipInfo.debug.allIpSources).map(([key, value]) => (
<div key={key} className="flex justify-between items-center p-2 bg-muted rounded">
<span className="text-sm font-mono">{key}:</span>
<span className="text-sm text-muted-foreground">
{value || 'null'}
</span>
</div>
))}
</div>
</div>
<Separator />
<div>
<h4 className="font-medium mb-2">:</h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-sm">Host:</span>
<span className="text-sm text-muted-foreground">
{ipInfo.debug.host || 'null'}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm">Referer:</span>
<span className="text-sm text-muted-foreground">
{ipInfo.debug.referer || 'null'}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm">User Agent:</span>
<span className="text-sm text-muted-foreground max-w-xs truncate">
{ipInfo.debug.userAgent || 'null'}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm">:</span>
<span className="text-sm text-muted-foreground">
{new Date(ipInfo.timestamp).toLocaleString('zh-TW')}
</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 地理位置信息 */}
{ipInfo.location && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MapPin className="w-5 h-5" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1 text-sm">
<div>: {ipInfo.location.country} ({ipInfo.location.countryCode})</div>
<div>: {ipInfo.location.regionName}</div>
<div>: {ipInfo.location.city}</div>
<div>: {ipInfo.location.zip}</div>
<div>: {ipInfo.location.timezone}</div>
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="space-y-1 text-sm">
<div>ISP: {ipInfo.location.isp}</div>
<div>: {ipInfo.location.org}</div>
<div>AS: {ipInfo.location.as}</div>
<div>: {ipInfo.location.mobile ? '是' : '否'}</div>
<div>: {ipInfo.location.proxy ? '是' : '否'}</div>
<div>: {ipInfo.location.hosting ? '是' : '否'}</div>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{/* 操作按鈕 */}
<div className="flex justify-center">
<Button onClick={fetchIpInfo} className="flex items-center gap-2">
<RefreshCw className="w-4 h-4" />
IP
</Button>
</div>
</div>
)
}

196
app/test/ip-test/page.tsx Normal file
View File

@@ -0,0 +1,196 @@
'use client'
import { useState } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Globe, ExternalLink, Info, CheckCircle, AlertCircle } from 'lucide-react'
export default function IpTestPage() {
const [externalIp, setExternalIp] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const fetchExternalIp = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch('https://api.ipify.org?format=json')
if (!response.ok) {
throw new Error('無法獲取外部IP')
}
const data = await response.json()
setExternalIp(data.ip)
} catch (error) {
console.error('Error fetching external IP:', error)
setError('無法獲取外部IP地址')
} finally {
setLoading(false)
}
}
return (
<div className="container mx-auto p-6 max-w-4xl space-y-6">
<div className="text-center">
<h1 className="text-3xl font-bold mb-2">IP </h1>
<p className="text-muted-foreground">IP地址</p>
</div>
{/* 說明 */}
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>
<div className="font-medium mb-2"> 127.0.0.1</div>
<div className="text-sm space-y-1">
<div> </div>
<div> </div>
<div> IP檢測會更準確</div>
<div> 使IP</div>
</div>
</AlertDescription>
</Alert>
{/* 外部IP檢測 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Globe className="w-5 h-5" />
IP檢測
</CardTitle>
<CardDescription>
IP地址
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<Button
onClick={fetchExternalIp}
disabled={loading}
className="flex items-center gap-2"
>
<ExternalLink className="w-4 h-4" />
{loading ? '檢測中...' : '檢測真實IP'}
</Button>
{externalIp && (
<div className="flex items-center gap-2">
<span className="font-medium">IP:</span>
<Badge variant="default" className="flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
{externalIp}
</Badge>
</div>
)}
</div>
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="text-sm text-muted-foreground">
<p>IP地址就是你的真實公網IP</p>
<ul className="list-disc list-inside mt-2 space-y-1">
<li>IP白名單</li>
<li>IP檢測功能</li>
<li></li>
</ul>
</div>
</CardContent>
</Card>
{/* 測試方法 */}
<Card>
<CardHeader>
<CardTitle>IP的方法</CardTitle>
<CardDescription>IP檢測功能的方法</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<div>
<h4 className="font-medium mb-2">1. 使 ngrok </h4>
<div className="text-sm text-muted-foreground space-y-1">
<p> ngrok: <code className="bg-muted px-1 rounded">npm install -g ngrok</code></p>
<p> : <code className="bg-muted px-1 rounded">ngrok http 3000</code></p>
<p> 使 ngrok URL訪問你的應用</p>
<p> IP檢測功能</p>
</div>
</div>
<div>
<h4 className="font-medium mb-2">2. </h4>
<div className="text-sm text-muted-foreground space-y-1">
<p> VercelNetlify </p>
<p> IP檢測會更準確</p>
<p> IP白名單功能</p>
</div>
</div>
<div>
<h4 className="font-medium mb-2">3. 使</h4>
<div className="text-sm text-muted-foreground space-y-1">
<p> Nginx Apache </p>
<p> IP </p>
<p> IP檢測</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 配置建議 */}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2"></h4>
<div className="text-sm text-muted-foreground">
<p> <code className="bg-muted px-1 rounded">.env.local</code> </p>
<pre className="bg-muted p-2 rounded mt-2 text-xs">
{`# 禁用IP白名單檢查
ENABLE_IP_WHITELIST=false
# 或者允許本地IP
ALLOWED_IPS=127.0.0.1,192.168.1.0/24`}
</pre>
</div>
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<div className="text-sm text-muted-foreground">
<p></p>
<pre className="bg-muted p-2 rounded mt-2 text-xs">
{`# 啟用IP白名單
ENABLE_IP_WHITELIST=true
# 設置允許的IP地址
ALLOWED_IPS=你的真實IP地址,其他允許的IP`}
</pre>
</div>
</div>
</div>
</CardContent>
</Card>
{/* 快速連結 */}
<div className="flex justify-center gap-4">
<Button variant="outline" onClick={() => window.open('/test/ip-debug', '_blank')}>
<Globe className="w-4 h-4 mr-2" />
IP調試
</Button>
<Button variant="outline" onClick={() => window.open('/api/ip', '_blank')}>
<ExternalLink className="w-4 h-4 mr-2" />
IP API
</Button>
</div>
</div>
)
}