Quản lý tài khoản đăng nhập qua .htpasswd bằng PHP

Bài viết hướng dẫn tạo, cập nhật và kiểm tra tài khoản quản lý trong file .htpasswd bằng PHP.

Nếu từng làm việc với Apache HTTP Server hẳn là ai cũng có lúc sử dụng .htpasswd để tạo mật khẩu ngăn chặn truy cập trái phép. Việc tạo file .htpasswd tương đối nhiêu khê, vì vậy cũng có không ít công cụ cho phép tạo file .htpasswd trực tuyến.

Trong bài viết này, tôi sẽ sử dụng .htpasswd như một kiểu quản lý tài khoản thay vì dùng database theo cách thông thường. Việc thêm/cập nhật tài khoản sẽ được thực hiện bằng PHP, tất nhiên là sau khi đăng nhập thành công rồi.

Cấu trúc file .htpasswd

.htpasswd là một file text, thường được tạo bằng lệnh htpasswd trong gói Apache. Mỗi dòng trong file .htpasswd là thông tin về tài khoản (user name) và mật khẩu của user đó (được mã hóa một chiều hoặc không), phân cách bằng dấu hai chấm ‘:’.

Ví dụ tài khoản icreativ có mật khẩu 12345678 được mã hóa bcrypt như sau:

icreativ:$2y$05$Z6JReuEu1JrC8YphmdlUFuJhg4gLbvMHgJTw9wa6SWc5wrukN4JQS

Các user lưu trong .htpasswd không được trùng nhau, chiều dài tối đa 255 ký tự và không được chứa dấu hai chấm ‘:’.

Mật khẩu có thể được mã hóa bằng những thuật toán khác nhau như MD5, SHA-1, crypt, bcrypt hoặc thuật toán mã hóa của hệ thống. Hãy tham khảo man của lệnh htpasswd để biết thêm chi tiết.

Chỉnh sửa .htpasswd bằng PHP

Bản chất .htpasswd là một file text, mỗi dòng lại là thông tin của một user nên đây đơn thuần là xử lý text file theo từng dòng (line-by-line).

Các user không trùng nhau nên ta sẽ đọc lần lượt từ đầu đến cuối tìm user tương ứng (trước dấu hai chấm ‘:’) để tìm mật khẩu hoặc chỉnh sửa.

Phần tưởng chừng phức tạp nhất là mã hóa một chiều thì lại cũng thật đơn giản vì PHP hỗ trợ hết các thuật toán mã hóa đó bao gồm thuật toán mạnh nhất là bcrypt.

Các hàm routine dưới đây xử lý file .htpasswd có mật khẩu hoàn toàn được mã hóa bằng bcrypt. Các hàm PHP được sử dụng là password_hash với kiểu mã hóa PASSWORD_BCRYPT để mã hóa mật khẩu và kiểm tra bằng hàm password_verify.

Lưu ý, vì lý do bảo mật, file .htpasswd không nên đặt trong thư mục web.

Các hàm PHP xử lý file .htpasswd

Kiểm tra tài khoản

/**
 * @param $username The user id
 * @param $password The entered password
 * @param $htpasswd The absolute path to the .htpasswd file
 * @return true if the user is found and the password matches
 */
function htpasswd_verify($username, $password, $htpasswd) {
    $lines = explode("\n", file_get_contents($htpasswd));
    foreach ($lines as $line) :
        $line = preg_replace('/\s+/', '', $line); // remove spaces
        if ($line) :
            list($user, $pass) = explode(":", $line, 2);
            if ( $user === $username ) : // found the user
                return ( password_verify($password, $pass) );            
            endif;
        endif;
    endforeach;
    return false;
}

Tạo/cập nhật tài khoản

/**
 * @param $username The user id
 * @param $password The entered password
 * @param $htpasswd The absolute path to the .htpasswd file
 */
function htpasswd_update($username, $password, $htpasswd) {    
    $password = password_hash($password, PASSWORD_BCRYPT);

    //read the file into an array
    $lines = explode("\n", file_get_contents($htpasswd));
    $new_file = "";
    $found = false;

    foreach ($lines as $line) :
        $line = preg_replace('/\s+/', '', $line); // remove spaces
        if ($line) :
            list($user, $pass) = explode(":", $line, 2);
            if ( $user === $username ) : // found the user
                $new_file .= $username . ':' . $password . "\n";
                $found = true;
            else :
                $new_file .= $user . ':' . $pass . "\n";
            endif;
        endif;
    endforeach;

    if (!$found) :
        $new_file .= $username . ':' . $password;
    endif;

    //save the information
    $fd = fopen( $htpasswd, 'w' );
    fwrite($fd, $new_file);
    fclose($fd);
}

Xóa tài khoản

/**
 * @param $username The user id
 * @param $password The entered password
 * @param $htpasswd The absolute path to the .htpasswd file
 */
function htpasswd_remove($username, $password, $htpasswd) {
    $lines = explode("\n", file_get_contents($htpasswd));
    $new_file = "";
    $removed = false;
    foreach ($lines as $line) :
        $line = preg_replace('/\s+/', '', $line); // remove spaces
        if ($line) :
            list($user, $pass) = explode(":", $line, 2);
            if ( $user === $username && password_verify($password, $pass) ) :
                    $removed = true;
            else :
                $new_file .= $user . ':' . $pass . "\n";
            endif;
        endif;
    endforeach;

    //save the information
    if ( $removed ) :
        $fd = fopen( $htpasswd, 'w' );
        fwrite($fd, $new_file);
        fclose($fd);
    endif;
    return $removed;
}

Ví dụ ta tạo cấu trúc thư mục Web root như dưới.

\_ index.php
\_ htpasswd.php
\_ ...
\_ .htaccess

Lưu ý trong đây không chứa file .htpasswd, thay vào đó là một đường dẫn ngoài ví dụ /path/to/.htpasswd. File .htpasswd này cần phân quyền phù hợp để PHP có thể chỉnh sửa được.

File cấu hình .htaccess sử dụng file .htpasswd để bảo vệ thư mục hiện hành.

.htaccess

AuthType Basic
AuthName "Password Protected Area"
AuthUserFile /path/to/.htpasswd
Require valid-user

File htpasswd.php là giao diện cho phép tạo hoặc cập nhật tài khoản đăng nhập bằng .htpasswd. Đoạn mã này đương nhiên cũng được bảo vệ bởi chính file .htpasswd.

Sau khi đổi mật khẩu thành công, Apache lập tức phát hiện sự thay đổi của file .htpasswd và sẽ yêu cầu đăng nhập lại ngay lập tức.

Giao diện tạo và cập nhật tài khoản trong .htpasswd

htpasswd.php

<?php
// The absolute path to the .htpasswd file
define( 'HTPASSWD', '/path/to/.htpasswd' );

if ( isset($_POST['username']) && isset($_POST['password']) ) :
    htpasswd_update($_POST['username'], $_POST['password'], HTPASSWD);
    // redirect to the main page
    header('Location: index.php');
    exit;
endif;

?>
<!DOCTYPE html>
<html>
<head>
    <title>.htpasswd</title>
</head>
<body>
<form method="POST">
    <label for="username">User Name</label>
    <input type="text" id="username" name="username" required>
    
    <label for="password">Password</label>
    <input type="password" id="password" name="password" placeholder="Password" required>

    <label for="confirm">Confirm</label>
    <input type="password" id="confirm" name="confirm" required>
    
    <input type="submit" name="submit" value="Add or update ›">
</form>
<script>
var password = document.getElementById("password"),
    confirm = document.getElementById("confirm");

// Validate whether password and confirmation password matches
function validatePassword() {
    if(password.value != confirm.value) {
        confirm.setCustomValidity("Password mismatched");
    } else {
        confirm.setCustomValidity('');
    }
}
password.onchange = validatePassword;
confirm.onkeyup = validatePassword;
</script>
</body>
</html>