Pull to refresh

Входите! Вход без логина и пароля

Reading time8 min
Views20K

Кто виноват?


Одна из часто встающих задач при разработке web-проектов — пустить пользователя на сайт без ввода логина и пароля, при этом авторизовав его.

Вот некоторые примеры таких ситуаций:
  • Ссылка на активацию аккаунта только что зарегистрированным пользователем.
  • Ссылка на восстановление пароля.
  • Приглашение (возвращение) на сайт пользователя, который давно не заходил.

В каждом из этих случаев нам нужно создать для пользователя некий ключ и добавить его в URL, отправленный в письме.

Как правило этот ключ должен быть:
  • уникальным;
  • URL-безопасным;
  • сложно (невозможно) подделываемым;
  • разумной длины.

Я видел много решений (да и сам раньше прибегал к ним) основанных на добавление в БД служебного поля или целой таблицы, в которую помещались сгенерированные ключи и некоторая дополнительная информация, позволяющая авторизовать пользователя, когда он придёт на сайт с этим ключом. Обычно этот ключ — результат работы некоторой хеш-функции. Например: sha1($userId. «secret_key». time());

Очень часто такое решение приходит первым. На самом деле его место в самом конце.

Предлагаю Вашему решение, которые позволяет обойтись «малой кровью» — не требуют работы с БД.

Что делать?


Кроме приведённых выше свойств ключа, на него часто накладываются и другие ограничения. От них, собственно, и приходится плясать.

В самых простых случаях можно обойтись конструкциями вида: sha1($userId. «secret_key»); или sha1($userId. «secret_key». «confirm_code»);

Если пользователь придёт с таким ключём на сайт по URL вида example.com/users/%user_id%?t=%key%, мы легко сможем его проверить.

Недостатки такого подхода:
  • для каждого пользователя ключ будет всегда одинаковый;
  • такой ключ всегда будет валидным, его нельзя ограничить по времени;
  • нужно передавать явным образом id пользователя в URL.

Недостатки существенные и поэтому, в большинстве случаев, такое решение не подойдёт.
Но это не повод хранить данные в БД. Их можно хранить в самом ключе. Хранить безопасно — в зашифрованном виде.
В php функции шифрования реализованы в библиотеке Mcrypt, которую можно легко установить из pecl или репозиториев Вашей ОС.
Вы сами можете реализовать любимый алгоритм. Это не важно — главное у нас есть функции позволяющие зашифровать и расшифровать произвольный текст.

Идея проста. В строку мы помещаем необходимые данные, шифруем её своим секретным ключом, приводим к URL-безопасному виду и вставляем в качестве ключа в ссылку, по которой к нам придёт пользователь.

Нужно помнить о том, что чем больше данных мы захотим поместить в ключ, тем длиннее будет сам ключ, т.к. это уже не хеш фиксированной длины.

Для своего решения я выделил такие данные для хранения в ключе:
  • id пользователя (4 байта);
  • время создания ключа (4 байта);
  • время действия ключа (4 байта);
  • режим (1 байт) — переменная при помощи которой мы можем разделить назначение ключей (восстановление пароля/подтверждение регистрации/просто приглашение);
  • Случайное число (1 байт) — добавляем уникальности;
  • Контрольная сумма (4 байта).

Забегая вперёд покажу пример получившегося ключа: 67147328f43d69f7784770a2d9c84b181a8c.
Что будете хранить в ключе Вы — зависит от Вашей задачи. Тут я показал, что в ключ приемлемой длины можно легко уместить 18 байт информации.

Перейдём к реализации. Я реализовал данное решение в одном классе с двумя статическими методами.

Copy Source | Copy HTML<br/><?php<br/> <br/>class AuthToken {<br/>    private static $key = "секретный ключ";<br/>    private static $iv = "должен быть каждый раз случайным, но для данного решения подойдёт просто секретный";<br/> <br/>    private static function int2char($int) {<br/>        $char = "";<br/>        $hex = sprintf("%08x", $int);<br/>        for ($i =  0; $i < 4; $i++) {<br/>            $char .= chr(hexdec(substr($hex, $i * 2, 2)));<br/>        }<br/>        return $char;<br/>    }<br/> <br/>    private static function char2int($char) {<br/>        $int =  0;<br/>        $hex = "";<br/>        for ($i =  0; $i < 4; $i++) {<br/>            $hex .= sprintf("%02x", ord($char{$i}));<br/>        }<br/>        $int = hexdec($hex);<br/>        return $int;<br/>    }<br/> <br/>    public static function create($id, $expire =  0, $mode =  0) {<br/>        $id = intval($id);<br/>        $expire = intval($expire);<br/>        $mode = intval($mode);<br/>        if ($id <  0 || $expire <  0 || $mode <  0) {<br/>            return null;<br/>        }<br/> <br/>        $info = array();<br/>        $info["id"] = $id;<br/>        $info["time"] = time();<br/>        $info["expire"] = $expire;<br/>        $info["mode"] = $mode;<br/>        $info["rnd"] = ceil(mt_rand( 0, 255));<br/>        $info["sum"] = $info["time"] - $info["expire"] - $info["mode"] - $info["rnd"] - $info["id"];<br/>        $info = self::int2char($info["id"]) . self::int2char($info["time"]) . self::int2char($info["expire"]) . chr($info["mode"]) . chr($info["rnd"]) . self::int2char($info["sum"]);<br/> <br/>        $token = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, md5(self::$key), $info, MCRYPT_MODE_OFB, md5(self::$iv));<br/>        $tokenHex = "";<br/>        $tokenLength = strlen($token);<br/>        for ($i =  0; $i < $tokenLength; $i++) {<br/>            $tokenHex .= sprintf("%02x", ord($token{$i}));<br/>        }<br/>        return $tokenHex;<br/>    }<br/> <br/>    public static function check($tokenHex, $mode = null) {<br/>        $token = "";<br/>        $tokenHexLength = strlen($tokenHex) / 2;<br/>        for ($i =  0; $i < $tokenHexLength; $i++) {<br/>            $token .= chr(hexdec(substr($tokenHex, $i * 2, 2)));<br/>        }<br/>        $info = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, md5(self::$key), $token, MCRYPT_MODE_OFB, md5(self::$iv));<br/>        if (strlen($info) == 18) {<br/>            $info = array("id" => self::char2int(substr($info,  0, 4)), "time" => self::char2int(substr($info, 4, 4)), "expire" => self::char2int(substr($info, 8, 4)), "mode" => ord($info{12}), "rnd" => ord($info{13}), "sum" => self::char2int(substr($info, 14, 4)));<br/>            if ($info["sum"] == $info["time"] - $info["expire"] - $info["mode"] - $info["rnd"] - $info["id"]) {<br/>                if ($info["expire"] >  0) {<br/>                    if ($info["expire"] + $info["time"] < time()) {<br/>                        return false;<br/>                    }<br/>                }<br/>                if ($info["mode"] >  0) {<br/>                    if ($mode !== null) {<br/>                        if ($info["mode"] != $mode) {<br/>                            return false;<br/>                        }<br/>                    }<br/>                }<br/>                return $info["id"];<br/>            } else {<br/>                return false;<br/>            }<br/>        } else {<br/>            return false;<br/>        }<br/>    }<br/>}<br/> <br/>?><br/> <br/>


Ключи self::$key и self::$iv, в целях безопасности, лучше хранить отдельно от алгоритма.

(string) AuthToken::create($id, $expire = 0, $mode = 0)
Создаёт ключ.

(mixed) AuthToken::check($tokenHex, $mode = null)
Проверяет валидность ключа. В случае успешной проверки возвращает ID пользователя, иначе логическое FALSE.

Если не указывать $expire и/или $mode — они не будут учитываться при проверке ключа. Если не указывать $mode при проверке — он так же не будет учитываться.

Если ничего не помогает?


Такое решение не подойдёт если нужен одноразовый ключ. Но это можно решить небольшими костылями. Например установка в memchache флага проверки такого ключа в сочетании с ограниченным временем действия ключа дадут нужный результат в большинстве случаев.

Ну а дальше — либо фантазия либо БД.
Tags:
Hubs:
Total votes 98: ↑82 and ↓16+66
Comments78

Articles