ScribeJava — навіть ваша бабуся зможе працювати з OAuth

image

Саме цією фразою нас вітає бібліотека для роботи з OAuth — ScribeJava (https://github.com/scribejava/scribejava). Якщо бути точніше, то фраза звучить так: “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-запити? Нам вистачить тих же трьох рядків. Нижче розглянемо це на прикладі.

1. OAuth? Про що це?
Більшості сайтів в тому чи іншому вигляді потрібна реєстрація користувачів: для коментарів, відстеження замовлень, відгуків на вакансії — не важливо.

При цьому в інтернеті люди зазвичай демонструють ліниве поведінку і не люблять заповнювати реєстраційні форми, особливо якщо вони це вже зробили. На допомогу таким сайтам приходить OAuth. Проте стаття присвячена не самому протоколу OAuth, тому ми поговоримо про те, як працювати з OAuth, не вдаючись у подробиці та механізм його роботи.

Якщо говорити в двох словах, то OAuth створений для того, щоб давати авторизацію сторонньому сервера (сайту) на отримання яких-небудь даних з іншого ресурсу (наприклад, соц.мережі). Тобто, наприклад, користувач ВКонтакте за допомогою OAuth може дати право якого-небудь сайту (наприклад, hh.ru) запросити його дані або зробити від його особи будь-якої дії в мережі ВКонтакте. Потрібно відзначити, що OAuth не створений для ідентифікації користувача. Однак, в числі іншого, ми майже завжди можемо запросити дані «про себе», таким чином отримавши id користувача і ідентифікував його.

Якщо покроково спробувати описати OAuth, то вийде приблизно так (на прикладі OAuth2 — він простіше).
  1. Ми реєструємо свою програму на сторонньому сайті, отримуємо client_id і client_secret — це робиться один раз.
  2. Коли до нас приходить користувач, наш сайт формує посилання на сторонній сайт, на якому ми хочемо отримати авторизацію на отримання даних про користувача. У засланні обов'язково фігурує client_id нашої програми на цьому сайті. Далі наш сайт дає користувачеві цю посилання.
  3. Користувач йде посилання на сторонній сайт, логинится (якщо ще не залягання), схвалює запитувані нами права (наприклад, отримання його ПІБ) і повертається до нас з додатковим GET-параметр 'code'.
  4. Наш сайт напряму (сервер-сервер) відсилає отриманий GET параметр на сторонній сайт і у відповідь отримує токен (access_token).
  5. Ми робимо запити на отримання даних або вчинення будь-яких активностей від імені користувача в кожен запит додаємо цей access_token.
2. Навіть ваша бабуся зможе використовувати OAuth
Спробуємо розібрати докладніше приклад з початку статті, зробивши запит на 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. Також потрібно буде вказати 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
Якщо відкрити цю адресу у браузері, користувач побачить форму логіна, потім форму видачі прав додатком. Якщо він вже залягання та/або давав права додатком раніше, то його відразу ж перенаправить на вказаний нами callback. У нашому випадку такий:
hhid.ru/oauth2/code?code=I2R6O5
Ось цей 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

Бібліотека релизится в 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>


3.scribe-java -> SubScribe -> ScribeJava або як зробити форк і повернути борг opensource спільноти
Ми отримали дані про користувача, які можна використовувати як для реєстрації нового користувача, так і для процесу аутентифікації старого. Це може знадобитися не тільки новим сайтам, яким ліньки морочитися з реєстрацією, але і старим, вже досвідченим, таким як hh.ru. Саме з цими думками ми й увійшли в 2013-й рік.

Потрібно відзначити, що, незважаючи на існування відносно непоганих специфікацій протоколу OAuth, як це зазвичай водиться, кожен сервер винаходив і винаходить свої модифікації. Десь вільно інтерпретуючи стандарти, десь користуючись свободою, що надається специфікаціями, а де-то йдучи проти них заради якихось своїх ідей.

На hh.ru ми хотіли додати входи відразу через кілька різних соцмереж, а писати код, який працює з кожною з них, природно, хотілося по мінімуму. Напевно хтось вже все написав! Як мінімум, список адрес, на які потрібно посилати запити (а по факту і ще купка дрібних нюансів). Крім того, хотілося б по мінімуму підтримувати написаний код, і, в разі необхідності, коли, наприклад, соцмережа вирішить поміняти урл, на який треба йти за токеном, просто оновити версію бібліотеки.

Ми вивчили існували на той момент варіанти, і так вийшло, що самій простий у використанні і одночасно з найбільшою базою АПІ, з якими бібліотека працювала б з коробки, виявилася scribe-java від https://github.com/fernandezpablo85/. На той момент вона мала кілька, але не всі АПІ, які ми хотіли. Ну не страшно, можна дописати відсутні і віддати їх в загальне користування.

З цього ми і почали. Але написавши перший наш 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

Крім описаного раннє, з причин створення форк можна виділити ще кілька. Будучи одним з найбільш навантажених сайтів рунета і джоб-сайтів Європи, нам дуже хотілося мати можливість асинхронної роботи. І це ніяк не входило в плани простий оригінальної бібліотеки. Також ми вирішили виключити «страх» автора, що бібліотека стане громіздкою з-за великої кількості специфічних якимось окремим конкретним АПІ функцій. Ми розділили проект на два модулі. Після деяких рухів, 3-його березня 2014-го року перша версія SubScribe (відразу 2.0) з'явилася в центральному сховищі Мавена http://central.maven.org/maven2/ru/hh/oauth/subscribe/subscribe/2.0/. Де проект і проіснував до версії 3.4, випущеної 30-го червня 2015-го року. За цей час, набравши трохи вже своєї власної популярності і нових функцій, нових АПІ, він не забуває бекпортить всі смаколики з батьків scribe-java.

Так би все було, якби на початку осені 2015 Pablo Fernandez (https://github.com/fernandezpablo85), мабуть, остаточно втомившись від свого дітища, не наштовхнувся б на наш форк. Пабло сказав, що вражений ним і бачить багато, що хотів би зробити сам, але не дійшли руки, і запропонував опрацювати деталі передачі проекту повністю нам. Трохи пом'явшись для пристойності, ми прийняли пропозицію, і так з'явився ScribeJava — по суті перейменований назад форк SubScribe. З цього моменту у бібліотеки з'явилася окрема організація на github.com — https://github.com/scribejava.

На даний момент ScribeJava являє собою open source проект під крилом hh.ru. Входить в перелік клієнтських бібліотек на java на головній сторінці офіційного сайт протоколу OAuth2: http://oauth.net/2/. Має 280 спостерігачів, 3 106 зірочок і 1 220 форков на github.com.

4. Додамо асинхронності та оновимо токен на прикладі роботи з Google
Якщо у вас сильно навантажений сайт і ви хочете заощадити потоків і/або просто використовувати 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

5. Корисні посилання
1.ScribeJava на github.com https://github.com/scribejava/scribejava
2.документація api.hh.ru https://github.com/hhru/api
3.документація Google https://developers.google.com/identity/protocols/OAuth2WebServer
4.RFC OAuth2 http://tools.ietf.org/html/rfc6749
5.javadoc online http://www.javadoc.io/doc/com.github.scribejava/scribejava-core

Post Scriptum
Коментарі дуже вітаються. Особливо у вигляді Пулл Реквестов на github.com

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.