- PVSM.RU - https://www.pvsm.ru -
Именно этой фразой нас приветствует библиотека для работы с OAuth — ScribeJava (https://github.com/scribejava/scribejava [1]). Если быть точнее, то фраза звучит так: “Who said OAuth/OAuth2 was difficult? Configuring ScribeJava is so easy your grandma can do it! check it out:”.
И это действительно похоже на правду:
OAuth20Service service = new ServiceBuilder().apiKey(clientId).apiSecret(clientSecret)
.callback("http://your.site.com/callback").grantType("authorization_code").build(HHApi.instance());
String authorizationUrl = service.getAuthorizationUrl();
OAuth2AccessToken accessToken = service.getAccessToken(code);
Готово! Этих трех строчек достаточно, чтобы начать делать OAuth запросы. А сам OAuth запрос можно будет сделать так:
OAuthRequest request = new OAuthRequest(Verb.GET, "https://api.hh.ru/me", service);
service.signRequest(accessToken, request);
String response = request.send().getBody();
Данные о пользователе у нас в руках (в переменной response). И ни капли понимания, как в деталях работает OAuth. Хотим асинхронные http-запросы? Нам хватит тех же трех строчек. Ниже рассмотрим это на примере.
Большинству сайтов в том или ином виде нужна регистрация пользователей: для комментариев, отслеживания заказов, откликов на вакансии — не важно.
При этом в интернете люди обычно демонстрируют ленивое поведение и не любят заполнять регистрационные формы, особенно если они это уже где-то сделали. На помощь таким сайтам приходит OAuth. Однако статья посвящена не самому протоколу OAuth, поэтому мы поговорим о том, как работать с OAuth, не вдаваясь в подробности и механизм его работы.
Если говорить в двух словах, то OAuth создан для того, чтобы давать авторизацию стороннему серверу (сайту) на получение каких-либо данных с другого ресурса (например, соц.сети). Т.е., например, пользователь ВКонтакте с помощью OAuth может дать право какому-нибудь сайту (например, hh.ru) запросить его данные или сделать от его лица какие-либо действия в сети ВКонтакте. Нужно отметить, что OAuth не создан для идентификации пользователя. Однако, в числе прочего, мы почти всегда можем запросить данные “о себе”, таким образом получив id пользователя и идентифицировав его.
Если пошагово попробовать описать OAuth, то получится примерно так (на примере OAuth2 — он попроще).
Попробуем разобрать подробнее пример из начала статьи, сделав OAuth запрос на hh.ru. Для этого нам потребуется создать OAuthService, используя билдер ServiceBuilder. Эта строчка кода будет выглядеть так:
OAuth20Service service = new ServiceBuilder()
.apiKey(clientId)
.apiSecret(clientSecret)
.callback("http://your.site.com/callback")
.grantType("authorization_code")
.build(HHApi.instance());
Вам нужно будет только подставить clientId и clientSecret вашего приложения, которое вы можете получить, зарегистрировав новое приложение на https://dev.hh.ru [2]. Также нужно будет указать callback url, куда будет перенаправлен пользователь с нужным нам кодом (code).
String authorizationUrl = service.getAuthorizationUrl();
Отправляем пользователя на этот адрес. В нашем случае он будет выглядеть примерно так:
hh.ru/oauth/authorize?response_type=code&client_id=UHKBSA...&redirect_uri=https%3A%2F%2Fhhid.ru%2Foauth2%2Fcode [3]
Если открыть этот адрес в браузере, пользователь увидит форму логина, затем форму выдачи прав приложению. Если он уже залогинен и/или давал права приложению ранее, то его сразу же перенаправит на указанный нами callback. В нашем случае такой:
hhid.ru/oauth2/code?code=I2R6O5 [4]…
Вот этот GET параметр ‘code’ нам и нужен. Меняем его на токен:
String code = "I2R6O5...";
OAuth2AccessToken accessToken = service.getAccessToken(code);
На этом все! У нас есть токен (OAuth2AccessToken accessToken), если вывести его в консоль, то увидим внутренности:
OAuth2AccessToken {
access_token=I55KQQ...,
token_type=bearer,
expires_in=1209599,
refresh_token=PGELQV...,
scope=null}
Теперь попробуем получить какие-нибудь данные. Создаем запрос:
OAuthRequest request = new OAuthRequest(Verb.GET, "https://api.hh.ru/me", service);
Подписываем запрос токеном:
service.signRequest(accessToken, request);
Отсылаем запрос на hh.ru:
Response response = request.send();
Выводим результат:
System.out.println(response.getCode());
System.out.println(response.getBody());
Профит! В консоли мы увидим нечто подобное:
200
{"first_name": "Стас", "last_name": "Громов", "middle_name": null, "is_in_search": null, "is_anonymous": false, "resumes_url": null, "is_employer": false, "personal_manager": null, "email": "s.gromov@hh.ru", "manager": null, ...}
И нам не пришлось изучать, какие параметры нужно передавать, как это делать, как их шифровать, в каком порядке передавать, и много других нюансов как самого протокола OAuth, так и ее конкретной имплементации у HeadHunter.
ps. Полный код запускаемого примера можно увидеть здесь:
https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/HHExample.java [5]
Библиотека релизится в maven central, так что подключить ее к проекту будет очень просто:
<dependency>
<groupId>com.github.scribejava</groupId>
<artifactId>scribejava-apis</artifactId>
<version>2.3.0</version>
</dependency>
Если же у вас очень жесткие требования по размеру конечного приложения, то можно взять только core часть, без сборника различных API:
<dependency>
<groupId>com.github.scribejava</groupId>
<artifactId>scribejava-core</artifactId>
<version>2.3.0</version>
</dependency>
Мы получили данные о пользователе, которые можно использовать как для регистрации нового пользователя, так и для процесса аутентификации старого. Это может понадобиться не только новым сайтам, которым лень заморачиваться с регистрацией, но и старым, уже матерым, таким как hh.ru. Именно с этими мыслями мы и вошли в 2013-й год.
Нужно отметить, что, несмотря на существование относительно неплохих спецификаций OAuth протокола, как это обычно водится, каждый сервер изобретал и изобретает свои модификации. Где-то вольно интерпретируя стандарты, где-то пользуясь свободой, предоставляемой спецификациями, а где-то идя против них ради каких-то своих идей.
На hh.ru мы хотели добавить входы сразу через несколько различных соцсетей, а писать код, работающий с каждой из них, естественно, хотелось по минимуму. Наверняка кто-то уже все написал! Как минимум, список адресов, на которые нужно посылать запросы (а по факту и еще кучка мелких нюансов). Кроме того, хотелось бы по минимуму поддерживать написанный код, и, в случае необходимости, когда, например, соцсеть решит поменять урл, на который нужно идти за токеном, просто обновить версию библиотеки.
Мы изучили существовавшие на тот момент варианты, и так вышло, что самой простой в использовании и одновременно с самой большой базой АПИ, с которыми библиотека работала бы из коробки, оказалась scribe-java от https://github.com/fernandezpablo85/ [6]. На тот момент она имела несколько, но не все АПИ, которые мы хотели. Ну не страшно, можно дописать недостающие и отдать их в общее пользование.
С этого мы и начали. Но написав первый наш PullRequest на гитхабе, мы узнали, что автор “устал” уже от таких Пулл Реквестов и ответил нам заготовленной статьей на вики о том, что новые API он добавлять не будет ;-( По мнению автора, scribe-java должна была быть маленькой простой библиотекой (возможно без OAuth2 вовсе, оставив только первую версию протокола), а нам хотелось от нее иметь все же истинно “библиотечные” свойства, сборник всех адресов различных АПИ. Ну и это не страшно, если чем-то какая-то библиотека не устраивает, всегда можно сделать форк! Так и появился на свет проект SubScribe. С заголовком из пяти пунктов, которые обозначали основные причины создания нашего форка:
Main reasons of fork here:
1.https://github.com/fernandezpablo85/scribe-java/wiki/Scribe-scope-revised
2.We really think, OAuth2.0 should be here;
3.We really think, async http should be here for a high-load projects;
4.We really think, all APIs should be here. With all their specific stuff. It's easier to change/fix/add API here, in this lib, one time, instead of N programmers will do the same things on their sides;
5. Scribe should be multi-maven-module project. Core and APIs should be deployed as separated artifacts.
© https://github.com/hhru/subscribe/blob/a8450ec2ed35ecaa64ef03afc1bd077ce14d8d61/README.md [7]
Помимо описанного раннее, из причин создания форка можно выделить еще несколько. Будучи одним из самых нагруженных сайтов рунета и джоб-сайтов Европы, нам очень хотелось иметь возможность асинхронной работы. И это никак не входило в планы простой оригинальной библиотеки. Также мы решили исключить “страх” автора, что библиотека станет громоздкой из-за обилия специфичных каким-то отдельным конкретным АПИ фич. Мы разделили проект на два модуля. После некоторых телодвижений, 3-его марта 2014-го года первая версия SubScribe (сразу 2.0) появилась в центральном репозитории Мавена http://central.maven.org/maven2/ru/hh/oauth/subscribe/subscribe/2.0/ [8]. Где проект и просуществовал до версии 3.4, выпущенной 30-го июня 2015-го года. За это время, набрав немного уже своей собственной популярности и новых фич, новых АПИ, он не забывает бекпортить все вкусности из родителя scribe-java.
Так бы все и оставалось, если бы в начале осени 2015 Pablo Fernandez (https://github.com/fernandezpablo85 [9]), видимо, окончательно устав от своего детища, не наткнулся бы на наш форк. Пабло сказал, что впечатлен им и видит многое, что хотел бы сделать сам, но не дошли руки, и предложил проработать детали передачи проекта полностью нам. Немного помявшись для приличия, мы приняли предложение, и так появился ScribeJava — по сути переименованный обратно форк SubScribe. С этого момента у библиотеки появилась отдельная организация на github.com — https://github.com/scribejava [10].
На данный момент ScribeJava представляет собой open source проект под крылом hh.ru. Входит в перечень клиентских библиотек на java на главной странице официального сайт протокола OAuth2: http://oauth.net/2/ [11]. Имеет 280 наблюдателей, 3 106 звездочек и 1 220 форков на github.com.
Если у вас сильно нагруженный сайт и вы хотите сэкономить потоков и/или просто использовать ning http client, то мы можем попросить ScribeJava использовать асинхронный вариант работы. Для этого нужно, чтобы в вашем classpath присутствовал ning http client
<dependency>
<groupId>com.ning</groupId>
<artifactId>async-http-client</artifactId>
<version>1.9.32</version>
</dependency>
В этот раз будем использовать Асинхронный билдер сервиса ServiceBuilderAsync.
OAuth20Service service = new ServiceBuilderAsync()
.apiKey(clientId)
.apiSecret(clientSecret)
.scope("profile") // replace with desired scope
.state("secret" + new Random().nextInt(999_999))
.callback("https://hhid.ru/oauth2/code")
.asyncHttpClientConfig(clientConfig)
.build(GoogleApi20.instance());
Единственное отличие здесь — вызов метода asyncHttpClientConfig(clientConfig), в который мы должны отдать конфиг для асинхронного ning http клиента. Для примера, пусть он будет таким:
AsyncHttpClientConfig clientConfig = new AsyncHttpClientConfig.Builder()
.setMaxConnections(5)
.setRequestTimeout(10_000)
.setAllowPoolingConnections(false)
.setPooledConnectionIdleTimeout(1_000)
.setReadTimeout(1_000)
.build();
Так же Google требует передачи переменной state. Она необходима для защиты от CRSF атаки, но это за пределами интереса нашей статьи. Дальнейшая работа ничем не отличается от примера работы с api.hh.ru, рассмотренного в начале. Отсылаем пользователя по адресу:
String authorizationUrl = service.getAuthorizationUrl();
А к каждому методу с HTTP походом внутри добавляем постфикс ‘Async’. Т.е. вместо метода getAccessToken будем вызывать метод getAccessTokenAsync.
Future<OAuth2AccessToken> accessTokenFuture = service.getAccessTokenAsync("code", null);
В ответ мы получим Future (асинхронность ведь). Или же можем опционально передать вторым аргументом Callback, как нам удобнее.
Готово! Просто, не правда ли? Теперь можно отсылать асинхронные запросы (OAuthRequestAsync) в гугл от лица пользователя:
OAuth2AccessToken accessToken = accessTokenFuture.get();
OAuthRequestAsync request = new OAuthRequestAsync(Verb.GET, "https://www.googleapis.com/plus/v1/people/me", service);
service.signRequest(accessToken, request);
Response response = request.sendAsync(null).get();
System.out.println(response.getCode());
System.out.println(response.getBody());
У полученного OAuthRequestAsync мы вызвали метод sendAsync, который по аналогии опционально ожидает Callback и возвращает нам Future. При этом у нас остается возможность слать синхронные запросы паралельно с асинхронными. Если же мы хотим как-то профорсить асинхронность (или синхронность) запросов, можно попросить ScribeJava сделать это через статический “конфигуратор”:
ScribeJavaConfig.setForceTypeOfHttpRequests(ForceTypeOfHttpRequest.FORCE_ASYNC_ONLY_HTTP_REQUESTS);
В этом случае, при попытке использовать синхронный вариант ScribeJava, мы будем получать Exception. Возможны и другие варианты, например, не выкидывать Exception, но логировать о каждом таком случае. Или наоборот требовать исключительно синхронной работы.
Рассмотрим здесь еще один полезный момент OAuth — refresh_token. Дело в том, что получаемый нами access_token имеет ограниченый срок жизни. И когда он протухает, нам необходимо получить новый токен. Здесь есть два варианта: либо дождаться пользователя и еще раз провести его через весь механизм, либо использовать refresh_token (его поддерживают не все, но Google, на примере которого мы его попробуем, поддерживает). Итак, для получения свежего access_token все, что нам нужно, это всего лишь:
OAuth2AccessToken refreshedAccessToken accessToken = service.refreshAccessToken(accessToken.getRefreshToken());
или для асинхронного варианта:
Future<OAuth2AccessToken> refreshedAccessTokenFuture = service.refreshAccessTokenAsync(accessToken.getRefreshToken(), null);
Стоит отметить, что в случае с Google refresh_token, который нужно передать в метод refreshAccessToken, не придет, если его специально не попросить. Для этого нужно при формировании адреса, на который пойдет пользователь добавить пару параметров:
//передать access_type=offline чтобы получить refresh_token
//https://developers.google.com/identity/protocols/OAuth2WebServer#preparing-to-start-the-oauth-20-flow
Map<String, String> additionalParams = new HashMap<>();
additionalParams.put("access_type", "offline");
//Google отдаст refresh_token только на первый offline запрос, если нужно еще раз, нужно явно это попросить параметром prompt
additionalParams.put("prompt", "consent");
String authorizationUrl = service.getAuthorizationUrl(additionalParams);
ps. Этот и другие примеры в запускаемом виде (со статическим методом main) тут: https://github.com/scribejava/scribejava/tree/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples [12]
1.ScribeJava на github.com https://github.com/scribejava/scribejava [1]
2.документация api.hh.ru https://github.com/hhru/api [13]
3.документация Google https://developers.google.com/identity/protocols/OAuth2WebServer [14]
4.RFC OAuth2 http://tools.ietf.org/html/rfc6749 [15]
5.javadoc online http://www.javadoc.io/doc/com.github.scribejava/scribejava-core [16]
Комментарии очень приветствуются. Особенно в виде Пулл Реквестов на github.com
Автор: HeadHunter
Источник [17]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/115014
Ссылки в тексте:
[1] https://github.com/scribejava/scribejava: https://github.com/scribejava/scribejava
[2] https://dev.hh.ru: https://dev.hh.ru
[3] hh.ru/oauth/authorize?response_type=code&client_id=UHKBSA...&redirect_uri=https%3A%2F%2Fhhid.ru%2Foauth2%2Fcode: https://hh.ru/oauth/authorize?response_type=code&client_id=UHKBSA...&redirect_uri=https%3A%2F%2Fhhid.ru%2Foauth2%2Fcode
[4] hhid.ru/oauth2/code?code=I2R6O5: https://hhid.ru/oauth2/code?code=I2R6O5
[5] https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/HHExample.java: https://github.com/scribejava/scribejava/blob/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples/HHExample.java
[6] https://github.com/fernandezpablo85/: https://github.com/fernandezpablo85/
[7] https://github.com/hhru/subscribe/blob/a8450ec2ed35ecaa64ef03afc1bd077ce14d8d61/README.md: https://github.com/hhru/subscribe/blob/a8450ec2ed35ecaa64ef03afc1bd077ce14d8d61/README.md
[8] http://central.maven.org/maven2/ru/hh/oauth/subscribe/subscribe/2.0/: http://central.maven.org/maven2/ru/hh/oauth/subscribe/subscribe/2.0/
[9] https://github.com/fernandezpablo85: https://github.com/fernandezpablo85
[10] https://github.com/scribejava: https://github.com/scribejava
[11] http://oauth.net/2/: http://oauth.net/2/
[12] https://github.com/scribejava/scribejava/tree/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples: https://github.com/scribejava/scribejava/tree/master/scribejava-apis/src/test/java/com/github/scribejava/apis/examples
[13] https://github.com/hhru/api: https://github.com/hhru/api
[14] https://developers.google.com/identity/protocols/OAuth2WebServer: https://developers.google.com/identity/protocols/OAuth2WebServer
[15] http://tools.ietf.org/html/rfc6749: http://tools.ietf.org/html/rfc6749
[16] http://www.javadoc.io/doc/com.github.scribejava/scribejava-core: http://www.javadoc.io/doc/com.github.scribejava/scribejava-core
[17] Источник: https://habrahabr.ru/post/278957/
Нажмите здесь для печати.