Обеспечение безопасности безопасного микропрофиля RestClientBuilder с помощью Keycloak - PullRequest
0 голосов
/ 25 февраля 2020

У меня есть следующие настройки: Keycloak 9.0.0 работает на порту 8180 Серверное приложение Spring Boot, работающее на порте 8080 Демо-клиентское приложение, использующее CxfTypeSafeClientBuilder для доступа к сервису сервера

Взаимодействие Keycloak - Spring Boot работает нормально, и я могу получать токены от Keycloak, и демонстрационный сервис проверяет токен, если я передаю его как заголовок Authorization.

Как настроить CxfTypeSafeClientBuilder / RestClientBuilder для обработки токенов JWT, которые я получаю из экземпляра Keycloak? Нужно ли мне создавать свои собственные ClientResponseFilter, если да, то как обращаться с токенами с истекшим сроком? Существуют ли какие-либо реализации / стандарты, которые я не нашел?

Интерфейс веб-сервиса JAX-RS:

@Path("/demo")
public interface IDemoService {

    @GET
    @Path("/test")
    String test();

}

Конфигурация Simple Spring Security:

http.cors().and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .sessionAuthenticationStrategy(sessionAuthenticationStrategy()).and().authorizeRequests().antMatchers("/**")
    .authenticated();

РЕДАКТИРОВАТЬ: новый обходной путь для получения начального доступа и обновления токена sh с сервера:

AccessTokenResponse tokens = AuthUtil.getAuthTokens("http://localhost:8180/auth", "share-server", "test", "test", "share-server-service-login");
String accessToken = tokens.getToken();
String refreshToken = tokens.getRefreshToken();

Клиент, выполняющий сервисные вызовы до истечения срока действия токена:

URI apiUri = new URI("http://localhost:8080/services/");
RestClientBuilder client = new CxfTypeSafeClientBuilder().baseUri(apiUri).register(new TokenFilter(accessToken, refreshToken));

IDemoService service = client.build(IDemoService.class);
for (int i = 0; i < 200; i++) {
    System.out.println("client: " + new Date() + " " + service.test());
    Thread.sleep(10000);
}

TokenFilter, который работает до истечения срока действия токена доступа:

public static class TokenFilter implements ClientRequestFilter, ClientResponseFilter {

    private String accessToken;
    private String refreshToken;

    public TokenFilter(String accessToken, String refreshToken) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }

    @Override
    public void filter(ClientRequestContext requestContext, ClientResponseContext responseContext) throws IOException {
        if (responseContext.getStatus() == 401 && "invalid_token".equals(responseContext.getStatusInfo().getReasonPhrase())) {
            // maybe handle send the refresh token... probalby should be handled earlier using the 'expires' value
        }
    }

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        if (accessToken != null && !accessToken.isEmpty()) {
            requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer" + " " + accessToken);
        }
    }
}

Ответы [ 3 ]

0 голосов
/ 03 марта 2020

Нашли лучшее решение с только зависимостями от keycloak-authz-client :

String serverUrl = "http://localhost:8180/auth";
String realm = "share-server";
String clientId = "share-server-service-login";
String clientSecret = "e70752a6-8910-4043-8926-03661f43398c";
String username = "test";
String password = "test";

Map<String, Object> credentials = new HashMap<>();
credentials.put("secret", clientSecret);
Configuration configuration = new Configuration(serverUrl, realm, clientId, credentials, null);
AuthzClient authzClient = AuthzClient.create(configuration);

AuthorizationResource authorizationResource = authzClient.authorization(username, password);

URI apiUri = new URI("http://localhost:8080/services/");
RestClientBuilder client = new CxfTypeSafeClientBuilder().baseUri(apiUri).register(new TokenFilter(authorizationResource));
IDemoService service = client.build(IDemoService.class);
for (int i = 0; i < 200; i++) {
    System.out.println("client: " + new Date() + " " + service.test());
    Thread.sleep(10000);
}

authorizationResource.authorize() будет использовать org.keycloak.authorization.client.util.TokenCallable.call() в фоновом режиме, который проверяет время истечения токена и при необходимости автоматически обновляет токен.

, поэтому String accessToken = authorize.getToken(); всегда будет текущим действительным токеном.

@Priority(Priorities.AUTHENTICATION)
public static class TokenFilter implements ClientRequestFilter {

    private AuthorizationResource authorizationResource;

    public TokenFilter(AuthorizationResource authorizationResource) {
        this.authorizationResource = authorizationResource;
    }

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        AuthorizationResponse authorize = authorizationResource.authorize();
        String accessToken = authorize.getToken();
        System.out.println(accessToken);
        if (accessToken != null && !accessToken.isEmpty()) {
            requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer" + " " + accessToken);
        }
    }
}
0 голосов
/ 06 марта 2020

Более универсальное c решение с использованием Apache CXF OAuth2 (cxf-rt-rs-security-oauth2), без ClientRequestFilter. BearerAuthSupplier автоматически обрабатывает refreshTokens и получает новый accessTokens.

String serverUrl = "http://localhost:8180/auth";
String realm = "share-server";
String clientId = "share-server-service-login";
String clientSecret = "e70752a6-8910-4043-8926-03661f43398c";
String username = "test";
String password = "test";

String tokenUri = serverUrl + "/realms/" + realm + "/protocol/openid-connect/token";

Consumer consumer = new Consumer(clientId);

ResourceOwnerGrant grant = new ResourceOwnerGrant(username, password);
ClientAccessToken initial = OAuthClientUtils.getAccessToken(tokenUri, consumer, grant, true);

BearerAuthSupplier supplier = new BearerAuthSupplier();
supplier.setAccessToken(initial.getTokenKey());
supplier.setRefreshToken(initial.getRefreshToken());
supplier.setConsumer(consumer);
supplier.setAccessTokenServiceUri(tokenUri);

HTTPConduitConfigurer httpConduitConfigurer = new HTTPConduitConfigurer() {
    @Override
    public void configure(String name, String address, HTTPConduit c) {
        c.setAuthSupplier(supplier);
    }
};

Bus bus = BusFactory.getThreadDefaultBus();
bus.setExtension(httpConduitConfigurer, HTTPConduitConfigurer.class);

URI apiUri = new URI("http://localhost:8080/services/");
RestClientBuilder client = new CxfTypeSafeClientBuilder().baseUri(apiUri);

IDemoService service = client.build(IDemoService.class);
for (int i = 0; i < 200; i++) {
    System.out.println("client: " + new Date() + " " + service.test());
    Thread.sleep(5 * 60 * 1000);
}

Вместо входа в систему с именем пользователя и паролем (ResourceOwnerGrant) также возможно использование учетных данных клиента с ClientCredentialsGrant.

ClientCredentialsGrant grant = new ClientCredentialsGrant();
grant.setClientId(clientId);
grant.setClientSecret(clientSecret);
0 голосов
/ 03 марта 2020

Я нашел решение, которое автоматически обновляет токен доступа, но теперь у меня есть зависимость от keycloak-client-registration-cli (которая фактически предназначена для предоставления консоли). Там могут быть лучшие решения с менее тяжелыми зависимостями. В настоящее время не обрабатывается, если не удается войти в систему или реализована другая обработка исключений.

String serverUrl = "http://localhost:8180/auth";
String realm = "share-server";
String clientId = "share-server-service-login";
String username = "test";
String password = "test";

// initial token after login
AccessTokenResponse token = AuthUtil.getAuthTokens(serverUrl, realm, username, password, clientId);

String accessToken = token.getToken();
String refreshToken = token.getRefreshToken();

ConfigData configData = new ConfigData();
configData.setRealm(realm);
configData.setServerUrl(serverUrl);

RealmConfigData realmConfigData = configData.sessionRealmConfigData();
realmConfigData.setClientId(clientId);
realmConfigData.setExpiresAt(System.currentTimeMillis() + token.getExpiresIn() * 1000);
realmConfigData.setRefreshExpiresAt(System.currentTimeMillis() + token.getRefreshExpiresIn() * 1000);
realmConfigData.setToken(accessToken);
realmConfigData.setRefreshToken(refreshToken);

ConfigUtil.setupInMemoryHandler(configData);

URI apiUri = new URI("http://localhost:8080/services/");
RestClientBuilder client = new CxfTypeSafeClientBuilder().baseUri(apiUri).register(new TokenFilter(configData));
IDemoService service = client.build(IDemoService.class);
for (int i = 0; i < 200; i++) {
    System.out.println("client: " + new Date() + " " + service.test());
    Thread.sleep(10000);
}

Фильтр, который автоматически обновляет токен доступа, если срок его действия истек, используя AuthUtil.ensureToken(configData):

@Priority(Priorities.AUTHENTICATION)
public static class TokenFilter implements ClientRequestFilter {

    private ConfigData configData;

    public TokenFilter(ConfigData configData) {
        this.configData = configData;
    }

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        String accessToken = AuthUtil.ensureToken(configData);
        if (accessToken != null && !accessToken.isEmpty()) {
            requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer" + " " + accessToken);
        }
    }
}
...