Авторизация через .htaccess

У сервера apache есть возможность сделать базовую авторизацию. Чтобы закрыть директорию, в неё нужно поместить два файла – .htaccess и .htpasswd.

Содержание файла .htaccess

# .htaccess
AuthType Basic
AuthName "Authorization"
AuthUserFile /путь_до_директории/.htpasswd
Require valid-user

AuthName "Authorization" – сообщение в окне ввода логина и пароля, кириллица не поддерживается, в Google Chrome вообще не выводится.
AuthUserFile /путь_до_директории/.htpasswd – путь до файла с паролями.

Авторизацией можно закрыть только определенные файлы, например ZIP-архивы.

# .htaccess
<Files arhive.zip>
    AuthType Basic
    AuthName "Authorization"
    AuthUserFile /путь_до_директории/.htpasswd
    Require valid-user
</Files>

На некоторых хостингах авторизация на статические файлы (изображения, шрифты и т.д.) может не работать т.к. они отдаются через Nginx.

Стоит проверить прямой доступ к самим файлам .htaccess и .htpasswd из браузера, если да, то закрыть его:

# .htaccess
<FilesMatch ".(htaccess|htpasswd)$">
    Order Allow,Deny
    Deny from all
</FilesMatch>

Пароли в htpasswd

В файле хранится пары логина и хеша пароля, например:

admin:$apr1$TCrF2kqA$TSMYziwt.qCkrct9yx4vv1

Логин может содержать латинские буквы, цифры, - и _, регистрозависимый.

Создание пароля в MD5

Подробности

htpasswd -m .htpasswd user

Bcrypt

В настоящее время считается очень безопасным, работает начиная с версии 2.4, формат:

$2y$ или $2a$ + результат алгоритма crypt_blowfish.

<?php
function bcrypt($password)
{
    $rounds = 12;
    $salt = sprintf('$2a$%02d$', $rounds) . substr(str_replace('+', '.', base64_encode(pack('N4', mt_rand(), mt_rand(), mt_rand(), mt_rand()))), 0, 22);
    return crypt($password, $salt);
}

echo bcrypt('123456'); // $2a$12$dMHIiiPfeSMxqj3/Wt1.z.Mo7NPza1x/WANl7hDXZJzxxKKorz5um

MD5 (APR)

Специфический алгоритм Apache (1000 итераций MD5 случайной соли и пароля), работает во всех версиях.

$apr1$ + результат алгоритма.

<?php
function crypt_apr1_md5($password)
{
    $salt = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, 8);
    $len = strlen($password);
    $text = $password . '$apr1$' . $salt;
    $bin = pack('H32', md5($password . $salt . $password));
    for($i = $len; $i > 0; $i -= 16) {
        $text .= substr($bin, 0, min(16, $i));
    }
    for($i = $len; $i > 0; $i >>= 1) {
        $text .= ($i & 1) ? chr(0) : $password{0};
    }
    $bin = pack('H32', md5($text));
    for($i = 0; $i < 1000; $i++) {
        $new = ($i & 1) ? $password : $bin;
        if ($i % 3) {
            $new .= $salt;
        }
        if ($i % 7) {
            $new .= $password;
        }
        $new .= ($i & 1) ? $bin : $password;
        $bin = pack('H32', md5($new));
    }

    $tmp = '';
    for ($i = 0; $i < 5; $i++) {
        $k = $i + 6;
        $j = $i + 12;
        if ($j == 16) $j = 5;
        $tmp = $bin[$i] . $bin[$k] . $bin[$j] . $tmp;
    }
    $tmp = chr(0) . chr(0) . $bin[11] . $tmp;
    $tmp = strtr(
        strrev(substr(base64_encode($tmp), 2)),
        'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
        './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
    );

    return '$apr1$' . $salt . '$' . $tmp;
}

echo crypt_apr1_md5('123456'); // $apr1$h9j4azoy$unmKNqjZlRfZv5xRetm9p1

Crypt

Работает только на Unix хостингах и до версии Apache 2.2.17. Ограничивает длину пароля до 8 символов. Считается небезопасным.

<?php
function crypt3($password)
{
    return crypt($password, substr($password, 0, 2));
}

echo crypt3('123456'); // 12tir.zIbWQ3c
PHP
SHA-1
Этот алгоритм небезопасен по современным стандартам, работает во всех версиях.

{SHA} + результат SHA-1 (бинарная строка из 20-ти символов) закодированный в Base64.

function hash_sha1($password)
{
    return '{SHA}' . base64_encode(sha1($password, true));
}

echo hash_sha1('123456'); // {SHA}fEqNCco3Yq9h5ZUglD3CZJT4lBs=

Авторизация и выход

Реквизиты доступа к закрытой директории можно передать в URL:

https://логин:пароль@example.com/path

Если такие URL использовать в src изображений, скриптов и стилей, то работать они не будут, вызвав ошибку:

Subresource requests whose URLs contain embedded credentials (e.g. `https://user:pass@host/`) are blocked.

Завершение сеанса происходит по закрытию браузера, но не вкладки.

На стороне JS

document.execCommand("ClearAuthenticationCache");

A "basic" way

var p = window.location.protocol + '//'
// current location must return 200 OK for this GET
window.location = window.location.href.replace(p, p + 'logout:password@')

An "asynchronous" way of doing the above is to do an AJAX call utilizing the logout username.
Example:

(function(safeLocation){
    var outcome, u, m = "You should be logged out now.";
    // IE has a simple solution for it - API:
    try { outcome = document.execCommand("ClearAuthenticationCache") }catch(e){}
    // Other browsers need a larger solution - AJAX call with special user name - 'logout'.
    if (!outcome) {
        // Let's create an xmlhttp object
        outcome = (function(x){
            if (x) {
                // the reason we use "random" value for password is 
                // that browsers cache requests. changing
                // password effectively behaves like cache-busing.
                x.open("HEAD", safeLocation || location.href, true, "logout", (new Date()).getTime().toString())
                x.send("")
                // x.abort()
                return 1 // this is **speculative** "We are done." 
            } else {
                return
            }
        })(window.XMLHttpRequest ? new window.XMLHttpRequest() : ( window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : u ))
    }
    if (!outcome) {
        m = "Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser."
    }
    alert(m)
    // return !!outcome
})(/*if present URI does not return 200 OK for GET, set some other 200 OK location here*/)

Other way

 function logout(secUrl, redirUrl) {
    if (bowser.msie) {
        document.execCommand('ClearAuthenticationCache', 'false');
    } else if (bowser.gecko) {
        $.ajax({
            async: false,
            url: secUrl,
            type: 'GET',
            username: 'logout'
        });
    } else if (bowser.webkit) {
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open("GET", secUrl, true);
        xmlhttp.setRequestHeader("Authorization", "Basic logout");
        xmlhttp.send();
    } else {
        alert("Logging out automatically is unsupported for " + bowser.name
            + "\nYou must close the browser to log out.");
    }
    setTimeout(function () {
        window.location.href = redirUrl;
    }, 200);
}