🛑 第十二章: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 發現,王小明傳給伺服器的 latlng 真的是公司的完美經緯度,甚至連小數點後 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 考勤系統現在擁有以下三道無敵的防線:

  1. 身分防線 (JWT):解決「代打卡」。駭客無法偽造 Line 官方發出的通行證。
  2. 空間防線 (Haversine + Accuracy):解決「不在場打卡」。用地球曲率演算法精準測量距離,並用精度特徵辨識 Fake GPS。
  3. 時間防線 (Server-Side Time):解決「時光倒流」。絕對不信任手機端的時間,一切以伺服器收到請求的那一瞬間為準。

當你帶著這套系統去跟公司提案時,你賣的不是「一個漂亮的按鈕」,你賣的是「一套能幫公司每年省下幾百萬薪水漏洞的管理神器」。

這就是為什麼 Vibe Coding 這麼強大。如果你不懂微積分和三角函數,你一輩子都寫不出 Haversine 公式。 但現在,你只需要向 AI 描述「我要防作弊」、「我要算距離」,AI 就會為你產出這些學霸級的演算法。

打卡的問題解決了,但是我們如何讓系統在每個月底的最後一天,自動結算全公司的遲到紀錄,並自動發信給老闆呢? 下一章:第十三章:Python Cron Job 與自動化結算任務,我們將教你如何讓這隻 FastAPI 巨獸,不僅能接請求,還能設定「定時鬧鐘」,完全取代人資部門的月底噩夢!我們下堂課見!

解鎖完整教學內容

本章為付費內容。加入專案即可解鎖超過 5000 字的深度解析,包含 10 個以上神級 Prompt 與真實 Source Code 範例!