我具有以下设置:Keycloak9.0.0在端口8180上运行在端口8080上运行的Spring Boot服务器应用程序演示客户端应用程序,使用CxfTypeSafeClientBuilder
访问服务器服务
[Keycloak-Spring Boot交互工作正常,如果我将其作为Authorization
标头传递,我可以从Keycloak接收令牌,并且演示服务正在验证令牌。
我应该如何配置CxfTypeSafeClientBuilder
/ RestClientBuilder
以处理从Keycloak实例获得的JWT令牌?如果必须,我是否必须构建自己的ClientResponseFilter
?如何处理过期的令牌?我没有找到任何现有的实现/标准吗?
JAX-RS Web服务界面:
@Path("/demo")
public interface IDemoService {
@GET
@Path("/test")
String test();
}
简单的Spring Security配置:
http.cors().and().csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.sessionAuthenticationStrategy(sessionAuthenticationStrategy()).and().authorizeRequests().antMatchers("/**")
.authenticated();
编辑:从服务器获取初始访问权限和刷新令牌的新解决方法:
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);
}
}
}
我找到了一个自动刷新访问令牌的解决方案,但是现在我对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);
}
}
}
找到了仅依赖于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);
}
}
}
使用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);