🛑 第十二章:Anti-Cheat 防作弊機制:定位驗證與防護
在上一章中,我們寫出了能承受萬人同時打卡的 FastAPI 極速後台。 但是,系統再快,如果資料是假的,那這個系統就毫無價值。
考勤系統的終極死穴就是:「代打卡」與「不在場打卡」。 我們已經透過 LIFF 與 Line 的 JWT Token 解決了「代打卡」的問題 (因為你必須用自己的 Line 帳號登入)。 現在,我們要解決「不在場打卡」。 想像一下:一個員工早上 08:55 還在家裡吃早餐,他打開你的 Line 打卡網頁,按下了「上班打卡」。系統收到了正確的 JWT,把紀錄寫入資料庫。 到了月底,老闆發現這個員工每天都遲到,但系統卻顯示他全勤!老闆絕對會把你這套系統給退貨!
為了解決這個問題,這堂課我們將帶你寫出頂級 SaaS 產品才有的 「Anti-Cheat (防作弊) 引擎」。 我們將結合前端的 Geolocation (地理定位) 與後端的 Haversine 公式 (地球曲率距離演算法),打造一道連駭客都難以突破的防護網。
🌍 實戰 1:前端索取使用者的「GPS 經緯度」
要防作弊,第一步就是我們必須知道員工在按下打卡按鈕的「那個瞬間」,他到底站在地球的哪個座標上。
現代的 HTML5 提供了非常強大的 navigator.geolocation API。
💡 Vibe Prompt 實戰 1:優雅地索取並等待 GPS 授權
很多工程師會把索取 GPS 的程式碼寫得很暴力,導致使用者拒絕授權後,網頁直接當機。 在 Vibe Coding 中,我們要教 AI 寫出體驗極佳的非同步等待與錯誤處理。
[!IMPORTANT] 請複製以下 Prompt 傳送給 AI (這是給 React 前端的):
在我的打卡網頁中,我需要在使用者按下「打卡」按鈕時,先取得他目前手機的 GPS 經緯度。請幫我寫一個 async 函數:getCurrentLocation()。需求:1. 使用 HTML5 navigator.geolocation.getCurrentPosition。2. 將這個 callback-based 的舊 API,用 Promise 包裝成可以被 await 的現代寫法。3. 設定高精度模式 (enableHighAccuracy: true),並設定 timeout 為 5 秒。4. 如果成功,回傳包含 lat 和 lng 的物件。5. 如果使用者拒絕授權或逾時,拋出明確的中文錯誤訊息 (例如:"請開啟手機的定位服務才能打卡")。
🤖 AI 的高階 Promise 封裝術:
// geolocation.js
export const getCurrentLocation = () => {
// 將老舊的 Callback 語法包裝成現代的 Promise,讓外面可以使用 await!
return new Promise((resolve, reject) => {
// 檢查使用者的瀏覽器是否支援定位 (超舊手機防呆)
if (!navigator.geolocation) {
reject(new Error("您的瀏覽器或裝置不支援定位功能。"));
return;
}
// 開始抓取定位!
navigator.geolocation.getCurrentPosition(
// 成功時的 Callback
(position) => {
resolve({
lat: position.coords.latitude,
lng: position.coords.longitude,
// accuracy 是定位誤差範圍 (公尺),我們等等防作弊會用到!
accuracy: position.coords.accuracy
});
},
// 失敗時的 Callback
(error) => {
// AI 幫你把錯誤代碼翻譯成了人類看得懂的中文
switch (error.code) {
case error.PERMISSION_DENIED:
reject(new Error("打卡失敗:您拒絕了定位授權。請到系統設定開啟網頁定位權限。"));
break;
case error.POSITION_UNAVAILABLE:
reject(new Error("打卡失敗:目前無法獲取您的 GPS 訊號,請走到室外或窗邊再試。"));
break;
case error.TIMEOUT:
reject(new Error("打卡失敗:獲取定位逾時 (超過 5 秒),請確認網路連線。"));
break;
default:
reject(new Error("打卡失敗:發生未知的定位錯誤。"));
break;
}
},
// 第三個參數:設定檔
{
enableHighAccuracy: true, // 強制手機開啟 GPS 晶片定位 (比較耗電,但準確度高)
timeout: 5000, // 最多只等 5 秒,不然員工會以為當機了
maximumAge: 0 // 拒絕使用快取的舊位置,強制抓取最新位置!
}
);
});
};
📡 實戰 2:後端接收座標與「哈弗辛公式 (Haversine)」
現在,前端把座標夾帶在 API 裡面傳給了我們的 FastAPI 後端。
假設前端傳來:{"lat": 25.0339, "lng": 121.5644} (台北 101)。
後端資料庫裡面設定的「公司打卡中心點」也是 {"lat": 25.0338, "lng": 121.5645}。
這兩個座標看起來很近,但到底差了幾公尺? 你不能把緯度減緯度、經度減經度,然後用畢氏定理去算!因為地球是圓的! 經緯度越靠近南北極,它們之間的物理距離就越短。 要計算真實的公尺數,我們必須使用地圖學中最著名的數學公式:Haversine Formula (哈弗辛公式)。
💡 Vibe Prompt 實戰 2:指揮 AI 寫出曲率演算法
這種可怕的三角函數,自己寫絕對會寫錯。直接讓 AI 幫你寫出完美無瑕的 Python 模組!
[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:
在我的 FastAPI 後端中,我需要判斷員工打卡的經緯度,距離公司設定的中心點有多遠。請幫我寫一個 Python 函數 calculate_distance_meters(lat1, lon1, lat2, lon2)。需求:1. 使用著名的 Haversine 公式 (考慮地球曲率)。2. 地球半徑請設定為 6371 公里。3. 請使用 math 套件進行三角函數運算 (radians, sin, cos, atan2, sqrt)。4. 最終回傳一個整數 (公尺, meters)。請給我乾淨的程式碼與中文註解。
🤖 AI 的學霸級數學演算法:
import math
def calculate_distance_meters(lat1: float, lon1: float, lat2: float, lon2: float) -> int:
"""
使用 Haversine 公式計算地球上兩點之間的真實直線距離 (公尺)。
這個演算法完美處理了地球的曲面特性,是業界計算 GPS 距離的黃金標準!
"""
# 地球平均半徑 (公里)
R = 6371.0
# 將經緯度從「度數」轉換為三角函數需要的「弧度 (radians)」
lat1_rad = math.radians(lat1)
lon1_rad = math.radians(lon1)
lat2_rad = math.radians(lat2)
lon2_rad = math.radians(lon2)
# 計算經緯度的差異
dlon = lon2_rad - lon1_rad
dlat = lat2_rad - lat1_rad
# 哈弗辛公式的魔法數學運算
a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
# 計算出距離 (公里)
distance_km = R * c
# 將公里轉換為公尺,並四捨五入為整數
distance_meters = int(distance_km * 1000)
return distance_meters
# 【測試範例】:
# 假設公司在台北 101 (25.033964, 121.564468)
# 員工在市政府捷運站 (25.040857, 121.564977)
# print(calculate_distance_meters(25.033964, 121.564468, 25.040857, 121.564977))
# 輸出:768 (公尺) -> 系統判定:距離太遠,打卡失敗!
🚫 實戰 3:建立 Anti-Cheat 驗證閘門 (Validation Logic)
有了尺 (Haversine),我們現在要在打卡 API 中設立一道閘門。 如果員工的距離超過我們設定的「容許半徑 (Radius)」,就無情地拒絕他!
💡 Vibe Prompt 實戰 3:結合 FastApi 路由與距離驗證
[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:
在我的 FastAPI 中,我有一個 /api/punch 路由。現在前端除了 action 之外,還會傳送 lat 和 lng。請幫我更新 PunchRequest 的 Pydantic 模型。然後在路由中加入 Anti-Cheat 邏輯:1. 假設公司的經緯度為 COMPANY_LAT = 25.0339, COMPANY_LNG = 121.5644。2. 設定容許的打卡半徑為 ALLOWED_RADIUS = 100 (公尺)。3. 呼叫剛才寫好的 calculate_distance_meters 算出距離。4. 如果距離大於 100 公尺,拋出 HTTPException 403 (Forbidden),並回傳錯誤訊息:「打卡失敗!您目前距離公司 [X] 公尺,請走到公司範圍內再試。」請給我整合後的完整程式碼。
🤖 AI 的無情防護閘門:
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
# 公司總部的定海神針 (真實環境會存在資料庫裡)
COMPANY_LAT = 25.0339
COMPANY_LNG = 121.5644
ALLOWED_RADIUS = 100 # 容許 100 公尺內的誤差
class PunchRequest(BaseModel):
action: str
lat: float # 新增:接收前端傳來的緯度
lng: float # 新增:接收前端傳來的經度
@router.post("/api/punch")
async def handle_punch(request_data: PunchRequest, current_user: dict = Depends(verify_line_token)):
# 🛑 Anti-Cheat 防護層 1:距離驗證
distance = calculate_distance_meters(
COMPANY_LAT, COMPANY_LNG,
request_data.lat, request_data.lng
)
print(f"📡 距離偵測:員工 {current_user['name']} 距離總部 {distance} 公尺。")
# 如果員工在 100 公尺以外,閘門立刻落下!
if distance > ALLOWED_RADIUS:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"打卡失敗!您距離打卡範圍還有 {distance - ALLOWED_RADIUS} 公尺。請靠近公司後重試!"
)
# 如果通過了,才進入上一章寫好的資料庫寫入邏輯
# await record_punch_to_db(...)
return {"status": "success", "message": "完美命中!打卡成功。"}
😈 實戰 4:駭客的逆襲 —— 假 GPS 防護 (Fake Location)
老闆很高興,你的系統上線了。 一個禮拜後,老闆把你叫進辦公室:「王小明今天根本沒來上班,但他打卡成功了!這到底怎麼回事?」
你查了 Log 發現,王小明傳給伺服器的 lat 和 lng 真的是公司的完美經緯度,甚至連小數點後 6 位都一模一樣!
原來,王小明下載了 Android 上的 「Fake GPS (虛擬定位)」 APP,把手機的定位偽裝成在公司!
這就是高階後端工程師必須面對的魔高一丈。
虛擬定位是作業系統層級的欺騙,網頁的 navigator.geolocation 根本分不出來。
但... 狐狸總會露出尾巴!
💡 Vibe Prompt 實戰 4:打造揪出 Fake GPS 的 AI 邏輯
虛擬定位最大的破綻就是:「它太完美了」。
真實的 GPS 訊號因為受到大樓遮蔽、大氣層折射,定位精準度 (Accuracy) 通常在 15~60 公尺之間跳動。而且你站著不動,經緯度的小數點最後幾位也會不斷漂移。
而那些廉價的 Fake GPS APP,通常會回傳一個精度為 0 或是 極度不合理 的固定值,且經緯度永遠是同一個數字!
我們請 AI 幫我們寫一套高階的特徵偵測系統!
[!IMPORTANT] 請複製以下 Prompt 傳送給 AI:
有員工使用了 Fake GPS (虛擬定位) 欺騙我的打卡系統。前端除了傳送 lat 和 lng 之外,我還請前端傳送了 accuracy (定位精準度,公尺)。請在我的 FastAPI 路由中,加入高階的 Fake GPS 防禦邏輯 (Heuristic Detection)。偵測條件如下:1. 如果 accuracy 小於 5 (公尺):人類的手機 GPS 很難準到 5 公尺以內,這極有可能是軟體寫死的完美座標。直接拒絕並判定為作弊嫌疑。2. 如果 accuracy 大於 1000 (公尺):這代表他可能在地下室或是訊號極差,定位飄到幾公里外了,我們不能信任這個座標,請他去室外再打卡。3. 請更新 PunchRequest 接收 accuracy,並在路由開頭加入這些防呆攔截。
🤖 AI 的終極反作弊雷達:
class PunchRequest(BaseModel):
action: str
lat: float
lng: float
accuracy: float # 新增:雷達精準度 (前端的 position.coords.accuracy)
@router.post("/api/punch")
async def handle_punch(request_data: PunchRequest, current_user: dict = Depends(verify_line_token)):
# 🛑 Anti-Cheat 終極防護:虛擬定位特徵分析
# 特徵 1:精準度異常完美 (通常 Fake GPS 會回傳極小的數字)
if request_data.accuracy < 5.0:
print(f"⚠️ [防作弊警報] {current_user['name']} 疑似使用 Fake GPS,精準度竟然高達 {request_data.accuracy}m!")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="系統偵測到異常的定位訊號。請關閉所有虛擬定位軟體或 VPN 後重試。"
)
# 特徵 2:訊號極度微弱 (定位飄移,不能算數)
if request_data.accuracy > 500.0:
print(f"⚠️ [訊號不良] {current_user['name']} 的定位精度為 {request_data.accuracy}m,無法採信。")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="您的 GPS 訊號過於微弱,定位範圍太廣。請移動至室外或有窗戶的地方重新打卡。"
)
# ... 接下來才進入實體的距離驗證 (calculate_distance_meters) ...
🔍 深度解析:
這套 accuracy 防護網,雖然不能 100% 阻擋最高級的付費版修改器,但已經足以攔下 95% 用免費外掛想偷懶的員工了。
這就是「資訊戰」。我們在系統設計的底層,偷偷埋入了對「資料合理性」的檢驗,這比起單純寫 CRUD (新增刪除修改) 的程式碼,含金量高出好幾個層次。
⚡ 實戰 5:時間旅行者的防護 (NTP 攻擊)
防作弊的最後一個漏洞:如果員工把手機開飛航模式,手動把手機系統時間調回早上 08:50,然後連上 Wi-Fi 瞬間打卡?
前端的 new Date() 會給出 08:50 的時間!
這就是為什麼在我們的第十一章原始碼中,我們寫入資料庫的時間是:
datetime.utcnow().isoformat()
這行程式碼是在**「後端伺服器」**上執行的。
不論員工的手機時間被改成什麼年份,只要請求一到達 FastAPI 伺服器,我們只信任「伺服器的國際標準時間」。這徹底封殺了「時間修改」的作弊可能!
✅ 本章總結與終極架構心法
恭喜你,完成了這門長達 6000 字的最高殿堂:Anti-Cheat (防作弊系統) 實戰。
你的 Line 考勤系統現在擁有以下三道無敵的防線:
- 身分防線 (JWT):解決「代打卡」。駭客無法偽造 Line 官方發出的通行證。
- 空間防線 (Haversine + Accuracy):解決「不在場打卡」。用地球曲率演算法精準測量距離,並用精度特徵辨識 Fake GPS。
- 時間防線 (Server-Side Time):解決「時光倒流」。絕對不信任手機端的時間,一切以伺服器收到請求的那一瞬間為準。
當你帶著這套系統去跟公司提案時,你賣的不是「一個漂亮的按鈕」,你賣的是「一套能幫公司每年省下幾百萬薪水漏洞的管理神器」。
這就是為什麼 Vibe Coding 這麼強大。如果你不懂微積分和三角函數,你一輩子都寫不出 Haversine 公式。 但現在,你只需要向 AI 描述「我要防作弊」、「我要算距離」,AI 就會為你產出這些學霸級的演算法。
打卡的問題解決了,但是我們如何讓系統在每個月底的最後一天,自動結算全公司的遲到紀錄,並自動發信給老闆呢? 下一章:第十三章:Python Cron Job 與自動化結算任務,我們將教你如何讓這隻 FastAPI 巨獸,不僅能接請求,還能設定「定時鬧鐘」,完全取代人資部門的月底噩夢!我們下堂課見!