VK.com предоставляет многофункциональный и удобный API, который позволяет делать всё, что приходит на ум программисту, и даже, немного больше. API позволяет отправлять сообщения, получать и публиковать записи на стену, производить абсолютно любые действия, которые существуют в VK, от имени пользователя, кому был сгенерирован токен. Разобравшись с API на примере ВК, в будущем, вы безпроблемно сможете быстро вникать в API любых других сайтов. И, ввиду того, что 90% всех API имеют схожую реализацию, то понимание этой статьи будет для вас отличным стартом, и хорошей школой освоения «взрослых игрушек».
В недавней статье было рассмотрено, что такое REST API. Почитайте информацию по ссылке — это точно будет полезно.
- Что такое VK API, разные группы API.
- Получение токена доступа
- Запросы к API (на примере получения всех записей пользователя со стены)
- Резюме
Что в VK отличается от стандарта по умолчанию
Сlient Сredentials передаются в теле запроса
VK требует, чтобы в запросе Access-токена Сlient Сredentials передавались не в Basic Auth в заголовке Authorization (как у большинства провайдеров), а в теле запроса. Но перенести Сlient Сredentials в тело запроса просто — настройкой в application.properties:
spring.security.oauth2.client.registration.vk.client-authentication-method=post
То есть поведение по умолчанию было таким:
Будет не так
А будет другим — Client Credentials уйдут в тело запроса.
В ответе отсутствует token_type: Bearer
Запрос изменили, но с ответом тоже не все ладно. В ответном JSON отсутствует пункт «token_type»:»Bearer»:
{ «access_token»:»ed0f46118dec0a5e6935ca1986004cb6a1e92631c95eebffb4f184b3856f206e88eec68993e7d19162ac3″, «expires_in»:86400, «user_id»:1111111, «email»:»[email protected]» }
А Spring ожидает, что token_type должен быть непустым и выбрасывает исключение. Поэтому ниже мы модифицируем ответ с помощью CustomTokenResponseConverter.
После успешного получения Access Token-а по протоколу OpenID идет обращение за данными авторизовавшегося пользователя. Это конечная точка Userinfo провайдера. Тут ответ тоже нестандартный.
VK возвращает по адресу Userinfo ответ в обертке
Обычно в ответе сразу перечисляются атрибуты пользователя, но не в VK. Здесь ответ приходит обернутым в response и еще в массив:
{ «response»: [ { «first_name»: «Иван», «id»: 1005684, «last_name»: «Петров», «photo_max»: «https://sun1.is74.userapi.com/s/v1/if2/SM08JDYA4scNcgqgQ5rqoAPsRu-R7CO_k_BneLs6AWDO1mhFVLiwhgtgmgVkh-uvEHXDUrWJ5eiI8FisARAdOj.jpg?size=200×0&quality=96&crop=741,68,736,736&ava=1» } ] }
Мы исправим это с помощью пользовательского DefaultOAuth2UserService.
Стоит ли бороться с away.php?
Скорее нет, чем да. Почему? Все очень просто, ВКонтакте набрала такую популярность, что отныне успешно пытается контролировать не только свою социальную сеть, но сторонние сайты. К примеру данный сайт, на котором вы сейчас находитесь, также находится под фильтром ВКонтакте. Пользователь при попытке перехода видит грозное сообщение о том, что на сайте содержатся вирусы. Разумеется, что их здесь нет! Обратившись за разъяснениям к ВКонтакте я получил но честный ответ:
Агент поддержки #531 Максим, вирусов на вашем сайте действительно нет, однако деятельность Вашего сайта, по крайне мере, реклама на нем, не соблюдают правила нашего сайта. Например, распространение программ по накрутке.
Контроллер
Пусть будет контроллер, который по /user выдает имя аутентифицированного пользователя, а по /secret — данные, доступные только аутентифицированному пользователю:
@RestController public class SomeController { @RequestMapping(«/user») public Map user(@AuthenticationPrincipal OAuth2User principal) { return Collections.singletonMap(«name», principal.getName()); } @RequestMapping(«/secret») public String secret() { return «protected data»; } }
Причем /user запрашивается с помощью JavaScript с главной страницы (см. в коде index.html).
WebConfig
Настроим доступ к страницам. Главная страница и статика доступны, остальные url — только для аутентифицированных пользователей. Причем аутентификация осуществляется с помощью OpenID, на это указывает строка .oauth2Login().
На ней и можно было бы поставить точку (точнее, точку с запятой), если бы в логине мог участвовать только GitHub и Google. Но из-за VK мы прописываем пользовательский OAuth2AccessTokenResponseClient для добавления Bearer Token и СustomOAuth2UserService для преобразования ответа с конечной точки Userinfo.
@Configuration public class WebConfig extends WebSecurityConfigurerAdapter { .. @Override protected void configure(HttpSecurity http) throws Exception { // @formatter:off http .authorizeRequests(a -> a .antMatchers(«/», «/error», «/webjars/**»).permitAll() .anyRequest().authenticated() ) .exceptionHandling(e -> e .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) ) .csrf(c -> c .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) .logout(l -> l .logoutSuccessUrl(«/»).permitAll() ) .oauth2Login() //Access token Endpoint .tokenEndpoint() .accessTokenResponseClient(accessTokenResponseClient()) //Userinfo endpoint .and() .userInfoEndpoint() .userService(customOAuth2UserService); // @formatter:on } … }
Вышеуказанные пользовательские классы рассмотрим чуть ниже, а пока настройки GitHub, Google и VK в файле application.yml.
ВК-API блиц
ВК предоставляет несколько реализаций API:
- Streaming/Long Poll — позволяет получить данные в реальном времени. Сразу же, при обновлении данных в ВК, мы получаем актуальную информацию, без обновления страницы. Streaming и Long Poll реализуют разные подходы, но суть их одинаковая.
- Виджеты для сайта — здесь всё просто: скопировал код, без хитрых настроек, и у тебя рабочий виджет на сайте.
- Open API — работает на клиенте. Так же просто настраивается и используется. Преимуществом является то, что не обязательно иметь сервер для выполнения запросов. Будет работать в обычном HTML + JS.
- Callback API — позволяет получать данные от самомого ВК, не выполняя запрос. В случае обновления данных, ВК сам нам присылает новые данные. Например, пользователь подписался на нашу группу, и ВК, автоматически пришлёт информацию об этом пользователе нашему скрипту.
- REST API — позволяет взаимодействовать с данными ВК из скрипта. Позволяет автоматизировать любые пользовательские действия.
Для нас, как для программистов, на данном этапе — интересен REST API. Который позволяет взаимодействовать с пользователями, группами, рекламой, и прочими сущностями. Подробнее, обо всех доступных методах можно почитать здесь.
Для получение доступа к REST API, необходимо иметь специальный ключ vk api token. Это уникальное значение, аналогично логину и паролю — идентифицирует пользователя, от имени которого выполняются запросы.
Как гласит 2 правило проектирования REST API — между запросами не должно сохраняться состояния. Система по определению не может использовать сессии, потому применение токена актуально для идентификации пользователя.
Настройки GitHub, Google и VK в файле application.yml
Здесь client-id и client-secret нужно получить у соответствующего провайдера. Для этого нужно зарегистрировать у провайдера приложение и вставить сюда сгенерированные коды. В VK приложения создаются тут.
spring: thymeleaf: cache: false security: oauth2: client: registration: github: client-id: your-client-id client-secret: your-secret google: client-id: your-client-id client-secret: your-secret vk: client-id: your-client-id client-secret: your-secret redirect-uri: «{baseUrl}/login/oauth2/code/{registrationId}» #use post, as vk needs clientId and clientSecret as request params and does not accepts Basic auth client-authentication-method: post authorization-grant-type: authorization_code scope: email provider: vk: #revoke=1 needs to always show vk dialog authorization-uri: https://oauth.vk.com/authorize?revoke=1 token-uri: https://oauth.vk.com/access_token user-info-uri: https://api.vk.com/method/users.get?v=5.52&fields=photo_max user-name-attribute: first_name google: user-name-attribute: name github: user-name-attribute: login
Про эту настройку уже упоминалось в начале статьи:
spring.security.oauth2.client.registration.vk.client-authentication-method=post
Она переносит Client Credentials в тело запроса.
Выбор имени аутентифицированного пользователя
Обратите внимание на user-name-attribute — это название возвращаемого с Userinfo атрибута пользователя, который превратится в:
principal.getName()
и будет выведен на странице как имя пользователя. Можно посмотреть ответы от каждого провайдера и выбрать подходящий атрибут.
Это имя мы возвращаем в контроллере выше.
Освобождаем Джанго
Оптимальным решением всех выше перечисленных проблем может стать расширение для браузера. По очевидным причинам выбор падает на Chrome. На хабре есть отличная статья статья посвященная написанию расширений для Сhrome.
Для создания такого расширения нам понадобится создать в отдельной папке два файла: json-Manifest и JavaScript-файл для мониторинга текущего url адреса.
Создаем Manifest файл
Главное что нам нужно — дать расширению разрешение на работу с вкладками и назначить исполняемый скрипт: { «manifest_version»: 2, «name»: «Run Away From vk.com/away», «version»: «1.0», «background»: { «scripts»: [«background.js»] }, «permissions»: [«tabs»], «browser_action»: { «default_title»: «Run Away From vk.com/away» } }
Создаем js файл
Здесь всё просто: в событие, вызываемое при создании новой вкладки, добавляем проверку на url адрес, если он начинается на «vk.com/away.php», то заменяем его на правильный, который находится в GET-запросе: chrome.tabs.onCreated.addListener( function (tabId, changeInfo, tab) { chrome.tabs.query({‘active’: true, ‘lastFocusedWindow’: true}, function (tabs) { var url = tabs[0].url; if (url.substr(0,23) == «https://vk.com/away.php»){ var last = url.indexOf(«&», 0) if(last == -1)last = 1000; var url = decodeURIComponent(url.substr(27, last-27)); chrome.tabs.update({url: url}); } }); });
Собираем расширение
Убедившись что оба файла лежат в одной папке, открываем Chrome, выбираем вкладку расширения и нажимаем «Загрузить распакованное расширение». В открывшемся окне выбираем папку написанного расширенная и нажимаем собрать. Готово! Теперь все ссылки вида vk.com/away заменяются на исходные.
Модификация Access Token Response
В настройках WebConfig мы прописали свой accessTokenResponseClient(), вот он:
@Bean public OAuth2AccessTokenResponseClient accessTokenResponseClient() { DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient(); OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter(); tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter()); RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), tokenResponseHttpMessageConverter)); restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); accessTokenResponseClient.setRestOperations(restTemplate); return accessTokenResponseClient; }
Тут для restTemplate указаны два конвертера: FormHttpMessageConverter и OAuth2AccessTokenResponseHttpMessageConverter. И второй использует под капотом наш пользовательский конвертер CustomTokenResponseConverter:
public class CustomTokenResponseConverter implements Converter, OAuth2AccessTokenResponse> { @Override public OAuth2AccessTokenResponse convert(Map tokenResponseParameters) { String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN); OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER; Map additionalParameters = new HashMap<>(); tokenResponseParameters.forEach((s, s2) -> { additionalParameters.put(s, s2); }); return OAuth2AccessTokenResponse.withToken(accessToken) .tokenType(accessTokenType) .additionalParameters(additionalParameters) .build(); } }
Он прописывает «token_type»:»Bearer» для ответов с любого провайдера. Хотя во всех ответах, кроме с VK, «token_type»:»Bearer» и так есть, это не мешает.
Модификация Userinfo Response
Наконец, модифицируем ответ с конечной точки Userinfo. Как было сказано, он возвращается в обертке (см. выше), а нужно, чтобы сразу шли атрибуты, как с Google или Github.
GitHub Userinfo response:
{ «login»: «myluckagain», «id»: 28870472, «node_id»: «MDQ6VXNlcjI4ODcwNDcy», «avatar_url»: «https://avatars.githubusercontent.com/u/28870472?v=4», «gravatar_id»: «», «url»: «https://api.github.com/users/myluckagain», «html_url»: «https://github.com/myluckagain», … «repos_url»: «https://api.github.com/users/myluckagain/repos», «events_url»: «https://api.github.com/users/myluckagain/events{/privacy}», «received_events_url»: «https://api.github.com/users/myluckagain/received_events», «type»: «User», «site_admin»: false, «name»: «sys», «company»: NULL, … } У каждого поставщика свой набор атрибутов, настройка user-name-attribute отвечает за то, какой атрибут будет именем OAuth2User, как было сказано выше.
VK Userinfo response:
{ «response»: [ { «first_name»: «Иван», «id»: 1005684, «last_name»: «Петров», «photo_max»: «https://sun1.is74.userapi.com/s/v1/if2/SM08JDYA4scNcgqgQ5rqoAPsRu-R7CO_k_BneLs6AWDO1mhFVLiwhgtgmgVkh-uvEHXDUrWJ5eiI8FisARAdOj.jpg?size=200×0&quality=96&crop=741,68,736,736&ava=1» } ] }
Итак, для модификации ответа напишем свой CustomOAuth2UserService.
Вообще можно и не писать CustomOAuth2UserService, а вместо этого в качестве значения user-name-attribute прописать единственный доступный атрибут response. Но тогда результат principal.getName() будет странным — весь JSON, находящийся внутри response.
Метод loadUser() собственно и возвращает ответ — в виде OAuth2User.
Выглядит пользовательский класс страшно, но на самом деле это всего лишь копия DefaultOAuth2UserService с блоком if в начале метода:
- если провайдер не VK, то делегируем работу родителю super.loadUser(userRequest), чтобы все шло как обычно;
- если провайдер VK, то делаем то же самое, а в конце извлекаем атрибуты из обертки «response» и из массива.
Класс CustomOAuth2UserService:
@Component public class CustomOAuth2UserService extends DefaultOAuth2UserService { private RestOperations restOperations; private Converter> response; try { response = this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE); } catch (OAuth2AuthorizationException ex) { OAuth2Error oauth2Error = ex.getError(); StringBuilder errorDetails = new StringBuilder(); errorDetails.append(«Error details: [«); errorDetails.append(«UserInfo Uri: «).append( userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()); errorDetails.append(«, Error Code: «).append(oauth2Error.getErrorCode()); if (oauth2Error.getDescription() != NULL) { errorDetails.append(«, Error Description: «).append(oauth2Error.getDescription()); } errorDetails.append(«]»); oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, «An error occurred while attempting to retrieve the UserInfo Resource: » + errorDetails.toString(), NULL); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); } catch (RestClientException ex) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, «An error occurred while attempting to retrieve the UserInfo Resource: » + ex.getMessage(), NULL); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex); } //извлекаем атрибуты из обертки «response» ArrayList valueList = (ArrayList) response.getBody().get(«response»); Map userAttributes = (Map) valueList.get(0); Set authorities = new LinkedHashSet<>(); authorities.add(new OAuth2UserAuthority(userAttributes)); OAuth2AccessToken token = userRequest.getAccessToken(); for (String authority : token.getScopes()) { authorities.add(new SimpleGrantedAuthority(«SCOPE_» + authority)); } return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName); } }
Теперь VK работает, как и остальные провайдеры.
До логина
После логина
Защищенная страница тоже открывается для аутентифицированного пользователя:
Защищенная страница