Описание

Хочу описать логику как с использованием сервиса авторизации 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.

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

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

Комментарии (2)


  1. Bugerman
    18.04.2022 15:32

    1. Вы используете адаптер старой версии;

    2. п. 1 не имеет смысла использовать т.к. в Spring уже есть замечательная реализация для разных провайдеров. Плюс к этому: https://www.keycloak.org/2022/02/adapter-deprecation


    1. miha5418 Автор
      19.04.2022 08:57

      Большое спасибо, за совет, я изучу документацию и постараюсь дополнить данную статью!