ระบบสมาชิก - สมัครสมาชิก

งานการทำงานด้านเว็บด้วยภาษาฝั่งเซิฟเวอร์ มักจะมีความเกี่ยวข้องกับระบบฐานข้อมูลอยู่เสมอ ๆ หนึ่งในนั้นคือ ระบบสมาชิก เว็บหลายเว็บในปัจจุบัน มีการเชื่อมต่อระบบสมาชิกเข้ากับ 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 เอาไว้ และไม่แก้ไข ทั้ง ๆ ที่รู้อยู่ว่าไม่ปลอดภัย จนกระทั่งโดนแฮ็ค

blog comments powered by Disqus