สอนสร้างหน้าเข้าสู่ระบบด้วย React ตั้งแต่ฟอร์มจนถึงการป้องกันหน้า

สอนสร้างหน้าเข้าสู่ระบบด้วย React ตั้งแต่ฟอร์มจนถึงการป้องกันหน้า

หน้าเข้าสู่ระบบเป็นหนึ่งในหน้าที่เว็บเกือบทุกเว็บต้องมี บทความนี้สร้างหน้าเข้าสู่ระบบด้วย React ทีละขั้น ตั้งแต่โครงสร้างฟอร์ม การควบคุมช่องกรอก การตรวจความถูกต้อง การจำลองการเข้าสู่ระบบ การล็อกชั่วคราวเมื่อผิดหลายครั้ง ไปจนถึง Context และหน้าที่ป้องกันไม่ให้คนที่ยังไม่เข้าสู่ระบบเห็นข้อมูล ทุกตัวอย่างเป็นโค้ดที่รันแล้วและผลลัพธ์ที่แสดงคือผลจริง

สิ่งที่ต้องเตรียม

ต้องมี Node.js 18 ขึ้นไป พื้นฐาน React และ JavaScript ตัวอย่างใช้ React 18.3.1 และรันด้วยตัวทดสอบของบทความที่จำลองการคลิกในเบราว์เซอร์ด้วย jsdom หมายเหตุสำคัญ: การตรวจรหัสผ่านในเบราว์เซอร์เป็นเพียงตัวอย่างสำหรับเรียนรู้ ระบบจริงต้องตรวจรหัสผ่านที่เซิร์ฟเวอร์เสมอ เก็บรหัสผ่านแบบแฮช และใช้ HTTPS

ภาพประกอบ

ลำดับการทำงานของการเข้าสู่ระบบ: กรอกข้อมูล ตรวจรูปแบบ ส่งไปตรวจสอบ บันทึกสถานะ และป้องกันหน้า
ลำดับการทำงานของการเข้าสู่ระบบ: กรอกข้อมูล ตรวจรูปแบบ ส่งไปตรวจสอบ บันทึกสถานะ และป้องกันหน้า

ตัวอย่างที่ 1: โครงสร้างฟอร์มเข้าสู่ระบบ

เริ่มจากฟอร์มที่มีช่องอีเมล ช่องรหัสผ่าน และปุ่มส่ง ใช้ renderToStaticMarkup แสดงโครงสร้าง HTML ที่ได้จริง

import React from "react";

export function App() {
  return (
    <form>
      <h2>เข้าสู่ระบบ</h2>
      <label>อีเมล<input type="email" name="email" /></label>
      <label>รหัสผ่าน<input type="password" name="password" /></label>
      <button type="submit">เข้าสู่ระบบ</button>
    </form>
  );
}

ผลลัพธ์ที่ได้จากการรันจริง

<form><h2>เข้าสู่ระบบ</h2><label>อีเมล<input type="email" name="email"/></label><label>รหัสผ่าน<input type="password" name="password"/></label><button type="submit">เข้าสู่ระบบ</button></form>

ตัวอย่างที่ 2: แสดงหรือซ่อนรหัสผ่าน

เก็บสถานะการแสดงรหัสผ่านไว้ใน state แล้วสลับประเภทของ input ระหว่าง password กับ text เมื่อคลิกปุ่ม

1. ปุ่มแสดงรหัสผ่านคลิกหนึ่งครั้ง จากปิดเป็นเปิด

import React, { useState } from "react";

export function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <input type={show ? "text" : "password"} value="secret1" readOnly />
      <button id="toggle" onClick={() => setShow(!show)}>แสดงรหัสผ่าน</button>
      <p data-out>แสดงรหัสผ่าน: {show ? "เปิด" : "ปิด"}</p>
    </div>
  );
}

export const steps = ["#toggle"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: แสดงรหัสผ่าน: ปิด
หลังคลิกครั้งที่ 1: แสดงรหัสผ่าน: เปิด

ตัวอย่างที่ 3: ตรวจความถูกต้องของข้อมูล

ฟังก์ชัน validate คืนรายการข้อความผิดพลาด แสดงผลเมื่อผู้ใช้กดส่งฟอร์มเท่านั้น เพื่อไม่รบกวนขณะกรอก

import React, { useState } from "react";

// กติกาการตรวจสอบ: คืนรายการข้อความแจ้งเตือน ถ้าไม่มีข้อผิดพลาดจะได้อาร์เรย์ว่าง
export function validate(email, password) {
  const errors = [];
  if (!email) errors.push("กรอกอีเมล");
  else if (!/^\S+@\S+\.\S+$/.test(email)) errors.push("รูปแบบอีเมลไม่ถูกต้อง");
  if (password.length < 6) errors.push("รหัสผ่านอย่างน้อย 6 ตัว");
  return errors;
}

export function App() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [submitted, setSubmitted] = useState(false);
  const errors = validate(email, password);
  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" onClick={() => setSubmitted(true)}>ส่งฟอร์ม</button>
      <p data-out>{!submitted ? "ยังไม่ส่งฟอร์ม" : errors.length ? errors.join(" | ") : "ไม่มีข้อผิดพลาด"}</p>
    </div>
  );
}

export const steps = ["#submit"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: ยังไม่ส่งฟอร์ม
หลังคลิกครั้งที่ 1: กรอกอีเมล | รหัสผ่านอย่างน้อย 6 ตัว

ตัวอย่างที่ 4: จำลองการเข้าสู่ระบบ

เปรียบเทียบอีเมลและรหัสผ่านกับข้อมูลจำลอง แล้วแสดงข้อความสำเร็จหรือไม่พบบัญชี ตัวอย่างนี้ใช้ค่าเริ่มต้นที่ถูกต้อง

import React, { useState } from "react";

// ตัวอย่างฐานข้อมูลผู้ใช้จำลอง จริง ๆ ต้องตรวจที่เซิร์ฟเวอร์เท่านั้น
const USERS = { "[email protected]": "secret1" };

export function login(email, password) {
  if (USERS[email] === undefined) return { ok: false, message: "ไม่พบบัญชีนี้" };
  if (USERS[email] !== password) return { ok: false, message: "รหัสผ่านไม่ถูกต้อง" };
  return { ok: true, message: "เข้าสู่ระบบสำเร็จ: " + email };
}

export function App() {
  const [email, setEmail] = useState("[email protected]");
  const [password, setPassword] = useState("secret1");
  const [result, setResult] = useState("ยังไม่ได้ลองเข้าสู่ระบบ");
  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" onClick={() => setResult(login(email, password).message)}>เข้าสู่ระบบ</button>
      <p data-out>{result}</p>
    </div>
  );
}

export const steps = ["#submit"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: ยังไม่ได้ลองเข้าสู่ระบบ
หลังคลิกครั้งที่ 1: เข้าสู่ระบบสำเร็จ: [email protected]

ตัวอย่างที่ 5: ล็อกชั่วคราวหลังผิดหลายครั้ง

นับจำนวนครั้งที่รหัสผ่านผิด ปิดปุ่มเมื่อถึงขีดจำกัด เพื่อกันการเดารหัสผ่านซ้ำ ๆ

import React, { useState } from "react";

const MAX_TRIES = 3;

export function App() {
  const [password, setPassword] = useState("wrong");
  const [tries, setTries] = useState(0);
  const [message, setMessage] = useState("");
  const locked = tries >= MAX_TRIES;

  function submit() {
    if (locked) return;
    if (password === "secret1") {
      setMessage("เข้าสู่ระบบสำเร็จ");
      return;
    }
    const next = tries + 1;
    setTries(next);
    setMessage(next >= MAX_TRIES ? "ล็อกชั่วคราว ลองใหม่ภายหลัง" : `รหัสผ่านไม่ถูกต้อง (ครั้งที่ ${next} จาก ${MAX_TRIES})`);
  }

  return (
    <div>
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" disabled={locked} onClick={submit}>เข้าสู่ระบบ</button>
      <p data-out>{message || "พร้อมใช้งาน"}</p>
    </div>
  );
}

export const steps = ["#submit", "#submit", "#submit"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: พร้อมใช้งาน
หลังคลิกครั้งที่ 1: รหัสผ่านไม่ถูกต้อง (ครั้งที่ 1 จาก 3)
หลังคลิกครั้งที่ 2: รหัสผ่านไม่ถูกต้อง (ครั้งที่ 2 จาก 3)
หลังคลิกครั้งที่ 3: ล็อกชั่วคราว ลองใหม่ภายหลัง

ตัวอย่างที่ 6: ตัวเลือกจดจำการเข้าสู่ระบบ

ใช้ checkbox ที่ควบคุมด้วย state และแสดงสถานะให้ผู้ใช้เห็นทันที

import React, { useState } from "react";

export function App() {
  const [remember, setRemember] = useState(false);
  return (
    <div>
      <label>
        <input id="remember" type="checkbox" checked={remember} onChange={e => setRemember(e.target.checked)} />
        จดจำการเข้าสู่ระบบ
      </label>
      <p data-out>จดจำการเข้าสู่ระบบ: {remember ? "เปิด" : "ปิด"}</p>
    </div>
  );
}

export const steps = ["#remember"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: จดจำการเข้าสู่ระบบ: ปิด
หลังคลิกครั้งที่ 1: จดจำการเข้าสู่ระบบ: เปิด

ตัวอย่างที่ 7: เก็บสถานะผู้ใช้ด้วย Context

สร้าง AuthContext เพื่อให้ปุ่มและแถบสถานะเข้าถึงข้อมูลผู้ใช้ได้โดยไม่ต้องส่ง props ผ่านหลายชั้น

import React, { createContext, useContext, useState } from "react";

// เก็บสถานะผู้ใช้ไว้ที่ Context เพื่อให้ทุกส่วนของหน้าเข้าถึงได้โดยไม่ต้องส่ง props ต่อกันหลายชั้น
const AuthContext = createContext(null);

function StatusBar() {
  const { user, login, logout } = useContext(AuthContext);
  return (
    <div>
      <p data-out>สถานะ: {user ? "สวัสดี " + user : "ยังไม่ได้เข้าสู่ระบบ"}</p>
      {user
        ? <button id="logout" onClick={logout}>ออกจากระบบ</button>
        : <button id="login" onClick={() => login("สมชาย")}>เข้าสู่ระบบ</button>}
    </div>
  );
}

export function App() {
  const [user, setUser] = useState(null);
  const value = { user, login: setUser, logout: () => setUser(null) };
  return (
    <AuthContext.Provider value={value}>
      <StatusBar />
    </AuthContext.Provider>
  );
}

export const steps = ["#login", "#logout"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: สถานะ: ยังไม่ได้เข้าสู่ระบบ
หลังคลิกครั้งที่ 1: สถานะ: สวัสดี สมชาย
หลังคลิกครั้งที่ 2: สถานะ: ยังไม่ได้เข้าสู่ระบบ

ตัวอย่างที่ 8: หน้าที่ป้องกันด้วยสถานะผู้ใช้

ถ้ายังไม่มีผู้ใช้จะแสดงข้อความให้เข้าสู่ระบบ ถ้ามีจะแสดงหน้าแดชบอร์ด นี่คือแนวคิดของเส้นทางที่ต้องเข้าสู่ระบบก่อน

1. เข้าสู่ระบบ แล้วออกจากระบบ

import React, { useState } from "react";

// หน้าที่ต้องเข้าสู่ระบบก่อนจึงจะเห็น ถ้ายังไม่มีผู้ใช้จะแสดงข้อความให้เข้าสู่ระบบแทน
function Dashboard({ user }) {
  if (!user) return <p data-out>กรุณาเข้าสู่ระบบก่อน</p>;
  return <p data-out>หน้าแดชบอร์ดของ {user}</p>;
}

export function App() {
  const [user, setUser] = useState(null);
  return (
    <div>
      <button id="login" onClick={() => setUser("สมชาย")}>เข้าสู่ระบบ</button>
      <button id="logout" onClick={() => setUser(null)}>ออกจากระบบ</button>
      <Dashboard user={user} />
    </div>
  );
}

export const steps = ["#login", "#logout"];

ผลลัพธ์ที่ได้จากการรันจริง

ข้อความเริ่มต้น: กรุณาเข้าสู่ระบบก่อน
หลังคลิกครั้งที่ 1: หน้าแดชบอร์ดของ สมชาย
หลังคลิกครั้งที่ 2: กรุณาเข้าสู่ระบบก่อน

ตัวอย่างที่ 9: รวมทุกส่วนเป็นหน้าเข้าสู่ระบบ

หน้าสุดท้ายประกอบจากฟอร์ม ช่องกรอกที่มี autocomplete ที่ถูกต้อง ตัวเลือกจดจำ และลิงก์ลืมรหัสผ่าน

import React from "react";

// หน้าเข้าสู่ระบบแบบสมบูรณ์ที่รวมทุกส่วนไว้ด้วยกัน: ฟอร์ม ช่องกรอก ตัวเลือกจดจำ และลิงก์ลืมรหัสผ่าน
export function App() {
  return (
    <main>
      <h1>เข้าสู่ระบบ</h1>
      <form>
        <label>อีเมล<input type="email" name="email" autoComplete="username" /></label>
        <label>รหัสผ่าน<input type="password" name="password" autoComplete="current-password" /></label>
        <label><input type="checkbox" name="remember" /> จดจำการเข้าสู่ระบบ</label>
        <button type="submit">เข้าสู่ระบบ</button>
      </form>
      <a href="/forgot">ลืมรหัสผ่าน?</a>
    </main>
  );
}

ผลลัพธ์ที่ได้จากการรันจริง

<main><h1>เข้าสู่ระบบ</h1><form><label>อีเมล<input type="email" name="email" autoComplete="username"/></label><label>รหัสผ่าน<input type="password" name="password" autoComplete="current-password"/></label><label><input type="checkbox" name="remember"/> จดจำการเข้าสู่ระบบ</label><button type="submit">เข้าสู่ระบบ</button></form><a href="/forgot">ลืมรหัสผ่าน?</a></main>

ขั้นต่อไป

ขั้นต่อไปให้ต่อหน้าเข้าสู่ระบบเข้ากับ API จริงด้วย fetch ใช้ HTTP-only cookie หรือ token อย่างปลอดภัย และเพิ่มการลืมรหัสผ่านด้วยลิงก์ทางอีเมล

สรุป

หน้าเข้าสู่ระบบที่ดีประกอบจากฟอร์มที่ควบคุมด้วย state การตรวจความถูกต้อง การจำกัดการลองผิดซ้ำ สถานะผู้ใช้ผ่าน Context และหน้าที่ป้องกันการเข้าถึง ระบบจริงต้องตรวจสิทธิ์ที่เซิร์ฟเวอร์ทุกครั้ง