Всем привет! Меня зовут Иван Зыков, я старший Java разработчик в компании X5 Tech. За моими плечами больше 5 лет опыта разработки.
Хочу познакомить вас с модулем аутентификации Micronaut и заодно продемонстрировать, как настроить OAuth2.0 у нескольких провайдеров.
Немного вводных
Micronaut – это современный JVM фреймворк, который в данный момент активно разрабатывается. У него есть несколько интересных, на мой взгляд, преимуществ:
Относительно новый. Это значит, что ещё не успела появиться куча различных условностей для обратной совместимости и т. п. Но, в то же время, он уже стабильный, и крупные компании уже используют его (например, Mojang).
Супербыстрый. Особенно если сравнивать со Spring. Быстрый старт приложения при разработке позволяет быть более сконцентрированным на написании кода, а не в ожидании, когда же запустится программа. И такой же быстрый старт уже на стенде, что позволяет легче управлять количеством инстансов.
Обширная документация, живой Gitter (можно напрямую с разработчиками пообщаться), чистый код. Всё это делает переход на
Micronautлёгким и приятным занятием.
-
Какие провайдеры будут?
Google (OpenID)
Yandex
-
Что потребуется:
JDK 8+
Micronaut 3.7.0+
Ваш любимый редактор кода
Традиционные 15 минут свободного времени
Конфигурация Micronaut
Чтобы собрать проект на Micronaut, можно использовать несколько инструментов:
Micronaut Launch – сайт для сбора приложения, аналогичный Spring Initializr.
CLI – Command Line Interface от Micronaut.
SDKMAN! – инструмент для параллельного менеджмента различных версий одного и того же SDK на одной машине (чаще всего UNIX).
Я буду использовать первый вариант, потому что мне он кажется самым удобным, особенно для первого запуска. Для этого заходим на сайт и добавляем нужные фичи будущего приложения. В рамках текущей статьи нужно выбрать три зависимости:
security;
security-jwt;
security-oauth2.
Также я использую Lombok, поскольку он позволяет уменьшить количество Boilerplate-кода. Так что можете добавить его тоже.
Осталось выбрать версию Java и название проекта. Choose what you want.
В итоге должно получиться что-то вроде этого:

Соответственно, остаётся только нажать кнопку Generate Project, и сайт предложит скачать его или выложить на GitHub. Тут репозиторий на эту статью с примерами кода.
Также давайте добавим некий контроллер, который позволит нам поприветствовать только что аутентифицированного пользователя:
@Controller
@Secured(SecurityRule.IS_AUTHENTICATED)
public class MainController {
@Get
public String greeting(Authentication authentication) {
return "Hello, " + authentication.getName() + "!";
}
}
Реализация провайдеров
Для реализации аутентификации через сторонних провайдеров необходимо для начала зарегистрировать у них приложение, которое и будет осуществлять механизм идентификации пользователя через внешнюю систему. Первым на очереди будет Google. Во-первых, это один из самых популярных сервисов, которые есть в мире, и скорее всего, у большинства ваших потенциальных пользователей есть Google-аккаунт. Во-вторых, он реализует особую спецификацию OAuth2.0 под названием OpenID. Вот хорошая статья, которая рассказывает об этих стандартах.
Ну а мы приступаем к написанию аутентификации через Google.
Подробно расписывать получение клиента для аутентификации от Google я не вижу смысла. Как минимум, там относительно простой и интуитивно понятный интерфейс, к тому же в интернете уже есть подробное описание всех действий.
Стоит сказать, что нужно внимательно отнестись к callback url. В процессе аутентификации провайдер сходит на ваш сервер, чтобы узнать, не являетесь ли вы злоумышленником. Сейчас нужно выставить http://localhost:8080/oauth/callback/google, но на проде советую так не делать.

После этого надо переместиться в application.yml и прописать client-secret и client-id, которые показываются после создания Credentials.
micronaut:
security:
oauth2:
clients:
google:
client-id: your-client-id
client-secret: your-client-secret
openid:
issuer: https://accounts.google.com
В принципе, самый простой вариант уже готов, и вы можете попробовать запустить наше приложение, а затем через браузер сходить на адрес http://localhost:8080/oauth/login/google.
Но можно также сделать более тонкую настройку, например, выдачи ролей пользователю. Для этого потребуется написать UserDetailsMapper:
@Named("google") // Bean должен иметь данную аннотацию с тем же значением, которое было написано в файле конфигураций
@Singleton
public class OpenIdUserDetailsMapper implements OpenIdAuthenticationMapper {
@Override
@NonNull
public AuthenticationResponse createAuthenticationResponse(
String providerName,
OpenIdTokenResponse tokenResponse,
OpenIdClaims openIdClaims,
@Nullable State state
) {
return AuthenticationResponse.success(
openIdClaims.getName(),
Collections.singletonList("ROLE_GOOGLE")
);
}
}
На всякий случай уточню, что создание такого обработчика опционально для OpenID провайдера. Теперь, если перейти по контроллеру, который мы объявили ранее, сначала надо будет подтвердить своё желание войти через Google, а после увидеть приветствие от приложения:
Hello, Иван Зыков!
Другими примерами провайдеров, реализующих OpenID, могут выступать GitHub, Okta, KeyCloack.
Yandex
Приступим к следующему провайдеру. Для начала также необходимо создать клиента в системе Yandex. Для этого можно прочитать инструкцию. На самом деле, процесс схож с Google. Вводим имя приложения, его тип, добавляем необходимые для вашего приложения доступы, а также определяем callback url. В результате должно получиться примерно так:

Следующий шаг – заполнить необходимые параметры в application.yml. Так как Yandex реализует только OAuth2.0, а не OpenID, нужно указать больше параметров, чем в предыдущем разделе.
Минимально необходимой конфигурацией для OAuth2.0 приложения в Micronaut является:
установить параметр endpoint'а для авторизации;
установить параметр endpoint'а для получения токена;
добавить
client-idиclient-secret, полученные ранее;реализация
OauthAuthenticationMapper.
Таким образом, получится следующая запись:
yandex:
client-id: ${YANDEX_CLIENT_ID}
client-secret: ${YANDEX_CLIENT_SECRET}
authorization:
url: https://oauth.yandex.ru/authorize
token:
url: https://oauth.yandex.ru/token
auth-method: client-secret-post
scopes: # этот параметр опционален, его указывать необязатльно
- "login:birthday"
- "login:email"
- "login:info"
- "login:avatar"
Вы могли заметить, что появился ещё один новый параметр, который до этого в статье не упоминался. Это token.auth-method – свойство, отвечающее за то, как будет аутентифицироваться наше приложение при выпуске токена в провайдере.
Всего в Micronaut существуют 7 способов, найти их можно тут. Вообще, подробной информации о методах аутентификации нет в стандарте RFC 6749, который как раз и описывает работу OAuth2.0. Каждый волен делать, как хочет. Однако, большинство провайдеров используют client_secret_post, в котором client-id и client-secret передаются в теле запроса, либо же используется client-secret-basic, где те же параметры передаются в виде Basic аутентификации.
После того, как были внесены необходимые данные в application.yml, Micronaut требует, чтобы был реализован OauthAuthenticationMapper. Давайте к этому и приступим. Данная реализация должна иметь специальную аннотацию @Named, в которой значение должно совпадать с именем провайдера, указанного в конфигурационном файле.
Задача этого маппера – сконвертировать TokenResponse в Authentication. В дальнейшем это приведёт к тому, что будет происходить вызов некоторого API у провайдера для того, чтобы получить информацию о пользователе. Как только она будет получена, будут созданы user details в соответствии с написанным кодом.
Чаще всего это используется для того, чтобы скомбинировать данные от провайдера с уже существующими записями в БД, либо же создать новую запись и дать пользователю права, аватарку, ник и т. д. В Authentication будут храниться следующие стандартные свойства: username, roles и attributes. Далее эти данные будут доступны из любого контроллера, который принимает Autentication в качестве параметра.
Для начала нужно создать класс, представляющий данные пользователя от провайдера:
@Introspected
@Data
@AllArgsConstructor
@NoArgsConstructor
public class YandexUser {
private String id;
@JsonProperty("first_name")
private String firstName;
@JsonProperty("last_name")
private String lastName;
@JsonProperty("display_name")
private String nickName;
@JsonProperty("default_email")
private String email;
}
Потом нужно сделать HttpClient для выполнения запроса:
@Header(name = "User-Agent", value = "micronaut")
@Client("https://login.yandex.ru")
public interface YandexApiClient {
@Get("/info")
Flowable<YandexUser> getUser(@Header("Authorization") String authorization);
}
И финальный шаг – создание user details mapper, который при использовании клиента сможет выписать пользователю Authentication:
@Named("yandex") // Bean должен иметь данную аннотацию с тем же значением, которое было написано в файле конфигураций
@Singleton
@RequiredArgsConstructor
public class YandexUserDetailsMapper implements OauthAuthenticationMapper {
private final YandexApiClient yandexApiClient;
@Override
public Publisher<AuthenticationResponse> createAuthenticationResponse(
TokenResponse tokenResponse, @Nullable State state) {
return Flux.from(yandexApiClient.getUser("OAuth " + tokenResponse.getAccessToken()))
.map(user -> {
List<String> roles = Collections.singletonList("ROLE_YANDEX");
return AuthenticationResponse.success(user.getNickName(), roles);
});
}
}
Проверить работу можно аналогично Google, просто сходить по адресу http://localhost:8080/oauth/login/yandex, и, подтвердив своё желание аутентифицироваться через Yandex, увидеть приветствие.
Процесс аутентификации
Настало время поговорить о том, как устроена аутентификация Micronaut внутри. Отчасти, мы уже немного затронули эту тему, когда создавали пользователя через UserDetailsMapper. Существует объект Authentication, который хранит данные о пользователе. Но что же происходит под капотом? Давайте разберём!
Для начала установим логгирование на уровень trace. Я использую logback, так что я просто добавлю такую строчку:
<logger name="io.micronaut.security" level="trace"/>
Запустим и посмотрим, что появится в консоли:
DEBUG i.m.s.o.e.e.r.EndSessionEndpointResolver - Resolving the end session endpoint for provider [google]. Looking for a bean with the provider name qualifier
DEBUG i.m.s.o.e.e.r.EndSessionEndpointResolver - No EndSessionEndpoint bean found with a name qualifier of [google]
DEBUG i.m.s.o.e.e.r.EndSessionEndpointResolver - No EndSessionEndpoint can be resolved. The issuer for provider [google] does not match any of the providers supported by default
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering login route [GET: /oauth/login/vk] for oauth configuration [vk]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering callback route [GET: /oauth/callback/vk] for oauth configuration [vk]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering callback route [POST: /oauth/callback/vk] for oauth configuration [vk]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering login route [GET: /oauth/login/yandex] for oauth configuration [yandex]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering callback route [GET: /oauth/callback/yandex] for oauth configuration [yandex]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering callback route [POST: /oauth/callback/yandex] for oauth configuration [yandex]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering login route [GET: /oauth/login/google] for oauth configuration [google]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering callback route [GET: /oauth/callback/google] for oauth configuration [google]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Registering callback route [POST: /oauth/callback/google] for oauth configuration [google]
DEBUG i.m.s.o.routes.OauthRouteBuilder - Skipped registration of logout route. No openid clients found that support end session
Первое, что можно увидеть – EndSessionEndpointResolver. Этот бин отвечает, как не сложно догадаться, за определение контроллера, с помощью которого пользователь сможет завершить свою OpenID сессию. Тут же можно увидеть, что для Google такой Endpoint найти не удалось, соответственно, он не будет зарегистрирован.
Следующий шаг – это OauthRouteBuilder. Он служит для того, чтобы зарегистрировать контроллеры для всех провайдеров, которые будут настроены. Я добавил ещё один провайдер, так что теперь будут создаваться 9 роутов, по одному для инициализации аутентификации и по два на callback для каждого провайдера.
Для начала посмотрим, как поведет себя Micronaut, когда получит запрос без аутентификации.
DEBUG i.m.s.t.reader.HttpHeaderTokenReader - Looking for bearer token in Authorization header #1
DEBUG i.m.s.t.reader.DefaultTokenResolver - Request GET, /, no token found. #2
DEBUG i.m.security.rules.IpPatternsRule - One or more of the IP patterns matched the host address [127.0.0.1]. Continuing request processing. #3
DEBUG i.m.s.rules.AbstractSecurityRule - None of the given roles [[isAnonymous()]] matched the required roles [[isAuthenticated()]]. Rejecting the request #4
DEBUG i.m.security.filters.SecurityFilter - Unauthorized request GET /. The rule provider io.micronaut.security.rules.SecuredAnnotationRule rejected the request. #5
DEBUG i.m.s.a.DefaultAuthorizationExceptionHandler - redirect uri: / #6
Быстро посмотрим шаг за шагом, что происходит:
Проверка наличия
bearerтокена в заголовкеAuthorization.Сообщение вида «Какой тип запроса, путь, информация о токене».
В процессе конфигурации безопасности можно указать, с каких ip-адресов можно принимать запросы (по умолчанию можно с любых). Здесь просто происходит проверка, что запрос пришёл с разрешённого адреса.
-
Происходит проверка, есть ли у пользователя, который пришёл на
/, нужная роль. В этом случае просто достаточно, чтобы он был аутентифицирован. На данный момент это условие не выполняется.Вообще,
AbstractSecurityRuleвыглядит очень логично. Если в объектеAuthenticationнет ничего, тоMicronautсообщает, что у данного пользователя есть толькоisAnonymous(). Потом, когда уже происходит вызов методаcompareRoles, фреймворк просто считывает, что есть только роль анонимуса.
SecurityFilterсообщает текущую ситуацию и какой провайдер правил «принял» такое решение.Дальше идёт просто сообщение о том, что происходит редирект на заранее определённый путь. Настраивается через конфигурацию.
Далее, для примера, я буду аутентифицироваться через Yandex. Сначала большой разницы не будет, так что я это опущу. Давайте перейдём сразу к интересным моментам.
DEBUG i.m.s.rules.AbstractSecurityRule - The given roles [[isAnonymous()]] matched one or more of the required roles [[isAnonymous()]]. Allowing the request
DEBUG i.m.security.filters.SecurityFilter - Authorized request GET /oauth/login/yandex. The rule provider io.micronaut.security.rules.SecuredAnnotationRule authorized the request.
TRACE i.m.s.o.r.DefaultOauthController - Received login request for provider [yandex]
TRACE i.m.s.o.client.DefaultOauthClient - Starting authorization code grant flow to provider [yandex]. Redirecting to [https://oauth.yandex.ru/authorize]
TRACE i.m.s.o.e.a.r.DefaultAuthorizationRedirectHandler - Built the authorization URL [https://oauth.yandex.ru/authorize?...]
Первая большая разница: данный контроллер принимает анонимных пользователей, что логично, ведь мы хотим залогиниться.
Далее следует информация о том, что получен запрос на логин через Yandex.
DefaultOauthClientсообщает о начале authorization code grant flow для Yandex.В следующем шаге
DefaultAuthorizationRedirectHandlerсообщает о том, что построен специальный авторизационный URL.
Что происходит после? Пользователь получает ответ от сервера, где есть информация о редиректе на этот URL. Там необходимо подтвердить своё желание войти в наше приложение через провайдера (в данном случае, через Yandex).
В следующей пачке логов наконец-то произойдёт момент аутентификации в нашем приложении! Опять же, будут и стандартные логи, которые мы уже разобрали, так что предлагаю их пропустить и перейти к интересным моментам:
TRACE i.m.s.o.r.DefaultOauthController - Received callback from oauth provider [yandex]
TRACE i.m.s.o.client.DefaultOauthClient - Received a successful authorization response from provider [yandex]
TRACE i.m.s.o.e.a.r.DefaultOauthAuthorizationResponseHandler - Validating state found in the authorization response from provider [yandex]
TRACE i.m.s.o.e.t.r.DefaultTokenEndpointClient - Sending request to token endpoint [https://oauth.yandex.ru/token]
TRACE i.m.s.o.e.t.r.DefaultTokenEndpointClient - The token endpoint supports [[client_secret_post]] authentication methods
TRACE i.m.s.o.e.t.r.DefaultTokenEndpointClient - Using client_secret_post authentication. The client_id and client_secret will be present in the body
TRACE i.m.s.o.c.c.p.ClientCredentialsHttpClientFilter - Did not find any OAuth 2.0 client which should decorate the request with an access token received from client credentials request
TRACE i.m.s.o.e.a.r.DefaultOauthAuthorizationResponseHandler - Token endpoint returned a success response. Creating a user details
TRACE i.m.s.o.c.c.p.ClientCredentialsHttpClientFilter - Did not find any OAuth 2.0 client which should decorate the request with an access token received from client credentials request
TRACE i.m.s.o.r.DefaultOauthController - Authentication succeeded. User [Иван Зыков] is now logged in
Сообщение о том, что получен
callbackот провайдера Yandex.Дальше фреймворк сообщает, что получил успешный авторизационный ответ от провайдера.
Логично, что после получения успешного ответа, нужно провалидировать, что же там хранится. Сейчас
stateхранится вCookieпользователя, и нужно сравнить их значения, чтобы убедиться, что ответ получен у верного пользователя.Следующие три строчки говорят о том, что будет сделан запрос на получение токена пользователя от провайдера. Куда, какого типа будет запрос и где будут переданы
client_secretиclient_id. Собственно, именно для этого ранее был реализованYandexApiClient. Возможно, возникнет вопрос: ведь в этом клиенте используетсяGetзапрос, а тут написано, что для аутентификации используетсяclient_secret_post. Но всё достаточно просто. Перед тем, как предоставить информацию о пользователе, провайдеру нужно убедиться, что у нас есть на это право! И для этого наше приложение аутентифицируется у Yandex, получает токен, и только после этого идёт за информацией о пользователе.Применяя различные декораторы (если они есть), приложение обращается к провайдеру уже за данными человека. Если всё пройдет успешно, в этот момент включится написанный ранее
YandexUserDetailsMapperи вернёт уже аутентифицированного юзера.
Заключение
Надеюсь, что эта статья сможет вам помочь в понимании процессов, которые происходят во время аутентификации пользователя, если вы используете Micronaut. Как можно заметить, события разворачиваются достаточно просто и прямолинейно. Исходники Micronaut написаны достаточно хорошо, чтобы читать их, как книгу. Документация также является достаточно ясной, хоть и на русском её нет.
Кстати, есть вот такая интересная статья про Micronaut, советую её прочитать.
excentro
Супербыстрый?? По времени старта - может быть. А в среднем, если верить techempower, micronaut на последнем месте из java фреймворков и гораздо ниже spring. Я вообще не понимаю, зачем нужен micronaut - аналог spring, только хуже. Я бы смотрел в сторону quarkus - как по мне - гораздо интересней.