Русский
Русский
English
Статистика
Реклама

Блог компании reksoft

Внедрение E2E-тестирования с Puppeteer и Jest

18.02.2021 10:20:32 | Автор: admin

Привет, Хабр!

Хотим поделиться краткой историей о том, как мы на одном из проектов Рексофт пришли к написанию автотестов, и почему сделали акцент именно на e2e-тестах.

Речь пойдет о работе с порталом крупного федерального заказчика, работающего в сфере B2B и B2C, имеющего серьезный трафик из всех регионов. Сайт писал не Рексофт. Ресурс попал к нам на поддержку какое-то время назад. Изначально проект содержал довольно много легаси кода, тестов не было, как и документации. Требовалось сделать редизайн по новым макетам, параллельно расширяя существующую функциональность. Обычная история.

В какой-то момент мы поняли, что ручное прокликивание интерфейса стало занимать очень много времени, и решили, что пришло время для автотестов.

Перед выбором подхода к автотестам классифицировали баги по типу: их большая часть касалась именно бизнес-логики (Javascript). Багов, связанных с кроссбраузерностью или вёрсткой, было не очень много.

Итак, цель была поставлена автоматизировать проверку бизнес-логики, т.е. заполнение и отправку форм, работу слайдеров и т.д.

В качестве базового фреймворка для тестов был выбран Jest, а для имитации действий пользователя Puppeteer. Последний выбрали благодаря тому, что он поддерживается Google и имеет отличную документацию. Нам было вполне достаточно тестов в одном браузере (Chrome) (хотя, есть инструменты, позволяющие с помощью Puppeteer запускать тесты и в других браузерах, например, Firefox и IE). Playwright тогда ещё не было, а Selenium, чисто субъективно, казался более сложным в развертывании.

Далее кратко опишу преимущества и недостатки e2e-тестов.

Недостатки e2e-тестов

Относительно высокая сложность написания. Нужно обладать определёнными навыками, чтобы написать компактный и легкочитаемый e2e-тест.

Проблема закопаться. Я точно знаю, что желание проверить всё и вся, может привести к написанию огромного количества избыточных тестов. Как побороть эту проблему? Не стоит пытаться протестировать абсолютно все возможные кейсы, достаточно сосредоточиться на основных.

Низкая скорость прохождения тестов (в сравнении с юнит-тестами). Довольно много времени тратится на запуск браузера, ожидание загрузки страницы, завершения анимаций и прочее. Для ускорения прохождения тестов потребуется свести к минимуму число пользовательских действий, а также число загрузок новых страниц.

Хрупкость e2e-тестов. Что я имею ввиду? Случай, когда тест падает без видимых на то причин, а при повторном запуске успешно выполняется, или же когда к падению теста приводит незначительное изменение в верстке. С хрупкостью тестов можно бороться с помощью автоматического перезапуска теста после падения. Помогут в этом раннер jest-circus и параметр retryTimes. Это практически полностью решает проблему ложных падений тестов.

Преимущества e2e-тестов

Гарантии. С e2e-тестами вы получаете определенные гарантии того, что система в целом работает корректно. Юниты такой гарантии не дают. Разумеется, это не значит, что вы получаете 100% гарантию того, что у вас работает абсолютно всё. Но вы будете уверены, что по крайней мере протестированные сценарии точно работают.

Снижение рабочей нагрузки. Без тестов разработчик вынужден думать как бы чего не сломать. Ведь не прокликивать же весь проект после очередного коммита с правками! С тестами вы узнаете о поломке еще до того, как код будет залит на стенд.

Ускорение разработки. Благодаря снижению количества багов и устранению ручных проверок интерфейса, серьезно экономится время. Причём e2e-тесты экономят не только ваше время, но и время тестировщиков (позволяя им заводить меньше багов, меньше коммуницировать, выясняя их детали и т. д.), время аналитиков (понятные тесты отличная документация, отвечать на вопросы аналитиков вы будете намного быстрее).

Ускорение рефакторинга. При рефакторинге js, e2e-тесты не придется переписывать. Да и рефакторинг в вёрстке тоже редко приводит к их переписыванию.

Нет зависимости от кодовой базы и js-фреймворка. Вы можете написать e2e-тесты даже к легаси коду, что поможет быстрее его отрефакторить. При этом вы практически не зависите от js-фреймворка, т.к. в тестах оперируете только сущностями браузера.

Лайфхаки

1. Для ускорения написания первых тестов можно записывать действия пользователя с помощью Headless recorder или с помощью встроенных в Chrome инструментов.

2. Если требуется проверить несколько наборов данных в рамках одного кейса и руки тянутся написать for внутри теста, используйте jest-each. Эта библиотека позволит прогнать несколько наборов данных в одном кейсе, сохранив читаемость кода:

each` url | selector | expectedIsVisible ${'/page1.html?land'} | ${'.js-order-button'} | ${true} ${'/page1.html'} | ${'.js-order-button'} | ${false} ${'/page2.html?land'} | ${'.js-order-button'} | ${true} ${'/page2.html'} | ${'.js-order-button'} | ${false}`.it('Отображение кнопок заказа на странице $url', async ({ url, selector, expectedIsVisible }) => { await page.goto(`${SERVER_URL}${url}`, {   waitUntil: 'networkidle2',   timeout: 0 }); expect(await isVisible(page, selector)).toEqual(expectedIsVisible);});

3. В случае возникновения ошибок, puppeteer не всегда корректно подсвечивает строку с ошибкой. Поэтому следует проверить код перед ошибочной строкой, часто ошибка кроется именно там.

4. Puppeteer позволяет запускать браузер с различными флагами, что иногда очень полезно (chrome://flags/). Ниже пример запуска браузера с отключенным флагом SameSite by default cookies

puppeteer.launch({ headless: true, slowMo: 80, args: [   '--disable-features=SameSiteByDefaultCookies' ]});

В качестве заключения

Написание e2e-тестов занимают около 5-10% от времени написания фичи. На фиксы багов уходило сильно больше времени, так что оно того стоило. Несмотря на ряд недостатков, e2e-тесты позволяют значительно ускорить процесс разработки и обрести уверенность в том, что основная функциональность системы работает.

Разумеется, при внедрении e2e-тестов для фронтенда необходимо учитывать специфику вашего проекта. Спасибо за внимание!

Подробнее..

Подключение Keycloak к Spring Boot приложению

14.04.2021 12:16:12 | Автор: admin

Привет Хабр!

Как известно, spring OAuth2.0.x переведен в режим поддержки уже почти как 2 года назад , а большая часть его функциональности теперь доступна в spring-security (матрица сопоставления). В spring-security отказались переносить Authorization service (roadmap) и предлагают использовать вместо него свободные или платные аналоги, в частности keycloak. В этом посте мы хотели бы поделится различными вариантами подключения keycloak к приложениям spring-boot.

Содержание

Немного о Keycloak

Это реализация SSO (Single sign on) с открытым исходным кодом для управления идентификацией и доступом пользователей.

Основной функционал, поддерживаемый в Keycloak:

  • Single-Sign On and Single-Sign Out.

  • OpenID/OAuth 2.0/SAML.

  • Identity Brokering аутентификация с помощью внешних OpenID Connect или SAML.

  • Social Login поддержка Google, GitHub, Facebook, Twitter.

  • User Federation синхронизация пользователей из LDAP и Active Directory серверов.

  • Kerberos bridge использование Kerberos сервера для автоматической аутентификации пользователей.

  • Гибкое управление политиками через realm.

  • Адаптеры для JavaScript, WildFly, JBoss EAP, Fuse, Tomcat, Jetty, Spring.

  • Возможность расширения с использованием плагинов.

  • И многое-многое другое...

Запускаем и настраиваем keycloak

Для запуска keycloak на машине разработчика удобно использовать docker-compose. В этом случае мы можем в разное время для разных приложений запускать свой сервис авторизации, тем самым избавляя себя от кучи проблем, связанных с конфигурацией под различные приложения. Ниже приведен один из вариантов конфигурации docker-compose для запуска standalone сервера с базой данных postgres:

docker-compose.yml
version: "3.8"services:  postgres:    container_name: postgres    image: library/postgres    environment:      POSTGRES_USER: ${POSTGRES_USER:-postgres}      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}      POSTGRES_DB: keycloak_db    ports:      - "5432:5432"    restart: unless-stopped  keycloak:    image: jboss/keycloak    container_name: keycloak    environment:      DB_VENDOR: POSTGRES      DB_ADDR: postgres      DB_DATABASE: keycloak_db      DB_USER: ${POSTGRES_USER:-postgres}      DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres}      KEYCLOAK_USER: admin      KEYCLOAK_PASSWORD: admin_password    ports:      - "8484:8080"    depends_on:      - postgres    links:      - "postgres:postgres"

После успешного запуска необходимо произвести настройки realm, клиентов, ролей и пользователей.

Произведем некоторые первоначальные настройки. Создадим realm "my_realm":

После этого создадим клиент "my_client", через который будем производить авторизацию пользователей (оставим все настройки по-умолчанию):

Не забываем указывать redirect_url. В нашем случае он будет равен: http://localhost:8080/*

Создадим роли для пользователей нашей системы - "ADMIN", "USER":

Добавляем пользователей "admin" с ролью "ADMIN":

И пользователя "user" с ролью "USER". Не забываем устанавливать пароли на вкладке "Credentials":

Основная настройка закончена, теперь можно приступить к подключению spring boot приложений.

Подключаем Keycloak при помощи адаптера

В официальной документации к keycloak для использования в приложениях рекомендуют использовать готовые библиотеки - адаптеры, которые дают возможность избавиться от boilerplate кода и излишнего конфигурирования. Есть реализация для большинства популярных языков и фреймворков (supported-platforms). Мы будем использовать Spring Boot Adapter.

Создадим небольшое демонстрационное, приложение на spring-boot (исходники можно найти здесь) и подключим к нему Keycloak Spring Boot адаптер. Конфигурационный файл maven будет выглядеть так:

pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>" xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.9.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><groupId>org.akazakov.keycloak</groupId><artifactId>demo-keycloak-adapter</artifactId><version>0.0.1-SNAPSHOT</version><name>Demo Keycloak Adapter</name><description>Demo project for Spring Boot and Keycloak</description><properties><java.version>11</java.version></properties><dependencyManagement><dependencies><dependency><groupId>org.keycloak.bom</groupId><artifactId>keycloak-adapter-bom</artifactId><version>12.0.3</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.keycloak</groupId><artifactId>keycloak-spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies></project>

Для целей проверки добавим контроллер, который будет выставлять методы для различных ролей пользователей и информацию о текущем пользователе (этот же контроллер мы будем использовать в других примерах ниже):

@RestController@RequestMapping("/api")public class SampleController {    @GetMapping("/anonymous")    public String getAnonymousInfo() {        return "Anonymous";    }    @GetMapping("/user")    @PreAuthorize("hasRole('USER')")    public String getUserInfo() {        return "user info";    }    @GetMapping("/admin")    @PreAuthorize("hasRole('ADMIN')")    public String getAdminInfo() {        return "admin info";    }    @GetMapping("/service")    @PreAuthorize("hasRole('SERVICE')")    public String getServiceInfo() {        return "service info";    }    @GetMapping("/me")    public Object getMe() {        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();        return authentication.getName();    }}

Чтобы наше приложение успешно запустилось и подключилось к keycloak, нам необходимо добавить соответствующую конфигурацию. Первое, что мы сделаем, это в application.yml добавим настройки клиента и подключения к серверу авторизации:

server:  port: ${SERVER_PORT:8080}spring:  application.name: ${APPLICATION_NAME:spring-security-keycloak}keycloak:  auth-server-url: http://localhost:8484/auth  realm: my_realm  resource: my_client  public-client: true

После этого добавим конфигурацию spring-security, переопределим KeycloakWebSecurityConfigurerAdapter, поставляемый вместе с адаптером:

@KeycloakConfiguration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {    @Override    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {        return new NullAuthenticatedSessionStrategy();    }    @Autowired    public void configureGlobal(AuthenticationManagerBuilder authManagerBuilder) {        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());        authManagerBuilder.authenticationProvider(keycloakAuthenticationProvider);    }    @Bean    public KeycloakConfigResolver keycloakConfigResolver() {        return new KeycloakSpringBootConfigResolver();    }    @Override    protected void configure(HttpSecurity http) throws Exception {        super.configure(http);        http                .authorizeRequests()                .antMatchers("/api/anonymous/**").permitAll()                .anyRequest().fullyAuthenticated();    }}

Теперь проверим работу нашего приложения. Запустим приложение и попробуем зайти пользователем на соответствующий url. Например: http://localhost:8080/api/admin. В результате, браузер перенаправит нас на окно логина пользователя:

После ввода корректного имени пользователя и пароля, браузер перенаправит нас на изначальный адрес. В результате получим страницу с некоторой информацией, доступной пользователю:

Если перейдем по адресу получения информации о текущем пользователе (http://localhost:8080/api/me), то получим в результате uuid пользователя в keycloak:

Если нам нужно, чтобы сервис только проверял токен доступа и не инициализировал процедуру аутентификации пользователя, достаточно включить bearer-only: true в конфигурацию приложения:

keycloak:  auth-server-url: http://localhost:8484/auth  realm: my_realm  resource: my_client  public-client: true  bearer-only: true

Используем OAuth2 Client из spring-security

Использование keycloak адаптера избавляет нас от написания кучи boilerplate кода. Но в то же время наше приложение становится зависимым от реализации. В некоторых случаях не стоит завязываться на какой-то конкретный сервис авторизации, это даст нам больше гибкости в дальнейшей эксплуатации системы.

Одной из ключевых особенностей spring security 5 является поддержка протоколов OAuth2 и OIDC. Мы можем использовать OAuth2 клиент из пакета spring-security для интеграции с сервером keycloak.

Итак, для использования клиента подключим соответствующую библиотеку в зависимости от проекта (исходный код примера). Полный текст pom.xml:

pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"         xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">    <modelVersion>4.0.0</modelVersion>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.3.9.RELEASE</version>        <relativePath/> <!-- lookup parent from repository -->    </parent>    <groupId>org.akazakov.keycloak</groupId>    <artifactId>demo-keycloak-oauth</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>demo-keycloak-oauth</name>    <description>Demo project for Spring Boot OAuth and Keycloak</description>    <properties>        <java.version>11</java.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-oauth2-client</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-security</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.springframework.security</groupId>            <artifactId>spring-security-test</artifactId>            <scope>test</scope>        </dependency>    </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>            </plugin>        </plugins>    </build></project>

Далее в application.yaml необходимо указать параметры подключения к сервису авторизации:

server:  port: ${SERVER_PORT:8080}spring:  application.name: ${APPLICATION_NAME:spring-security-keycloak-oauth}  security:    oauth2:      client:        provider:          keycloak:            issuer-uri: http://localhost:8484/auth/realms/my_realm        registration:          keycloak:            client-id: my_client

По умолчанию роли пользователей будут вычисляться на основе значения "scope" в access token, и к ним прибавляется "ROLE_USER" для всех авторизованных пользователей системы. Можно оставить как есть и перейти на модель scope. Но в нашем примере мы будем использовать роли пользователей в рамках realm'а. Все, что нам нужно, это переопределить oidcUserService и задать свой маппинг ролей для пользователя. Нужные роли приходят в разделе "groups" токена доступа, его мы и будем использовать для определения ролей пользователя. В результате, наша конфигурация для spring security с переопределенным oidcUserService будет выглядеть так:

@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .authorizeRequests(authorizeRequests -> authorizeRequests                        .antMatchers("/api/anonymous/**").permitAll()                        .anyRequest().authenticated())                .oauth2Login(oauth2Login -> oauth2Login                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint                                .oidcUserService(this.oidcUserService())                        )                );    }    @Bean    public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {        final OidcUserService delegate = new OidcUserService();        return (userRequest) -> {            OidcUser oidcUser = delegate.loadUser(userRequest);            final Map<String, Object> claims = oidcUser.getClaims();            final JSONArray groups = (JSONArray) claims.get("groups");            final Set<GrantedAuthority> mappedAuthorities = groups.stream()                    .map(role -> new SimpleGrantedAuthority(("ROLE_" + role)))                    .collect(Collectors.toSet());            return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());        };    }}

В данном случае работа приложения будет практически аналогична работе с использованием keycloak адаптера.

Подключаем приложение как ResourceService

Довольно часто не нужно, чтобы наше приложение инициировало аутентификацию пользователя. Достаточно лишь проверки авторизации пользователя по предоставляемому токену доступа. Вариантом подключения авторизации с keycloak без использования адаптера является настройка приложения как resource server. В этом случае приложение не может инициировать аутентификацию пользователя, а только авторизует пользователя и проверяет подпись токена доступа. Подключим соответствующие библиотеки: spring-security-oauth2-resource-server и spring-security-oauth2-jose (исходный код). Полный файл pom.xml будет выглядеть так:

pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.9.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.akazakov.keycloak</groupId><artifactId>demo-keycloak-resource</artifactId><version>0.0.1-SNAPSHOT</version><name>demo-keycloak-resource</name><description>Demo project for Spring Boot and Spring security and Keycloak</description><properties><java.version>11</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

Далее нам необходимо указать путь к JWK (JSON Web Key) набору ключей, с помощью которых наше приложение будет проверять токены доступа. В keycloak они доступны по адресу: http://${host}/auth/realms/${realm)/protocol/openid-connect/certs. В итоге application.yml будет выгдядеть следующим образом:

server:  port: ${SERVER_PORT:8080}spring:  application.name: ${APPLICATION_NAME:spring-security-keycloak-resource}  security:    oauth2:      resourceserver:        jwt:          jwk-set-uri: ${KEYCLOAK_REALM_CERT_URL:http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/certs}

Как и в случае с OAuth2 Client нам также необходимо переопределить конвертер ролей пользователя. В данном случае мы можем переопределить jwtAuthenticationConverter.

Полный текст WebSecurityConfiguration:

@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {    @Override    protected void configure(HttpSecurity http) throws Exception {        http                .authorizeRequests(authorizeRequests -> authorizeRequests                        .antMatchers("/api/anonymous/**").permitAll()                        .anyRequest().authenticated())                .oauth2ResourceServer(resourceServerConfigurer -> resourceServerConfigurer                        .jwt(jwtConfigurer -> jwtConfigurer                                .jwtAuthenticationConverter(jwtAuthenticationConverter()))                );    }    @Bean    public Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter());        return jwtAuthenticationConverter;    }    @Bean    public Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter() {        JwtGrantedAuthoritiesConverter delegate = new JwtGrantedAuthoritiesConverter();        return new Converter<>() {            @Override            public Collection<GrantedAuthority> convert(Jwt jwt) {                Collection<GrantedAuthority> grantedAuthorities = delegate.convert(jwt);                if (jwt.getClaim("realm_access") == null) {                    return grantedAuthorities;                }                JSONObject realmAccess = jwt.getClaim("realm_access");                if (realmAccess.get("roles") == null) {                    return grantedAuthorities;                }                JSONArray roles = (JSONArray) realmAccess.get("roles");                final List<SimpleGrantedAuthority> keycloakAuthorities = roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());                grantedAuthorities.addAll(keycloakAuthorities);                return grantedAuthorities;            }        };    }}

Здесь мы создаем конвертер (jwtGrantedAuthoritiesConverter), который принимает токен и извлекает из секции "realm_access" роли пользователя. Далее мы можем либо сразу вернуть их, либо, как в данном случае, расширить список, который извлекается конвертером по умолчанию.

Проверим работу. Воспользуемся встроенным в Intellij idea http клиентом, либо плагином к VSCode - Rest Client. В начале получим токен пользователя, произведем запрос к keycloak, используя логин и пароль зарегистрированного пользователя:

###POST <http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/token>Content-Type: application/x-www-form-urlencodedclient_id=my_client&grant_type=password&scope=openid&username=admin&password=admin> {% client.global.set("auth_token", response.body.access_token); %}

Ответ будет примерно следующего содержания:

Ответ
POST <http://localhost:8484/auth/realms/my_realm/protocol/openid-connect/token>HTTP/1.1 200 OK...Content-Type: application/json{  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlb21qWFY2d3dNek8xVS0tYUdhVllpSHM3eURaZVM1aU96bl9JR3RlS1ZzIn0.eyJleHAiOjE2MTY2NTQzNjEsImlhdCI6MTYxNjY1NDA2MSwianRpIjoiMGQwMjg2YWUtYTlmYy00MzcxLWFmM2ItZjJlNTM5N2I4NzViIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjkzMGIxMTNmLWI0NzUtNDhkMC05NTQxLWMyYzI2MWZlYmRmZCIsInR5cCI6IkJlYXJlciIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJhY3IiOiIxIiwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwiQURNSU4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.dvGvYhhhfH8r6EP8k_spFwBS35ulYMTWNL4lcz9PR2e-p4FU-ehre1EQA8xpbkYzYEWRB_elzTya5IhbYR8KArrujplIDNAOlqJ9W6a4Tx-r44QCteM0DW4BNzbZAH2L0Bg7aSstRKUuULceRNYQcdCvSFjEU5DsHk26a6TM5KCrkv0ryGo11pam-pnbs2Z2jOSfSHvOAfMNL9OVJYRBjlTmsEzzgH9dHSa_pT2Q-SvgvfCcwfY0XkgUZkMPUtz85-lqchROb4XpHOiy3Cfn8MgrGNwhf-MsmN5wiAGe0DI_LW2Jxr3boZMLS4AuuNQ7agr65g-JuO9-LhlgndxN8g",  "expires_in": 300,  "refresh_expires_in": 1800,  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmNGEwNWQxNy0yNWU4LTRjMjEtOTMyMC0zMzcwODlhNTg5MjQifQ.eyJleHAiOjE2MTY2NTU4NjEsImlhdCI6MTYxNjY1NDA2MSwianRpIjoiMjNmNDBiZWUtNmQ3Ny00ZTIxLTg0NTItNDg1NDc2OTk1ZDUyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo4NDg0L2F1dGgvcmVhbG1zL215X3JlYWxtIiwic3ViIjoiOTMwYjExM2YtYjQ3NS00OGQwLTk1NDEtYzJjMjYxZmViZGZkIiwidHlwIjoiUmVmcmVzaCIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIn0.r4BrjwfavKFF8dst3AyRi0LTfymbSVfDKDT9KyMpmzk",  "token_type": "bearer",  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJlb21qWFY2d3dNek8xVS0tYUdhVllpSHM3eURaZVM1aU96bl9JR3RlS1ZzIn0.eyJleHAiOjE2MTY2NTQzNjEsImlhdCI6MTYxNjY1NDA2MSwiYXV0aF90aW1lIjowLCJqdGkiOiJiN2UwNDhmZS01ZTRjLTQxMWYtYTBjMC0xNGExYzhlOGJhYWEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg0ODQvYXV0aC9yZWFsbXMvbXlfcmVhbG0iLCJhdWQiOiJteV9jbGllbnQiLCJzdWIiOiI5MzBiMTEzZi1iNDc1LTQ4ZDAtOTU0MS1jMmMyNjFmZWJkZmQiLCJ0eXAiOiJJRCIsImF6cCI6Im15X2NsaWVudCIsInNlc3Npb25fc3RhdGUiOiI1ZDI5ZDQ2ZS1iOTI2LTRkNTktODlmOC0yNDM2ZWRjYWU0ZjAiLCJhdF9oYXNoIjoiRlh2VzB2Z3pwd3R6N1FabEZtTFhJdyIsImFjciI6IjEiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.ZDeZg4Z-PPmn2fVm7opGLRutzDh6l8uRYqZzbqIX7wk0GhgtMHV1CW8RvDd51AuYw81WyoMyRAD_-T6ne58Rt9f5XNZZfS8xoXzTFV1xH6XigOVQH2jIHN-2VIM1IgJnteo7nuTz9zo4OXIFvEjaFHq4AXDkiq6jhThv0qPS3WrAA-MutyW8G37GM0fsCgANvlGKoWm1_1wKyeTZ0Gfug32Vf6gUikfxA9bmaS4oGYGc6lqFE6EHgtjIn0q9gNUfpEXaqpiL3mCBu9V6sJG5Rp_MOqp-aXrM9NbLTz2JTXevtClHI6qVUIoh8OXXXT98QmKrVr9Cyr9BRUrQyt0Zzg",  "not-before-policy": 0,  "session_state": "5d29d46e-b926-4d59-89f8-2436edcae4f0",  "scope": "openid profile email"}Response code: 200 (OK); Time: 114ms; Content length: 2987 bytes

Теперь проверим, что методы доступны пользователю с соответствующими правами:

GET <http://localhost:8080/api/admin>Authorization: Bearer {{auth_token}}Content-Type: application/json

В ответ получим:

GET <http://localhost:8080/api/admin>HTTP/1.1 200 ...admin infoResponse code: 200; Time: 34ms; Content length: 10 bytes

Авторизация вызовов сервисов с использованием keycloak

При работе с микросервисной архитектурой иногда возникают требования авторизованных вызовов между сервисами. В случаях, когда инициатором взаимодействия является какой-то внутренний процесс или служба, нам где-то нужно брать токен доступа. В качестве решения данного вопроса мы можем использовать Client Credentials Flow, чтобы получить токен из keycloak (исходный код примера доступен по ссылке).

Для начала создадим нового клиента, под которым будут авторизоваться наши сервисы:

Для возможности авторизации сервиса нам нужно изменить тип доступа ("Access Type") на "confidential" и включить флаг "Service accounts Enabled". В остальном конфигурация не отличается от конфигурации по умолчанию:

Если нам необходимо, чтобы у сервисов, авторизованных под данным клиентом, была своя роль, добавим ее в роли:

Далее эту роль необходимо добавить клиенту. На вкладке "Service Account Roles" выбираем необходимую роль - в нашем случае роль "SERVICE":

Сохраняем client_id и client_secret для дальнейшего использования в сервисах для авторизации:

Для демонстрации создадим небольшое приложение, которое будет получать информацию доступную по адресу http://localhost:8080/api/service из предыдущих примеров.

Для начала создадим компонент, который будет авторизовывать наш сервис в keycloak:

@Componentpublic class KeycloakAuthClient {    private static final Logger log = LoggerFactory            .getLogger(KeycloakAuthClient.class);    private static final String TOKEN_PATH = "/token";    private static final String GRANT_TYPE = "grant_type";    private static final String CLIENT_ID = "client_id";    private static final String CLIENT_SECRET = "client_secret";    public static final String CLIENT_CREDENTIALS = "client_credentials";    @Value("${app.keycloak.auth-url:http://localhost:8484/auth/realms/my_realm/protocol/openid-connect}")    private String authUrl;    @Value("${app.keycloak.client-id:service_client}")    private String clientId;    @Value("${app.keycloak.client-secret:acb719cf-4afd-42d3-91f2-93a60b3f2023}")    private String clientSecret;    private final RestTemplate restTemplate;    public KeycloakAuthClient(RestTemplate restTemplate) {        this.restTemplate = restTemplate;    }    public KeycloakAuthResponse authenticate() {        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();        paramMap.add(CLIENT_ID, clientId);        paramMap.add(CLIENT_SECRET, clientSecret);        paramMap.add(GRANT_TYPE, CLIENT_CREDENTIALS);        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);        String url = authUrl + TOKEN_PATH;        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(paramMap, headers);        log.info("Try to authenticate");        ResponseEntity<KeycloakAuthResponse> response =                restTemplate.exchange(url,                        HttpMethod.POST,                        entity,                        KeycloakAuthResponse.class);        if (!response.getStatusCode().is2xxSuccessful()) {            log.error("Failed to authenticate");            throw new RuntimeException("Failed to authenticate");        }        log.info("Authentication success");        return response.getBody();    }}

Метод authenticate производит вызов к keycloak и в случае успешного ответа возвращает объект KeycloakAuthResponse:

public class KeycloakAuthResponse {    @JsonProperty("access_token")    private String accessToken;    @JsonProperty("expires_in")    private Integer expiresIn;    @JsonProperty("refresh_expires_in")    private Integer refreshExpiresIn;    @JsonProperty("refresh_token")    private String refreshToken;    @JsonProperty("token_type")    private String tokenType;    @JsonProperty("id_token")    private String idToken;    @JsonProperty("session_state")    private String sessionState;    @JsonProperty("scope")    private String scope;    // Getters and setters or lombok ...}

Далее мы берем access_token из ответа для того, чтобы использовать в дальнейших вызовах к защищенным методам. Ниже пример вызова к защищенному методу:

@SpringBootApplicationpublic class DemoServiceAuthApplication implements CommandLineRunner {    private static final String BEARER = "Bearer ";    private static final String SERVICE_INFO_URL = "http://localhost:8080/api/service";    private final KeycloakAuthClient keycloakAuthClient;    private final RestTemplate restTemplate;    private static final Logger log = LoggerFactory            .getLogger(DemoServiceAuthApplication.class);    public DemoServiceAuthApplication(KeycloakAuthClient keycloakAuthClient, RestTemplate restTemplate) {        this.keycloakAuthClient = keycloakAuthClient;        this.restTemplate = restTemplate;    }    public static void main(String[] args) {        SpringApplication.run(DemoServiceAuthApplication.class, args);    }    @Override    public void run(String... args) {        final KeycloakAuthResponse authenticate = keycloakAuthClient.authenticate();        HttpHeaders headers = new HttpHeaders();        headers.setContentType(MediaType.APPLICATION_JSON);        headers.setBearerAuth(authenticate.getAccessToken());        log.info("Make request to resource server");        final ResponseEntity<String> responseEntity = restTemplate.exchange(SERVICE_INFO_URL, HttpMethod.GET, new HttpEntity(headers), String.class);        if (!responseEntity.getStatusCode().is2xxSuccessful()) {            log.error("Failed to request");            throw new RuntimeException("Failed to request");        }        log.info("Response data: {}", responseEntity.getBody());    }}

Сначала мы авторизуем наш сервис через keycloak, потом производим запрос к защищенному ресурсу, добавив в HTTP Headers параметр Authorization: Bearer ...

В результате выполнения программы мы получим содержимое защищенного метода:

.   ____          _            __ _ _ /\\\\ / ___'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\ \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )  '  |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot ::                (v2.4.4)2021-04-13 16:04:36.672  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Starting DemoServiceAuthApplication using Java 14.0.1 on MacBook-Pro.local with PID 19240 (/Users/akazakov/Projects/spring-boot-keycloak/demo-service-auth/target/classes started by akazakov in /Users/akazakov/Projects/spring-boot-keycloak)2021-04-13 16:04:36.674  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : No active profile set, falling back to default profiles: default2021-04-13 16:04:37.199  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Started DemoServiceAuthApplication in 0.814 seconds (JVM running for 6.425)2021-04-13 16:04:37.203  INFO 19240 --- [           main] o.akazakov.keycloak.KeycloakAuthClient   : Try to authenticate2021-04-13 16:04:53.697  INFO 19240 --- [           main] o.akazakov.keycloak.KeycloakAuthClient   : Authentication success2021-04-13 16:04:53.697  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Make request to resource server2021-04-13 16:04:54.088  INFO 19240 --- [           main] o.a.keycloak.DemoServiceAuthApplication  : Response data: service infoDisconnected from the target VM, address: '127.0.0.1:57479', transport: 'socket'Process finished with exit code 0

Конечно, представленный в примере выше строго в ознакомительных целях KeycloakAuthClient нельзя использовать в продуктовой среде, как минимум нужно добавить поддержку сохранения токена доступа на некоторое время, а еще лучше поддержку механизма обновления токена доступа при истечении его срока действия.

Выводы

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

Спасибо за внимание!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru