Описание
Хочу описать логику как с использованием сервиса авторизации 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».
![](https://habrastorage.org/getpro/habr/upload_files/35a/f53/baa/35af53baab6a7d1bd5da7ff0bee9841d.png)
Создадим клиент «my_app», через который будем производить авторизацию пользователей. Так как мы хотим получать Token нужно настроить:
Access Type = «confidential»
Authorization Enable «ON»
После сохранения появиться вкладка тут нас интересует Secret, но это позже.
Указываем valid redirect Urls В нашем случае он будет равен: http://localhost:8080/*
![](https://habrastorage.org/getpro/habr/upload_files/5df/233/1cc/5df2331cc763c1a778af4c0958dddf33.png)
Создадим роли для пользователей нашей системы — «ADMIN», «USER»
![](https://habrastorage.org/getpro/habr/upload_files/5de/33d/d04/5de33dd0433d396ea73ca964297d7c34.png)
Добавляем пользователей «admin» с ролью «ADMIN»:
И пользователя «user» с ролью «USER». Не забываем устанавливать пароли на вкладке «Credentials». Сначала создаём пользователей, потом их настраиваем.
![](https://habrastorage.org/getpro/habr/upload_files/61b/6bb/946/61b6bb946d27559565449660cc756319.png)
Подключаем 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/
Добавить комментарий