Кто виноват?
Одна из часто встающих задач при разработке 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 флага проверки такого ключа в сочетании с ограниченным временем действия ключа дадут нужный результат в большинстве случаев.
Ну а дальше — либо фантазия либо БД.