Описание
Хочу описать логику как с использованием сервиса авторизации 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.
Всем кто дочитал спасибо!
Если у кого есть замечания и предложения, готов выслушать.
Bugerman
Вы используете адаптер старой версии;
п. 1 не имеет смысла использовать т.к. в Spring уже есть замечательная реализация для разных провайдеров. Плюс к этому: https://www.keycloak.org/2022/02/adapter-deprecation
miha5418 Автор
Большое спасибо, за совет, я изучу документацию и постараюсь дополнить данную статью!