ป้องกันการกรอกข้อมูลเล่นๆ หรือสแปมในฟอร์ม

 



มีหลายวิธีที่ใช้ป้องกันการกรอกข้อมูลเล่นๆ หรือสแปมในฟอร์มได้ครับ แนะนำให้ใช้ร่วมกันหลายวิธี เพราะแต่ละวิธีป้องกันคนละแบบ:

1. CAPTCHA / reCAPTCHA

  • Google reCAPTCHA v3 (ทำงานเงียบๆ ไม่ต้องให้ผู้ใช้คลิกยืนยัน) หรือ v2 (ติ๊ก "ฉันไม่ใช่โปรแกรมอัตโนมัติ")
  • hCaptcha หรือ Cloudflare Turnstile เป็นทางเลือกที่โหลดเร็วกว่าและเป็นมิตรกับความเป็นส่วนตัวมากกว่า

2. Honeypot field

  • ใส่ input field ที่ซ่อนไว้ด้วย CSS (เช่น display:none) คนจริงจะมองไม่เห็นและไม่กรอก แต่บอทที่กรอกทุกช่องอัตโนมัติจะติดกับ ถ้าฟิลด์นี้มีค่า = ปฏิเสธการส่งทันที
  • ข้อดีคือไม่รบกวนผู้ใช้เลย

3. Rate limiting / Throttling

  • จำกัดจำนวนครั้งที่ IP หรือ session เดียวกันส่งฟอร์มได้ในช่วงเวลาหนึ่ง (เช่น ส่งได้ 1 ครั้งต่อ 1 นาที)

4. ตรวจสอบเวลาในการกรอกฟอร์ม (Time-based check)

  • ถ้าฟอร์มถูกส่งเร็วเกินไป (เช่นภายใน 1-2 วินาทีหลังโหลดหน้า) แสดงว่าน่าจะเป็นบอท ไม่ใช่คนจริง

5. ยืนยันตัวตนผ่านอีเมล/เบอร์โทร

  • ส่งลิงก์ยืนยัน (double opt-in) หรือ OTP ก่อนที่ข้อมูลจะถูกบันทึกจริง

6. ตรวจสอบข้อมูลฝั่ง Server (Server-side validation)

  • อย่าเชื่อ validation ฝั่ง client อย่างเดียว ต้องตรวจซ้ำที่ server เช่น รูปแบบอีเมล เบอร์โทร ความยาวข้อความ

7. บล็อกคำ/รูปแบบที่น่าสงสัย

  • กรองคำหยาบ ลิงก์สแปม หรือรูปแบบข้อความซ้ำๆ (เช่นใช้ Akismet ถ้าเป็น WordPress)

8. จำกัดสิทธิ์ตาม IP/ประเทศ (ถ้าจำเป็น)

  • ถ้าธุรกิจมีกลุ่มเป้าหมายเฉพาะพื้นที่ อาจบล็อก IP จากประเทศที่ไม่เกี่ยวข้อง

Code.gs

/**
 * Code.gs
 * รับข้อมูลจากฟอร์ม HTML แล้วตรวจสอบสแปมก่อนบันทึกลง Google Sheet
 *
 * วิธีติดตั้ง:
 * 1. เปิด Google Sheet ที่จะใช้เก็บข้อมูล -> Extensions > Apps Script
 * 2. วางโค้ดนี้แทนไฟล์ Code.gs เดิม
 * 3. สร้างชีทชื่อ "Responses" (หรือแก้ชื่อในโค้ดให้ตรงกับชีทที่มี)
 * 4. Deploy > New deployment > Web app
 *    - Execute as: Me
 *    - Who has access: Anyone
 * 5. คัดลอก URL ที่ได้ไปใส่ในตัวแปร GAS_URL ของไฟล์ index.html
 */

const SHEET_NAME = 'Responses';
const RATE_LIMIT_SECONDS = 60;      // 1 client id ส่งได้ 1 ครั้งต่อช่วงเวลานี้
const MIN_FILL_SECONDS = 3;         // ต้องใช้เวลากรอกฟอร์มอย่างน้อยเท่านี้ (วินาที)
const MAX_MESSAGE_LENGTH = 2000;

function doPost(e) {
  const output = ContentService.createTextOutput();
  output.setMimeType(ContentService.MimeType.JSON);

  try {
    const data = JSON.parse(e.postData.contents);

    // ---- 1) เช็ค Honeypot ----
    if (data.website && data.website.trim() !== '') {
      // เป็นบอท: ปฏิเสธเงียบๆ (ไม่ต้องบอกเหตุผลตรงๆ ให้บอทรู้)
      return output.setContent(JSON.stringify({ status: 'success' }));
    }

    // ---- 2) เช็คเวลากรอกฟอร์ม ----
    const elapsed = Number(data.elapsedSeconds || 0);
    if (elapsed < MIN_FILL_SECONDS) {
      return output.setContent(JSON.stringify({ status: 'rejected' }));
    }

    // ---- 3) ตรวจสอบข้อมูลพื้นฐาน ----
    const name = sanitize(data.name);
    const email = sanitize(data.email);
    const message = sanitize(data.message);

    if (!name || !email || !message) {
      return output.setContent(JSON.stringify({ status: 'invalid_data' }));
    }

    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailPattern.test(email)) {
      return output.setContent(JSON.stringify({ status: 'invalid_email' }));
    }

    if (message.length > MAX_MESSAGE_LENGTH) {
      return output.setContent(JSON.stringify({ status: 'message_too_long' }));
    }

    // ---- 4) Rate limiting ด้วย CacheService ----
    const clientId = String(data.clientId || 'unknown');
    const cache = CacheService.getScriptCache();
    const cacheKey = 'submit_' + clientId;

    if (cache.get(cacheKey)) {
      return output.setContent(JSON.stringify({ status: 'too_many_requests' }));
    }
    cache.put(cacheKey, '1', RATE_LIMIT_SECONDS);

    // ---- 5) กรองคำ/ลิงก์สแปมเบื้องต้น (ปรับ list ได้ตามต้องการ) ----
    if (containsSpamPattern(message) || containsSpamPattern(name)) {
      return output.setContent(JSON.stringify({ status: 'rejected' }));
    }

    // ---- 6) บันทึกลง Sheet ----
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
    if (!sheet) {
      throw new Error('ไม่พบชีทชื่อ ' + SHEET_NAME);
    }
    sheet.appendRow([new Date(), name, email, message]);

    return output.setContent(JSON.stringify({ status: 'success' }));

  } catch (err) {
    return output.setContent(JSON.stringify({ status: 'error', message: err.message }));
  }
}

/**
 * ทำความสะอาดข้อความเบื้องต้น: ตัดช่องว่างหัวท้าย, จำกัดความยาว, ตัด tag HTML พื้นฐาน
 */
function sanitize(value) {
  if (!value) return '';
  return String(value)
    .trim()
    .replace(/<[^>]*>/g, '') // ตัด HTML tag ป้องกัน injection
    .slice(0, 5000);
}

/**
 * ตรวจจับรูปแบบที่มักพบในสแปม เช่น จำนวนลิงก์เยอะผิดปกติ
 * ปรับแต่ง/เพิ่มคำต้องห้ามได้ตามการใช้งานจริง
 */
function containsSpamPattern(text) {
  if (!text) return false;

  // นับจำนวนลิงก์ในข้อความ ถ้ามากกว่า 2 ลิงก์ ถือว่าน่าสงสัย
  const linkMatches = text.match(/https?:\/\/\S+/g) || [];
  if (linkMatches.length > 2) return true;

  // ตัวอย่างคำต้องห้าม (ปรับตามบริบทจริง)
  const bannedWords = ['viagra', 'casino', 'crypto airdrop', 'bit.ly'];
  const lower = text.toLowerCase();
  return bannedWords.some(function (w) { return lower.indexOf(w) !== -1; });
}


    


index.html

<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>แบบฟอร์มติดต่อ</title>
<style>
  * { box-sizing: border-box; }
  body {
    font-family: 'Segoe UI', Tahoma, sans-serif;
    background: #f4f6f8;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
    padding: 20px;
  }
  .form-container {
    background: #fff;
    padding: 32px;
    border-radius: 12px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.08);
    width: 100%;
    max-width: 420px;
  }
  h2 {
    margin-top: 0;
    color: #222;
    font-size: 22px;
  }
  label {
    display: block;
    margin-top: 16px;
    margin-bottom: 6px;
    font-size: 14px;
    color: #444;
    font-weight: 600;
  }
  input, textarea {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid #d5d9de;
    border-radius: 8px;
    font-size: 14px;
    font-family: inherit;
    transition: border-color 0.2s;
  }
  input:focus, textarea:focus {
    outline: none;
    border-color: #4a7dff;
  }
  textarea { resize: vertical; min-height: 90px; }

  /* Honeypot: ซ่อนจากสายตาคน แต่บอทที่กรอกทุกช่องจะยังเห็นและกรอก */
  .hp-field {
    position: absolute;
    left: -9999px;
    top: -9999px;
    height: 0;
    width: 0;
    overflow: hidden;
  }

  button {
    margin-top: 22px;
    width: 100%;
    padding: 12px;
    background: #4a7dff;
    color: #fff;
    border: none;
    border-radius: 8px;
    font-size: 15px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.2s;
  }
  button:hover { background: #3a67e0; }
  button:disabled { background: #a9bce8; cursor: not-allowed; }

  #statusMsg {
    margin-top: 14px;
    font-size: 14px;
    text-align: center;
    min-height: 20px;
  }
  .success { color: #1a8b3f; }
  .error { color: #d92d20; }
</style>
</head>
<body>

<div class="form-container">
  <h2>ติดต่อเรา</h2>
  <form id="contactForm">
    <label for="name">ชื่อ</label>
    <input type="text" id="name" name="name" required>

    <label for="email">อีเมล</label>
    <input type="email" id="email" name="email" required>

    <label for="message">ข้อความ</label>
    <textarea id="message" name="message" required></textarea>

    <!-- Honeypot field: ห้ามใส่ label ให้เห็น, บอทมักกรอกทุกช่องที่เจอ -->
    <div class="hp-field" aria-hidden="true">
      <label for="website">Website</label>
      <input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
    </div>

    <button type="submit" id="submitBtn">ส่งข้อความ</button>
    <div id="statusMsg"></div>
  </form>
</div>

<script>
  // ===== ตั้งค่า =====
  const GAS_URL = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec'; // เปลี่ยนเป็น URL ของคุณ
  const MIN_FILL_SECONDS = 3; // เวลาขั้นต่ำที่ต้องใช้กรอกฟอร์ม (กันบอทที่ submit เร็วผิดปกติ)

  const form = document.getElementById('contactForm');
  const statusMsg = document.getElementById('statusMsg');
  const submitBtn = document.getElementById('submitBtn');
  const formLoadTime = Date.now();

  // สร้าง client id แบบง่ายๆ เก็บใน sessionStorage เพื่อใช้ประกอบการ rate-limit ฝั่ง server
  function getClientId() {
    let id = sessionStorage.getItem('cid');
    if (!id) {
      id = 'c_' + Date.now() + '_' + Math.random().toString(36).slice(2);
      sessionStorage.setItem('cid', id);
    }
    return id;
  }

  function showStatus(text, type) {
    statusMsg.textContent = text;
    statusMsg.className = type;
  }

  form.addEventListener('submit', async function (e) {
    e.preventDefault();
    showStatus('', '');

    // --- 1) เช็ค honeypot ---
    const honeypotValue = form.website.value.trim();
    if (honeypotValue) {
      // เป็นบอทแน่นอน: แกล้งแสดงว่าสำเร็จ เพื่อไม่ให้บอทรู้ว่าโดนจับได้ แต่ไม่ส่งข้อมูลจริง
      showStatus('ส่งข้อความเรียบร้อยแล้ว ขอบคุณครับ', 'success');
      form.reset();
      return;
    }

    // --- 2) เช็คเวลาในการกรอกฟอร์ม ---
    const elapsedSeconds = (Date.now() - formLoadTime) / 1000;
    if (elapsedSeconds < MIN_FILL_SECONDS) {
      showStatus('กรุณาลองใหม่อีกครั้ง', 'error');
      return;
    }

    // --- 3) เช็ครูปแบบข้อมูลเบื้องต้นฝั่ง client ---
    const name = form.name.value.trim();
    const email = form.email.value.trim();
    const message = form.message.value.trim();
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

    if (!name || !email || !message) {
      showStatus('กรุณากรอกข้อมูลให้ครบทุกช่อง', 'error');
      return;
    }
    if (!emailPattern.test(email)) {
      showStatus('รูปแบบอีเมลไม่ถูกต้อง', 'error');
      return;
    }
    if (message.length > 2000) {
      showStatus('ข้อความยาวเกินไป', 'error');
      return;
    }

    // --- 4) ส่งข้อมูลไปยัง GAS ---
    submitBtn.disabled = true;
    submitBtn.textContent = 'กำลังส่ง...';

    const payload = {
      name: name,
      email: email,
      message: message,
      website: honeypotValue,        // ส่งไปด้วยเพื่อให้ server เช็คซ้ำ
      clientId: getClientId(),
      elapsedSeconds: elapsedSeconds
    };

    try {
      const response = await fetch(GAS_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'text/plain;charset=utf-8' }, // ใช้ text/plain เพื่อเลี่ยงปัญหา CORS preflight กับ GAS
        body: JSON.stringify(payload)
      });
      const result = await response.json();

      if (result.status === 'success') {
        showStatus('ส่งข้อความเรียบร้อยแล้ว ขอบคุณครับ', 'success');
        form.reset();
      } else if (result.status === 'too_many_requests') {
        showStatus('กรุณารอสักครู่ก่อนส่งข้อความใหม่', 'error');
      } else if (result.status === 'invalid_email') {
        showStatus('รูปแบบอีเมลไม่ถูกต้อง', 'error');
      } else {
        showStatus('เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง', 'error');
      }
    } catch (err) {
      showStatus('ไม่สามารถส่งข้อมูลได้ กรุณาลองใหม่', 'error');
    } finally {
      submitBtn.disabled = false;
      submitBtn.textContent = 'ส่งข้อความ';
    }
  });
</script>

</body>
</html>


    
แสดงความคิดเห็น (0)
ใหม่กว่า เก่ากว่า