Apichart Nakarungutti
The Cake is a Lie
งานการทำงานด้านเว็บด้วยภาษาฝั่งเซิฟเวอร์ มักจะมีความเกี่ยวข้องกับระบบฐานข้อมูลอยู่เสมอ ๆ หนึ่งในนั้นคือ ระบบสมาชิก เว็บหลายเว็บในปัจจุบัน มีการเชื่อมต่อระบบสมาชิกเข้ากับ social network ต่าง ๆ เพื่อความสะดวกให้กับผู้ใช้ แต่ผู้ใช้บางคนอาจจะไม่ต้องการผูกบัญชีของ social network เข้ากับบางเว็บ ด้วยเหตุผลต่าง ๆ นานา ระบบสมาชิกจึงยังจำเป็นอยู่
ตอนแรกขอพูดถึงการสมัครสมาชิกก่อน แล้วตอนต่อไปจะว่ากันเรื่องการ log in และ log out เพราะถ้ายังไม่สมัครสมาชิกก่อน ก็ log in ไม่ได้…
ในระบบสมาชิก สิ่งที่จำเป็นจริง ๆ มีอยู่ 2 อันคือ ชื่อผู้ใช้ และ รหัสผ่าน แต่ควรจะมี อีเมล เพื่อใช้ในการติดต่อกับสมาชิกไว้ด้วย
จะขอข้ามไม่พูดถึงเรื่องการสร้างฐานข้อมูล และตาราง แต่จะบอกแค่ชื่อ และโครงสร้างของตาราง นอกจากนั้น ใช้ความรู้เรื่อง PHP และ PDO เขียนสคริปท์ PHP กันเอาเอง หรือไม่ก็ใช้เครื่องมือจัดการฐานข้อมูลอย่าง phpMyAdmin (MySQL), phpPgAdmin (PostgreSQL) หรือตัวอื่น ๆ ตามแต่ละชนิดฐานข้อมูลกันตามสะดวก
-- TABLE: members
id int NOT NULL PRIMARY KEY
username varchar(255) NOT NULL
password varchar(255) NOT NULL
email varchar(255) NOT NULL
เมื่อฐานข้อมูลพร้อมแล้ว ก็สร้างแบบฟอร์มสำหรับสมัครสมาชิกขึ้นมาก่อน
<form id="register" method="post" action="regis.php">
<label>Username: </label>
<input type="text" id="username" name="username" />
<label>Password: </label>
<input type="password" id="password" name="password" />
<label>Confirm Password: </label>
<input type="password" id="cpassword" name="cpassword" />
<label>E-Mail: </label>
<input type="text" id="email" name="email" />
<input type="reset" name="reset" id="reset" value="Reset" />
<input type="submit" name="register" id="register" value="Register" />
</form>
เมื่อมีคนมาสมัครสมาชิก ข้อมูลจะถูกส่งมายัง regis.php ก็ต้องตรวจสอบค่าให้ถูกต้องเรียบร้อยก่อนว่าผู้ใช้กรอกข้อมูลครบถ้วนสมบูรณ์ดี หากผิดพลาดก็แจ้งเตือนกลับไป
<?php
// FILE: regis.php
$error = false;
$u_name = $_POST['username'];
$u_pass = $_POST['password'];
$u_cpass = $_POST['cpassword'];
$u_email = $_POST['email'];
// validate
if (strlen($u_name) < 4) {
echo 'Username too short, minimum is 4 characters';
$error = true;
}
if (strlen($u_pass) < 6) {
echo 'Password too short, minimum is 6 characters';
$error = true;
}
if ($u_pass != $u_cpass) {
echo 'Password an comfirm password are different';
$error = true;
}
if (!filter_var($u_email, FILTER_VALIDATE_EMAIL)) {
echo 'Invalide E-Mail';
$error = true;
}
เมื่อตรวจสอบข้อมูลเรียบร้อยแล้ว หากไม่มีอะไรผิดพลาดก็เชื่อมต่อกับฐานข้อมูล และตรวจสอบว่ามีใครใช้ชื่อนี้ไปก่อนหรือยัง เพราะชื่อบัญชีห้ามซ้ำกัน
<?php
// FILE: regis.php
.
.
.
if (!$error) {
// your database information
$db_host = 'localhost';
$db_name = 'keancode';
$db_user = 'keanuser';
$db_pass = 'keanpass';
// connect
try {
// You must change this if you use different database system
$conn = new PDO("mysql:host=$db_host; dbname=$db_name", $db_user, $db_pass);
$conn->exec("SET CHARACTER SET utf8");
// prepare sql for checking username
$result = $conn->prepare("SELECT COUNT(*) FROM members WHERE username='" . $u_name . "'");
$result->execute();
if ($result !== false) {
if ($result->fetchColumn() > 0) {
echo 'This username already taken';
$error = true;
}
}
if (!$error) {
// Everything fine, add register info into database
// TODO: write insert statement
}
$conn = null;
}
catch (PDOException $e) {
echo $e->getMessage();
}
}
อันนี้ใช้ความสามารถของ PDO::prepare()
ในการป้องกัน SQL Injection นอกจากนี้ ถ้าต้องการตรวจสอบว่าใช้ อีเมลเดียวกันในการสมัครหรือไม่ ก็อาจจะเพิ่มการตรวจสอบเข้าไปอีกครั้งก็ได้ โดยใช้วิธีเดียวกับตอนตรวจสอบชื่อบัญชี
หลังจากทุกอย่างเป็นปกติ ก็นำข้อมูลผู้ใช้เข้าสู่ฐานข้อมูล (เพิ่มโค้ดตรง // TODO: ...
บรรทัดที่ 35)
<?php
.
.
.
if (!$error) {
// Everyting fine, add register info into database
// prepare array
$user = array(
'name' => $u_name,
'pass' => $u_pass,
'email' => $u_email,
);
$result = $conn->prepare("INSERT INTO members VALUES (null, :name, :pass, :email)");
$result->execute($user);
if ($result !== false) {
echo 'Registration Completed.';
}
}
.
.
.
แค่นี้ระบบลงทะเบียนก็จะสมบูรณ์ แต่มันยังมีปัญหาด้านความปลอดภัยอยู่ หากมีผู้ไม่ประสงค์ดี สามารถเจาะเข้าฐานข้อมูล เขาจะได้รหัสผ่านไปใช้ได้ทันที และในกรณีที่เลวร้ายกว่า หากสมาชิกใช้ ชื่อบัญชี อีเมล และรหัสผ่านเดียวกันกับหลาย ๆ เว็บ คนร้ายก็จะเอาข้อมูลเหล่านี้ไปใช้กับเว็บอื่น ๆ ได้ด้วย ดังนั้นวิธีนี้จึงไม่เหมาะสมอย่างยิ่งอย่างยิ่งในการใช้งานจริง
ทางออกที่ดีกว่าสำหรับเรื่องนี้ คือการเก็บ hash ของรหัสผ่าน แทนที่จะเก็บตัวรหัสผ่านตรง ๆ ซึ่ง PHP มีฟังชั่นพื้นฐานที่นิยมใช้กับรหัสผ่านอยู่ 2 ตัวคือ md5 และ sha1
หากต้องการใช้ sha1 ก็ให้แก้ไขค่าของ pass
ในอาเรย์ $user
เสียใหม่
<?php
$user = array(
'name' => $u_name,
'pass' => sha1($u_pass),
'email' => $u_email,
);
รหัสที่เข้ามาจะถูกแปลงเป็นรหัสที่อ่านไม่ออกอย่างเช่น 7c4a8d09ca3762af61e59520943dc26494f8941b
และเก็บเข้าฐานข้อมูล ซึ่งหากข้อมูลที่เข้ามาเหมือนกัน ค่าที่ได้จะเหมือนกันทุกครั้ง
แม้ว่าตอนนี้รหัสผ่านจะเดาไม่ได้ด้วยตาเปล่าแล้ว แต่ปัญหาอีกอย่างคือ หากเป็นรหัสผ่านง่าย ๆ เพียงนำ hash ที่ได้ไปค้นหากับ Google เราก็อาจจะได้รหัสผ่านตัวจริงออกมาอย่างง่ายดาย
แต่ถึงแม้จะหาจาก Google ไม่เจอ คนร้ายยังสามารถใช้วิธี brute-force เพื่อหาข้อความที่มีค่า hash ตรงกับที่ได้มาได้ หรือหากไม่เจอ ก็สามารถใช้วิธี brute-force ค้นหาข้อความที่ได้ค่า hash ตรงกับที่ได้มาได้อยู่ดี
เพื่อเพิ่มความยุ่งยากอีกนิด จึงมีเทคนิคที่เรียกว่า salt ขึ้นมา โดยการเพิ่มข้อความต่อเข้ากับรหัสผ่าน (ไม่ว่าจะเป็นด้านหน้า ด้านหลัง หรือตรงกลาง) เพื่อให้รหัสผ่านยาวขึ้น และซับซ้อนมากขึ้น และป้องกัน rainbow table (ตาราง ฐานข้อมูล หรือเว็บบางเว็บ ที่เก็บผลของการ hash ข้อความต่าง ๆ เอาไว้ และใช้ตรวจสอบหาค่า hash โดยไม่ต้องคำนวนใหม่ทุกครั้ง — อ่านเพิ่มเติมจาก wiki, ThaiCERT) ทั้งนี้ salt ที่ใช้ควรจะมีความยาว และซับซ้อนพอสมควร
salt มีอยู่ 2 แบบคือ salt ที่ใช้ค่าคงที่
<?php
$user['pass'] = sha1($u_pass . 'Salt makes delicious password!');
และแบบ dynamic
<?php
$user['pass'] = sha1($u_pass . md5($u_pass));
หรือรวมกัน
<?php
$user['pass'] = sha1('NaCl' . $u_pass . md5($u_pass));
แม้ว่าจะปลอดภัยขึ้น แต่หากคนร้ายสามารถคาดเดาว่าใช้ salt ค่าของ salt รวมถึงวิธีสร้าง salt ได้ถูกต้อง เขาก็ยังสามารถ brute-force เพื่อหารหัสผ่านได้อยู่ดี แต่ยังมีวิธีการ และเทคนิคอื่น ๆ เพื่อช่วยให้เว็บของเราปลอดภัยขึ้น
หากเรียนรู้เรื่องความปลอดภัย เราจะเข้าใจว่า “ในโลกของความปลอดภัย ไม่มีอะไรปลอดภัย” แม้ว่าจะใช้เทคนิคด้านความปลอดภัยชั้นสูง หากมันคุ้มค่าที่จะเจาะข้อมูล คนร้ายก็ยอมเสียเวลา และทรัพยากร เพื่อจะเจาะมันให้ได้ แต่หากมันไม่คุ้มค่า ต่อให้ไม่มีความปลอดภัยใด ๆ ก็ไม่มีใครสนใจจะเจาะอยู่ดี (เรียนรู้จากความเห็นของ @lewcpe)
แต่ถึงอย่างไรก็ตาม เราควรทำให้ระบบของเราปลอดภัยระดับหนึ่ง และควรจะปรับปรุง และเพิ่มระดับความปลอดภัยขึ้นเรื่อย ๆ เมื่อมีฐานผู้ใช้มากขึ้น เพื่อไม่ให้เกิดปัญหาอย่างกรณีของ Tuts+ ที่แนะนำเกี่ยวกับการเก็บรหัสผ่านอย่างปลอดภัย แต่กลับเก็บรหัสผ่านแบบ plain text เอาไว้ และไม่แก้ไข ทั้ง ๆ ที่รู้อยู่ว่าไม่ปลอดภัย จนกระทั่งโดนแฮ็ค…