Для одного проекта понадобилось сделать авторизацию пользователя на сайте с помощью ВКонтакте API как на клиентской стороне, с помощью javascript Open API, так и на серверной, с помощью PHP.
На первый взгляд, задача не такая уж и интересная, благо у ВКонтакте есть хорошая документация с подробным описанием, что да как, и даже пример клиентской авторизации, плюс, в сети уже есть очень много сайтов с авторизацией через ВКонтакте API.
Давайте попытаемся получить два списка друзей пользователя, один с помощью серверного API, а другой с помощью клиентского.
Заодно на практике разберёмся с обоими вариантами процесса авторизации.
Но сначала, чуть подробнее обозначим себе задачу:
- Мы не хотим лишний раз презагружать страницу, и вести весь обмен с сервером только через Ajax;
- Для пользователя процесс авторизации должен вызывать как можно меньшее количество «задних мыслей», то есть, всё должно быть сделано максимально удобно.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Доступ к ВК</title>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body>
<section id="serverApi">Серверный API</section>
<section id="clientApi">Клиентский API</section>
</body>
</html>
Клиентская авторизация
С клиентской авторизацией действительно нет ничего сложного.
Согласно документации, достаточно асинхронно загрузить openapi.js и, «повесить» VK.init({appid: YOUR_APP_ID})
на событие window.vkAsyncInit
, после чего уже можно вызвать функцию VK.Auth.login(authInfo, YOUR_APP_PERMISSIONS)
, которая и выполнит авторизацию пользователя.
var vk = {
data: {},
api: "//vk.com/js/api/openapi.js",
appID: YOUR_APP_ID,
appPermissions: YOUR_APP_PERMISSIONS,
init: function(){
$.js(vk.api);
window.vkAsyncInit = function(){
VK.init({apiId: vk.appID});
load();
}
function load(){
VK.Auth.login(authInfo, vk.appPermissions);
function authInfo(response){
if(response.session){ // Авторизация успешна
vk.data.user = response.session.user;
vk.getFriends();
}else alert("Авторизоваться не удалось!");
}
}
},
getFriends: function(){
VK.Api.call('friends.get', {fields: ['uid', 'first_name', 'last_name'], order: 'name'}, function(r){
if(r.response){
r = r.response;
var ol = $('#clientApi').add('ol');
for(var i = 0; i < r.length; ++i){
var li = ol.add('li').html(r[i].first_name+' '+r[i].last_name+' ('+r[i].uid+')')
}
}else alert("Не удалось получить список ваших друзей");
})
}
}
$.ready(vk.init);
Во-первых, сразу оговорюсь, что используемые здесь функции $
— это не jQuery
. Тем не менее, их назначение может не быть интуитивно понятным, так что я буду рассказывать, что же делает та или иная функция. Их самостоятельная реализация не должна составить особого труда, ровно как и перекладка всего кода на jQuery
.
Итак,
$.ready(function)
— Это обработчик для DOMContentLoaded
;
$.js(text)
— асинхронно загружает javascript файл;
$(element)
— возвращает обёртку над DOM узлом element
;
$(element).add(node)
— создаёт новый дочерний для element
узел node
и возвращает обёртку над ним;
$(element).html(string)
— обёртка для element.innerHTML = string
.
По-сути, этот код делает следующее:
По готовности документа загружается API ВКонтакте, и, после его инициализации, вызывается метод VK.Auth.login()
, который показывает всплывающее окно, которое успешно «блочится» любой баннерорезкой, в котором пользователь должен подтвердить своё согласие предоставить данные клиентскому приложению.
Функция authInfo
вызывается после согласия или несогласия пользователя, и, в случае успешной авторизации, запрашивает список друзей.
Клиентскую часть таким образом можно считать законченной, однако, стоит как-то предупреждать пользователей о возможности блокировки их браузером окна подтверждения.
Серверная авторизация
Серверная авторизация гораздо веселее. Механизм серверной авторизации для доступа к ВКонтакте API с стороннего сайта создан на базе протокола OAuth 2.0.
Процесс авторизации происходит следующим образом:
- Необходимо в браузере пользователя показать страницу, где он разрешит приложению доступ к своим данным;
- После успешной авторизации приложения браузер пользователя будет перенаправлен по адресу REDIRECT_URI, указанному при открытии диалога авторизации. При этом в GET-параметре будет передан код для получения ключа доступа;
- Необходимо выполнить запрос с передачей кода и секретных данных приложения на специальный адрес, в ответ мы получим ключ доступа access_token, необходимый нам для совершения запросов к API.
К сожалению, второй пункт, а именно, перенаправление браузера, срабатывает далеко не всегда, точнее, не срабатывает нигде, кроме ИЕ, в тот самый первый раз, когда пользователь впервые разрешает приложению доступ к своим данным. И как только не извращаются разработчики, лишь бы получить заветный код.
Правда, уже довольно давно вместо написи «Login Success» на открываемой странице красуется предупреждение о том, что как раз не надо делать именно так, как написано — вы видели это сообщение на картинке в начале топика.
К тому же, открытие дополнительного окна опять-таки связано с опасностью, что оно будет заблокировано.
К счастью, если мы знаем ссылку, по которой нужно открыть окно, есть много хороших способов открыть его так, чтобы оно не было заблокировано. Например, старый добрый тег iframe
всё ещё в строю стандарта HTML, и он как нельзя лучше подходит для нашей задачи.
Во-первых, раз мы знаем адрес, то мы можем и открыть фрейм и показать пользователю ссылку, где можно всё проделать вручню.
Во-вторых, если-таки происходит редирект, и в фрейме открывается страница с нашего домена, то мы можем получить из фрейма доступ к главному окну и автоматически запустить функцию закрытия фрейма. При этом на сервере уже будет access_token, который мы сохраним в сессии и выполняя с главного окна запросы к серверу сможем получать ответы. Перезагрузки страницы при этом не произойдёт, что полностью решает нашу задачу.
В-третьих, если перенаправления не произошло, и пользователь вручную открыл страницу подтверждения доступа приложения, то там уже точно происходит редирект, на страницу с нашего домена, которая будет открыта не во фрейме, а вместо главной. К сожалению, это и есть перезагрузка, но это меньшая жертва, чем могло бы быть. С этой страницы можно отправить пользователя снова на главную, во второй раз авторизация пройдёт уже без перезагрузок.
Реализацию класса доступа к VK API по OAuth на PHP можно легко найти на гитхабе, что я и сделал (PHP >= 5.4).
(Совершенно случайно оказалось, что этот класс, скорее всего, написан хабрапользователем vladkens, за что ему огромное спасибо)
Теперь перейдём к самому интересному.
<?php
require_once('vk.php');
session_start();
function getPostStr($data, $default = false){ // возврачает строку - значение $_POST[$data]
if(!isset($_POST[$data])) return $default;
$data = $_POST[$data];
$data = htmlspecialchars(strip_tags(trim($data)));
return ($data != "" ? $data : $default);
}
function error($code, $text, $params = Array()){
$result = Array('error' => Array('code' => $code, 'message' => $text));
if(count($params) > 0) foreach($params as $key => $value) $result['error'][$key] = $value;
die(json_encode($result));
}
$vkConf = Array(
'appID' => YOUR_APP_ID,
'apiSecret' => YOUR_API_SECRET,
'callbackUrl' => YOUR_DOMAIN . '/auth.php',
'apiSettings' => YOUR_APP_PERMISSIONS
);
$vk = (isset($_SESSION['accessToken']))
? new VK($vkConf['appID'], $vkConf['apiSecret'], $_SESSION['accessToken']) : null;
function userIn($vk, $vkConf){ // Авторизация пользователя
unset($_SESSION['accessToken']);
$vk = new VK($vkConf['appID'], $vkConf['apiSecret']);
$authorizeUrl = $vk -> getAuthorizeURL($vkConf['apiSettings'], $vkConf['callbackUrl']);
error(-1, "Необходима авторизация ВКонтакте!", Array('url' => $authorizeUrl));
}
function getFriends($fields, $order, $vk){ // Получение списка друзей пользователя
$userFriends = $vk -> api('friends.get', array('fields' => $fields, 'order' => $order));
$result = Array();
foreach($userFriends['response'] as $key => $value){
$result[] = Array('firstName' => $value['first_name'], 'lastName' => $value['last_name'], 'uid' => $value['uid']);
}
echo json_encode($result);
}
$method = strtolower($api -> getStr("method"));
switch($method){
case "user.in" : userIn($vk, $vkConf); break;
case "friends.get" : getFriends(getPostStr("fields"), getPostStr("order"), $vk); break;
default: Api::error(0, "Неверный запрос к Api");
}
?>
Запросы на эту страницу мы будем посылать методом POST, где параметр method
содержит название метода, который мы хотим выполнить на сервере.
Для успешной авторизации, мы будем посылать запрос user.in
по готовности нашей главной страницы, на что сервер будет отвечать кодом ошибки -1
, который мы будем отлавливать и обрабатывать.
При этом вместе с ошибкой придёт также и URL страницы, на которой пользователь должен подтвердить доступ приложению, и именно её мы будем открывать во фрейме.
function Api(method, params, callback){
if(!params) params = {};
$.ajax($.copy({method: method}, params), 'server.php', function(r){
if(r.error){
switch(r.error.code){
case -1: { // notAuth
var div = $(document.body).add('div').id('authPopup'), iframe = div.add('iframe').attr({src: r.error.url});
div.add('p').html('Если вы видите предупреждение о возможности потери доступа к Вашему аккаунту в случае копирования данных из адресной строки, не волнуйтесь - просто не произошло перенаправления на наш сервер после авторизации ВКонтакте. Такое иногда бывает по соображениям безопасности, вам нужно просто проделать всё вручную, нажав на ').add('a').attr({href: r.error.url}).html('эту ссылку');
$.splash.open({onclose:function(){
div.remove()
}});
} break;
default: alert(r.error.code+': '+r.error.message);
}
}else if(callback) callback(r);
});
}
$.ready(function(){
Api('user.in');
});
Кратко опишу $
функции:
$.copy(object1, object2)
— копирует в object1
все параметры object2
;
$.ajax(text, addr, callback)
— отправляет POST запрос на addr
, передавая сообщение text
. Ответ принимает в виде JSON и предаёт его в функцию callback
;
$(element).id(elemID)
— обёртка для element.id = elemID
;
$(element).attr(object)
— устанавливает атрибуты тега элемента element
. Ключи object
— названия атрибутов, значения ключей будут установлены как значения атрибутов;
$(element).remove()
— удаляет element
;
$.splash.open(params)
— Затягивает окно браузера полупрозрачным тёмным блоком. Параметр params.onclose
— функция, которая будет вызвана при $.splash.close()
.
Итак, теперь при загрузке документа у нас всплывает окно клиентской авторизации приложения и фрейм с страницей подтверждения серверной авторизации. Либо, вместо окна подтверждения в фрейме мы видим предупреждение, но на этот случай у нас есть ссылка, по которой пользователь может перейти и сам подтвердить разрешения приложению.
Осталось только сделать страницу, на которую производится редирект.
<?php
require_once('vk.php');
session_start();
$vkConf = Array(
'appID' => YOUR_APP_ID,
'apiSecret' => YOUR_API_SECRET,
'callbackUrl' => YOUR_DOMAIN . '/auth.php',
);
if(isset($_REQUEST['code'])){
$vk = new VK($vkConf['appID'], $vkConf['apiSecret']);
$accessToken = $vk -> getAccessToken($_REQUEST['code'], $vkConf['callbackUrl']);
$_SESSION['accessToken'] = $accessToken['access_token'];
}else die('Auth failed');
?>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Подтверждение серверной авторизации с помощью ВК</title>
<script>
$.ready(function(){
if(window.top){
window.top.vk.getFriends();
window.top.$.splash.close();
}
})
</script>
</head>
<body>
<h4>Авторизация успешна!</h4>
<p>Теперь вы можете <a href="YOUR_DOMAIN">вернуться на сайт</a>, и всё заработает</p>
</body>
</html>
Здесь мы просто сохраняем access_token в переменные сессии, и скрипт обрабатывающий Ajax-запросы сможет его использовать. К тому же, если эта страница открылась во фрейме, мы вызываем метод главного окна, который запрашивает у сервера список друзей ВКонтакте, и метод, закрывающий фрейм.
Вот такая вот получилась реализация ненавязчивой авторизации пользователя сайта через ВКонтакте API.
В качестве домашнего задания попробуйте сделать редирект на страницу YOUR_DOMAIN
со страницы подтверждения, если она открыта не во фрейме, и улучшить метод userIn()
так, чтобы при наличии в сессии access_token проверялась его валидность, и, если ключ действителен, использовался он вместо новой авторизации.
Автор: Xao