สอนสร้างหน้าเข้าสู่ระบบด้วย 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 และหน้าที่ป้องกันการเข้าถึง ระบบจริงต้องตรวจสิทธิ์ที่เซิร์ฟเวอร์ทุกครั้ง