【问题标题】:secure microprofile REST client with OAuth2 / OIDC使用 OAuth2 / OIDC 的安全微配置文件 REST 客户端
【发布时间】:2020-06-19 08:41:17
【问题描述】:

我正在使用Apache CXF 访问 JAX-RS 服务。通过Keycloak (OpenID Connect/OAuth2) 对用户进行身份验证。

BearerAuthSupplier 会自动处理 refreshTokens 并接收新的 accessTokens

但是,当会话过期或注销(例如管理员注销)并收到 401 错误时,我该如何处理?
我猜在进行实际的服务调用之前,必须有一种方法可以提供用于进行新登录的凭据。

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);
}

javax.ws.rs.WebApplicationException: HTTP 401 Unauthorized

WARNUNG: Interceptor for {http://service.server.share.scodi.ch/}IDemoService has thrown exception, unwinding now
org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException: OAuthServiceException invoking http://localhost:8080/services/demo/test: OAuthError[error='invalid_grant', errorDescription='Session not active', errorUri='null']
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.mapException(HTTPConduit.java:1400)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1389)
    at org.apache.cxf.io.AbstractWrappedOutputStream.close(AbstractWrappedOutputStream.java:77)
    at org.apache.cxf.transport.AbstractConduit.close(AbstractConduit.java:56)
    at org.apache.cxf.transport.http.HTTPConduit.close(HTTPConduit.java:671)
    at org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor.handleMessage(MessageSenderInterceptor.java:63)
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308)
    at org.apache.cxf.jaxrs.client.AbstractClient.doRunInterceptorChain(AbstractClient.java:701)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.doRunInterceptorChain(MicroProfileClientProxyImpl.java:165)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.doChainedInvocation(ClientProxyImpl.java:899)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.invoke(ClientProxyImpl.java:345)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.invokeActual(MicroProfileClientProxyImpl.java:439)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.access$000(MicroProfileClientProxyImpl.java:70)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl$Invoker.call(MicroProfileClientProxyImpl.java:458)
    at org.apache.cxf.microprofile.client.cdi.CDIInterceptorWrapper$BasicCDIInterceptorWrapper.invoke(CDIInterceptorWrapper.java:43)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.invoke(MicroProfileClientProxyImpl.java:435)
    at com.sun.proxy.$Proxy19.test(Unknown Source)
    at ch.scodi.share.server.ClientTest.main(ClientTest.java:78)
Caused by: org.apache.cxf.rs.security.oauth2.provider.OAuthServiceException: OAuthError[error='invalid_grant', errorDescription='Session not active', errorUri='null']
    at org.apache.cxf.rs.security.oauth2.client.OAuthClientUtils.getAccessToken(OAuthClientUtils.java:321)
    at org.apache.cxf.rs.security.oauth2.client.OAuthClientUtils.refreshAccessToken(OAuthClientUtils.java:244)
    at org.apache.cxf.rs.security.oauth2.client.OAuthClientUtils.refreshAccessToken(OAuthClientUtils.java:235)
    at org.apache.cxf.rs.security.oauth2.client.BearerAuthSupplier.refreshAccessToken(BearerAuthSupplier.java:103)
    at org.apache.cxf.rs.security.oauth2.client.BearerAuthSupplier.getAuthorization(BearerAuthSupplier.java:63)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.authorizationRetransmit(HTTPConduit.java:1529)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.processRetransmit(HTTPConduit.java:1461)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleRetransmits(HTTPConduit.java:1435)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1565)
    at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1371)
    ... 16 more
javax.ws.rs.WebApplicationException: HTTP 401 Unauthorized
    at org.apache.cxf.microprofile.client.DefaultResponseExceptionMapper.toThrowable(DefaultResponseExceptionMapper.java:33)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.checkResponse(MicroProfileClientProxyImpl.java:183)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.handleResponse(ClientProxyImpl.java:1002)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.doChainedInvocation(ClientProxyImpl.java:907)
    at org.apache.cxf.jaxrs.client.ClientProxyImpl.invoke(ClientProxyImpl.java:345)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.invokeActual(MicroProfileClientProxyImpl.java:439)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.access$000(MicroProfileClientProxyImpl.java:70)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl$Invoker.call(MicroProfileClientProxyImpl.java:458)
    at org.apache.cxf.microprofile.client.cdi.CDIInterceptorWrapper$BasicCDIInterceptorWrapper.invoke(CDIInterceptorWrapper.java:43)
    at org.apache.cxf.microprofile.client.proxy.MicroProfileClientProxyImpl.invoke(MicroProfileClientProxyImpl.java:435)
    at com.sun.proxy.$Proxy19.test(Unknown Source)
    at ch.scodi.share.server.ClientTest.main(ClientTest.java:78)

【问题讨论】:

    标签: java oauth-2.0 cxf openid-connect microprofile


    【解决方案1】:

    应始终编写 OAuth 客户端以在调用 API 时检查 401 响应,并刷新令牌并在可能的情况下使用新令牌重试 API 请求,否则触发重新身份验证。

    我倾向于通过以下 2 个类进行管理:

    以上示例适用于 Web UI,但可以为任何 OAuth 客户端流程或技术堆栈编写。

    【讨论】:

    • 这正是我想做的,但我没有找到 Apache CXF 客户端的正确方法。
    • 如果处理直接HTTP调用,流可以直接在语言之间翻译,但是在这种情况下,OP是专门谈论JAX-RS和Apache CXF。 JAX-RS 是一个标准的 Java API,它“隐藏”了幕后的所有流程,并使用过滤器和拦截器来处理所有事情,因此您可以隔离所有与 REST 相关的代码,并且 CXF 具有 JAX-RS 的实现/扩展。如果您不知道如何在新 API 中执行此操作,则无法在具有不同范例和方法的不同 API 之间转换流程,这就是问题所在......
    【解决方案2】:

    这是我想出的解决方案,请登录以下用例:

    • 初始登录
    • 刷新令牌过期
    • 会话已过期(过期令牌)
    • 会话已注销(手动、服务重启)
    • 授权期间抛出其他 OAuthServiceException

    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";
    
    ResourceOwnerGrant grant = new ResourceOwnerGrant(username, password);
    AuthSupplier supplier = new AuthSupplier(tokenUri, clientId, clientSecret, grant);
    HTTPConduitConfigurer httpConduitConfigurer = new HTTPConduitConfigurer() {
        @Override
        public void configure(String name, String address, HTTPConduit c) {
            // don't do authentication on token URI
            if (!tokenUri.equals(address)) {
                c.setAuthSupplier(supplier);
            }
        }
    };
    
    Bus bus = BusFactory.getThreadDefaultBus();
    bus.setExtension(httpConduitConfigurer, HTTPConduitConfigurer.class);
    
    ... REST-Client Code ...
    

    public static class AuthSupplier extends BearerAuthSupplier {
    
        private WebClient webClient = null;
        private AccessTokenGrant grant;
    
        public AuthSupplier(String tokenUri, String clientId, String clientSecret, AccessTokenGrant grant) {
            grant = grant;
            webClient = WebClient.create(tokenUri);
            setAccessTokenServiceUri(tokenUri);
            setConsumer(new Consumer(clientId, clientSecret));
        }
    
        @Override
        public String getAuthorization(AuthorizationPolicy authPolicy, URI currentURI, Message message, String fullHeader) {
            String authorization = null;
            try {
                authorization = super.getAuthorization(authPolicy, currentURI, message, fullHeader);
            } catch (OAuthServiceException e) {
                System.out.println(e.getError().getState() + " " + e.getMessage());
            }
    
            if (authorization == null) {
                // refresh token expired or session expired (Stale token) or session was logged-out (manually, service-restart), do new login
                login();
    
                // try to get authorization again after login
                authorization = super.getAuthorization(authPolicy, currentURI, message, null);
            }
            return authorization;
        }
    
        private void login() {
            ClientAccessToken accessToken = OAuthClientUtils.getAccessToken(webClient, getConsumer(), grant, false);
            setAccessToken(accessToken.getTokenKey());
            setRefreshToken(accessToken.getRefreshToken());
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2013-04-19
      • 1970-01-01
      • 2016-10-31
      • 1970-01-01
      • 2017-11-16
      • 1970-01-01
      • 1970-01-01
      • 2022-01-20
      • 2012-02-21
      相关资源
      最近更新 更多