KeyCloak и Spring Boot

от автора

Описание

Хочу описать логику как с использованием сервиса авторизации Keycloak настроить авторизацию при этом получая token и refreshToken , а так-же обменивать refreshToken на новый token.

Мы такую логику использовали при работе с фронтом. Выставляли срок действия token 15 минут и когда он был просрочен, можно было обновить его при помощи refreshToken.

Запуск и настройка keycloak

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

  postgres:     container_name: postgres     image: library/postgres:12     environment:       POSTGRES_USER: postgres       POSTGRES_PASSWORD: postgres       POSTGRES_DB: crm     ports:       - "5432:5432"     volumes:       - ${WORK_DATA}/database:/var/lib/postgresql/data       - ./initdb:/docker-entrypoint-initdb.d     restart: unless-stopped    keycloak:     image: jboss/keycloak     container_name: keycloak     environment:       DB_VENDOR: POSTGRES       DB_ADDR: postgres       DB_DATABASE: crm       DB_SCHEMA: keycloak       DB_USER: postgres       DB_PASSWORD: postgres       KEYCLOAK_USER: admin       KEYCLOAK_PASSWORD: admin     ports:       - "8484:8080"     depends_on:       - postgres

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

Создадим realm «first_realm».

Создадим клиент «my_app», через который будем производить авторизацию пользователей. Так как мы хотим получать Token нужно настроить:
Access Type = «confidential»
Authorization Enable «ON»
После сохранения появиться вкладка тут нас интересует Secret, но это позже.

Указываем valid redirect Urls В нашем случае он будет равен: http://localhost:8080/*

Создадим роли для пользователей нашей системы — «ADMIN», «USER»

Добавляем пользователей «admin» с ролью «ADMIN»:
И пользователя «user» с ролью «USER». Не забываем устанавливать пароли на вкладке «Credentials». Сначала создаём пользователей, потом их настраиваем.

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

Создадим приложение на spring-boot и подключим к нему Keycloak Spring Boot адаптер. Файл maven будет выглядеть так:

    <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>  <dependencys>         <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.keycloak</groupId>             <artifactId>keycloak-admin-client</artifactId>             <version>${org.keycloak.admin-client.version}</version>         </dependency> </dependencys>

Добавим Контроллер для авторизации и получения Token

@Controller @RequiredArgsConstructor public class AuthController {      private final AuthService authService;      @PostMapping("/auth/login")      @PreAuthorize("permitAll()")     public ResponseEntity<LoginResponseMessage> login(String email, String pass) {)         val responseMessage = authService.login(loginRequestMessage);         return ResponseEntity.status(HttpStatus.OK)                 .body(responseMessage);    } }

Так же сервис для этих целей

@Slf4j @Service @RequiredArgsConstructor public class AuthService {      private final AuthzClient authzClient;      public LoginResponseMessage login(String email, String pass) {         log.info("START login for user {}", email);         try {             val response = authzClient.authorization(email, pass)                     .authorize();             val result = new LoginResponseMessage()                     .tokenType(response.getTokenType())                     .token(response.getToken());             log.info("FINISH login for user {} successfully", email)             return result;         } catch (AuthorizationDeniedException | HttpResponseException ex) {             log.debug("Exception when login {}", email, ex);             log.info("FINISH login for user {} is bad", email);             throw new BadAuthorizeException();         } catch (Exception ex) {             log.error("Some error occurred during login");             throw new BadAuthorizeException();         }     } }
public class LoginResponseMessage   {   @JsonProperty("token")   private String token;    @JsonProperty("refreshToken")   private String refreshToken;    @JsonProperty("tokenType")   private String tokenType;   }

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

@Slf4j @Controller public class ClientController {      @PostMapping("/client/add")     @PreAuthorize("hasRole('ADMIN')")     public ResponseEntity<ClientInfo> addNewClient(@Valid ClientInfo clientInfo) {         log.info("Call method addNewClient");         return ResponseEntity.status(HttpStatus.OK)                 .body("Client add");     }      @GetMapping("/client/{clientId}")     @PreAuthorize("hasRole('USER')")     public ResponseEntity<ClientInfo> getClientInfoById(Integer clientId) {         val result = clientService.getClientById(clientId);          return ResponseEntity.status(HttpStatus.OK)                 .body("CLIENT");     } }

Для запуска приложения и подключения к keycloak,
нам необходимо добавить соответствующую конфигурацию.
В application.yml добавим настройки клиента и подключения к серверу авторизации,
вот тут нас интересует поле secret во вкладке Credentials

keycloak:   authServerUrl: http://localhost:8484/auth   realm: first_realm   resource: my_app   credentials:     secret: S63XNDWRT8i4DlsKhBgTJdO94fasd

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

@KeycloakConfiguration @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = 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.cors().and().csrf().disable();         http                 .authorizeRequests()                 .antMatchers("/auth/**").permitAll()                 .anyRequest().fullyAuthenticated()                 .and()                 .exceptionHandling()                 .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));     } }

Делаем настройки Keycloak

@Configuration public class KeycloakConfiguration {      @Bean     public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {         return new KeycloakSpringBootConfigResolver();     }      @Bean     public AuthzClient keycloakAuthzClient(KeycloakSpringBootProperties props) {         val config = new org.keycloak.authorization.client.Configuration(                 props.getAuthServerUrl(), props.getRealm(),                 props.getResource(), props.getCredentials(), null);          return AuthzClient.create(config);     }      @Bean     public Keycloak keycloak(KeycloakSpringBootProperties props) {         return KeycloakBuilder.builder()                 .serverUrl(props.getAuthServerUrl())                 .realm(props.getRealm())                 .grantType(OAuth2Constants.CLIENT_CREDENTIALS)                 .clientId(props.getResource())                 .clientSecret((String) props.getCredentials().get("secret"))                 .build();     } }

После этого мы можем обратиться к эндпоинту /auth/login передать туда логин и пароль,
а в ответ получить token и refreshToken

Время актуальности Token мы можем выставлять в настройках realm во вкладке tokens

Когда наш Token устарел, его нужно обновить, для этого нам и нужен
refreshToken. Добавляем в наш контроллер ещё один метод. Теперь делаем запрос на Новый эдпоинт /auth/tokenRefresh и передаём туда наш refreshToken.

    @PostMapping("/auth/tokenRefresh")     @PreAuthorize("permitAll()")     public ResponseEntity<LoginResponseMessage> tokenRefresh(String refreshToken) {         val responseMessage = authService.tokenRefresh(refreshToken);         return ResponseEntity.status(HttpStatus.OK)                 .body(responseMessage);     }

И добавляем реализацию обновления token в наш сервис.

   @Transactional     public LoginResponseMessage tokenRefresh(String refresh) {         log.info("START tokenRefresh");         try {             String url = authzClient.getConfiguration().getAuthServerUrl() + "/realms/" + authzClient.getConfiguration().getRealm() + "/protocol/openid-connect/token";             String clientId = authzClient.getConfiguration().getResource();             String secret = (String) authzClient.getConfiguration().getCredentials().get("secret");             val http = new Http(authzClient.getConfiguration(), (params, headers) -> {             });              val response = http.<AccessTokenResponse>post(url)                     .authentication()                     .client()                     .form()                     .param("grant_type", "refresh_token")                     .param("refresh_token", refresh)                     .param("client_id", clientId)                     .param("client_secret", secret)                     .response()                     .json(AccessTokenResponse.class)                     .execute();              val result = new LoginResponseMessage()                     .tokenType(response.getTokenType())                     .token(response.getToken())                     .refreshToken(response.getRefreshToken());             log.info("FINISH tokenRefresh");             return result;         } catch (AuthorizationDeniedException | HttpResponseException ex) {             log.debug("Exception when tokenRefresh", ex);             log.info("FINISH tokenRefresh is bad");             throw new BadAuthorizeException();         }     }

И на выходе мы опять получаем действующий token и refreshToken.

Всем кто дочитал спасибо!

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


ссылка на оригинал статьи https://habr.com/ru/articles/661541/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *