Здравствуйте, хабровчане. Недавно у меня возникла необходимость сделать для сайта на любимом модиксе аутентификацию через Steam. Гугление, хабрение и даже стековерфлоувание (какое страшное слово получилось) готового результата не принесло, так что решил я собранные в разных местах куски объединить в одну статью, чтобы тем, кто пойдёт следом было легче.
Первым делом, как и большинство умных людей (по крайней мере тех, чьи посты я позже находил на разных форумах с вопросами «АААА КАК ЭТО РАБОТАЕТ???!!!»), направился я на http://steamcommunity.com/dev, где и прочитал весьма воодушевляющий абзац:
Скачав simpleOpenID и вдоволь с ним наигравшись, я обнаружил, что стим в качестве OpenID-провайдера им вообще никаким образом не рассматривается. Ещё немного поисков, и вот удача. На одном из форумов обнаружился класс, разработанный исключительно для авторизации через стим.
Правда, для использования в модиксе, в него пришлось внести некоторые доработки.
И вот, собрав такой класс и положив в папку, которую смогу запомнить, приступил ко второй части.
С модиксом вопросов возникло меньше, он мне знаком чуть поболе, нежели стим, учётку для которого я использовал впервые именно для тестирования авторизации.
Итак, создаём сниппет со следующим кодом:
В коде сниппета использовался самописный процессор для генерации пароля. Конечно, соблазн просто написать функцию и не заморачиваться, всегда выше. Но статья Парадигма программирования процессорами в MODx Revolution постоянно маячила перед глазами и сподвигала на правильные решения.
Steam-аутентификация
Первым делом, как и большинство умных людей (по крайней мере тех, чьи посты я позже находил на разных форумах с вопросами «АААА КАК ЭТО РАБОТАЕТ???!!!»), направился я на http://steamcommunity.com/dev, где и прочитал весьма воодушевляющий абзац:
Steam can act as an OpenID provider. This allows your application to authenticate a user's SteamID without requiring them to enter their Steam username or password on your site (which would be a violation of the API Terms of Use.) Just download an OpenID library for your language and platform of choice and use steamcommunity.com/openid as the provider. The returned Claimed ID will contain the user's 64-bit SteamID. The Claimed ID format is: steamcommunity.com/openid/id/steamid
Скачав simpleOpenID и вдоволь с ним наигравшись, я обнаружил, что стим в качестве OpenID-провайдера им вообще никаким образом не рассматривается. Ещё немного поисков, и вот удача. На одном из форумов обнаружился класс, разработанный исключительно для авторизации через стим.
Правда, для использования в модиксе, в него пришлось внести некоторые доработки.
Файлик с именем steamsignin.class.php
<?php
/**
*
* @package Steam Community API
* @copyright (c) 2010 ichimonai.com
* @license http://opensource.org/licenses/mit-license.php The MIT License
*
*/
class SteamSignIn
{
const STEAM_LOGIN = 'https://steamcommunity.com/openid/login';
/**
* Get the URL to sign into steam
*
* @param mixed returnTo URI to tell steam where to return, MUST BE THE FULL URI WITH THE PROTOCOL
* @param bool useAmp Use & in the URL, true; or just &, false.
* @return string The string to go in the URL
*/
public static function genUrl($returnTo = false, $useAmp = true)
{
//$returnTo = (!$returnTo) ? (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] : $returnTo;
//Эту строчку правим на
$returnTo = (!$returnTo) ? $_SERVER['SCRIPT_NAME'] : $returnTo;
$returnTo = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'].'/'. $returnTo;
/*поскольку вызывать будем из модикса, где $_SERVER['SCRIPT_NAME'] не указывает на страницу, с которой был вызов. А отправлять пользователя на главную страницу сайта некультурно*/
$params = array(
'openid.ns' => 'http://specs.openid.net/auth/2.0',
'openid.mode' => 'checkid_setup',
'openid.return_to' => $returnTo,
'openid.realm' => (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'],
'openid.identity' => 'http://specs.openid.net/auth/2.0/identifier_select',
'openid.claimed_id' => 'http://specs.openid.net/auth/2.0/identifier_select',
);
$sep = ($useAmp) ? '&' : '&';
return self::STEAM_LOGIN . '?' . http_build_query($params, '', $sep);
}
/**
* Validate the incoming data
*
* @return string Returns the SteamID64 if successful or empty string on failure
*/
public static function validate()
{
// Star off with some basic params
$params = array(
'openid.assoc_handle' => $_GET['openid_assoc_handle'],
'openid.signed' => $_GET['openid_signed'],
'openid.sig' => $_GET['openid_sig'],
'openid.ns' => 'http://specs.openid.net/auth/2.0',
);
// Get all the params that were sent back and resend them for validation
$signed = explode(',', $_GET['openid_signed']);
foreach($signed as $item)
{
$val = $_GET['openid_' . str_replace('.', '_', $item)];
$params['openid.' . $item] = get_magic_quotes_gpc() ? stripslashes($val) : $val;
}
// Finally, add the all important mode.
$params['openid.mode'] = 'check_authentication';
// Stored to send a Content-Length header
$data = http_build_query($params);
$context = stream_context_create(array(
'http' => array(
'method' => 'POST',
'header' =>
"Accept-language: en\r\n".
"Content-type: application/x-www-form-urlencoded\r\n" .
"Content-Length: " . strlen($data) . "\r\n",
'content' => $data,
),
));
//Работоспособность этой функции с https зависит от настроек хостинга и у меня по закону Мэрфи не сработала.
//$result = file_get_contents(self::STEAM_LOGIN, false, $context);
//так что, пришлось заменять её на
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_URL, self::STEAM_LOGIN.'?'.$data);
curl_setopt($ch, CURLOPT_REFERER, self::STEAM_LOGIN.'?'.$data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$result = curl_exec($ch);
curl_close($ch);
// Validate wheather it's true and if we have a good ID
preg_match("#^http://steamcommunity.com/openid/id/([0-9]{17,25})#", $_GET['openid_claimed_id'], $matches);
$steamID64 = is_numeric($matches[1]) ? $matches[1] : 0;
// Return our final value
//return preg_match("#is_valid\s*:\s*true#i", $result) == 1 ? $steamID64 : '';
//Поскольку одной валидации нам мало, то добавляем мякотку в виде ника и аватарки (при желании, можно читать и другие элементы)
$slf = "http://steamcommunity.com/profiles/$steamID64/?xml=1";
$url = simplexml_load_file($slf);
$ret = array("result"=>true,
"nick"=>(string)$url->steamID[0],
"username"=>(string)$url->steamID64[0],
"avatarIcon"=>(string)$url->avatarIcon[0],
"avatarMedium"=>(string)$url->avatarMedium[0],
"avatarFull"=>(string)$url->avatarFull[0]);
// Return our final value
return preg_match("#is_valid\s*:\s*true#i", $result) == 1 ? $ret : array("result"=>false);
}
}
И вот, собрав такой класс и положив в папку, которую смогу запомнить, приступил ко второй части.
Что же происходит в модиксе
С модиксом вопросов возникло меньше, он мне знаком чуть поболе, нежели стим, учётку для которого я использовал впервые именно для тестирования авторизации.
Итак, создаём сниппет со следующим кодом:
Сниппет с говорящим названием SteamOpenID
<?php
if(!isset($scriptProperties['tmpl_loggedin'])){ $tmpl_loggedin = "OpenIDLoggedIn"; }
if(!isset($scriptProperties['tmpl_loggedout'])){ $tmpl_loggedout = "OpenIDNotLoggedIn"; }
//Первые две строки вряд ли вызовут вопросы, да и следующая весьма обыденна. Подключаем класс к сниппету.
$modx->getService('steamsignin','SteamSignIn', $modx->getOption('base_path').'path/to/class');
$errors = "";
/*Итак, если мы выходим, то просто разлогиниваем пользовтеля и перезагружаем страницу. Перезагружаем, потому что метод $user = $modx->getObject('modUser', 0), предложенный на каком-то из форумов широко известным в узких кругах Безумкиным выдавал ошибки. */
if($_GET['openid_action'] == "logout"){
$response = $modx->runProcessor('/security/logout');
if ($response->isError()) {
// ошибка
$errors.='Logout error. Message: '.$response->getMessage();
}
$url = $modx->makeUrl($modx->resource->id);
$modx->sendRedirect($url);
}
//Если мы не выходим, то возможно логинимся
else if($_GET['openid_mode'] == 'id_res'){ // Perform HTTP Request to OpenID server to validate key
$sign = new SteamSignIn;
$result = $sign->validate();
//а если мы логинимся и стим отзывается полжительно, то в $result лежат всякие интересные вещи. Например, true в $result['result']
if($result['result']){
$count = $modx->getCount('modUser', array('username' => $result['username']));
//здесь мы создаём в модиксе пользователя c SteamID64 в качестве логина. Потому что ники менять можно, а айдишники нельзя
if($count == 0){
$user = $modx->newObject('modUser');
$user->set('username', $result['username']);
$user->set('password', $modx->runProcessor('path/to/processor/createRandomPass')->getMessage());
$user->save();
$profile = $modx->newObject('modUserProfile');
$profile->set('fullname', $result['nick']);
$extended['avatar']['icon']=$result['avatarIcon'];
$extended['avatar']['medium']=$result['avatarMedium'];
$extended['avatar']['full']=$result['avatarFull'];
$profile->set('extended', $extended);
$user->addOne($profile);
$profile->save();
$user->save();
}
/*был ли уже пользователь, или мы только что его создали - сбрасываем пароль на случайный и входим под этим паролем. Чтоб не заморачиваться с их хранением*/
$pass = $modx->runProcessor('path/to/processor/createRandomPass');
$user = $modx->getObject('modUser', array('username' => $result['username']));
$user->set('password', $pass->getMessage());
$user->save();
$logindata = array(
'username' => $result['username'], // имя пользователя
'password' => $pass->getMessage(), // пароль
'rememberme' => true // запомнить?
);
// сам процесс авторизации
$response = $modx->runProcessor('/security/login', $logindata);
// проверяем, успешно ли
if ($response->isError()) {
// произошла ошибка, например неверный пароль
$errors.='Login error. Message: '.$response->getMessage();
}
$modx->user = $user; //А вот эта конструкция срабатывает, перезагрузка страницы не нужна.
}}
//Дальше всё совсем просто. Определяем, авторизован ли пользователь и загружаем соответствующие чанки
//Плейсхолдеры в чанках простые, при необходимости опять же можно расширить их состав.
if($modx->user->id!=0) {
$profile = $modx->user->getOne('Profile');
$ext = $profile->get('extended');
$output = $modx->getChunk($tmpl_loggedin,array('identity'=>$profile->fullname,'avatar'=>$ext['avatar']['icon'],'errors'=>$errors));
return $output;
} else {
$sign = new SteamSignIn;
$output = $modx->getChunk($tmpl_loggedout,array('url'=>$sign->genUrl($modx->resource->get('uri')),'errors'=>$errors));
return $output;
}
Вместо заключения
В коде сниппета использовался самописный процессор для генерации пароля. Конечно, соблазн просто написать функцию и не заморачиваться, всегда выше. Но статья Парадигма программирования процессорами в MODx Revolution постоянно маячила перед глазами и сподвигала на правильные решения.