首页 > 解决方案 > 有状态的 FeignClient(客户端和服务器之间的会话)

问题描述

我一直在为 Spring Cloud 使用 @FeignClient。我们有根“网关” WAR,它调用“后端”服务 WAR,我们在其中缓存用户角色特定的元数据(这些数据应该足够大,可以在每次调用时请求它,并且足够小以将其存储在 RAM 内存中)。

我需要客户端能够处理 Set-Cookie,然后在客户端会话处于活动状态时将 JSESSIONID 放到服务器端。所以我相信我需要将所有服务器端的 sessionIds 存储在特定网关用户的某个会话范围的 bean 中。喜欢

"client1" => "8D06349922CD77D1CE68F78F4FAE04C5" 
"client2" => "another session id"

在网上冲浪后,我找到了 ApacheHttpClient 和 Spring RestTemplate 的解决方案。Feign 唯一的特点是可以获取 @RequestHeader("Cookie") 作为远程函数的参数。

好吧,我写了一些粗鲁的代码来解决这个问题:

HttpMessageConverter httpMessageConverter = new HttpMessageConverter() {
            @Override
            public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
                HttpHeaders headers = inputMessage.getHeaders();
                logger.info("headers: {}", headers);
                List<String> setCookie = headers.get("Set-Cookie");
                logger.info("setCookie: {}", setCookie);
                if(setCookie != null){
                    String jsId = setCookie.get(0).split(";")[0].split("=")[1];
                    logger.info("jsId Object read: {}", jsId);
                    cookies.setSessionId("key", jsId);
                }
                return delegate.read(clazz,inputMessage);
            }
};

......

RequestInterceptor jsessionFeignRequestInterceptor = new RequestInterceptor() {
    @Override
    public void apply(RequestTemplate template) {
        String sessionId = cookies.getSessionId("key");
        logger.info("THE JSESSIONID: {}", sessionId);
        if(sessionId != null) {
            template.header("Cookie", "JSESSIONID=" + sessionId);
        }
    }
};

……

 Feign.builder()
    .encoder(new SpringEncoder(new HttpMessageConverters(httpMessageConverter)))
    .requestInterceptor(jsessionFeignRequestInterceptor)
    .requestInterceptor(oauth2FeignRequestInterceptor)........

但是在我看来它看起来很奇怪。

是否有其他“合适的选择”来实现类似的功能?

注意:我们使用的是 OAuth2 Spring Auth Server。

谢谢。

标签: spring-cloudsetcookiejsessionidstatefulspring-cloud-feign

解决方案


我通过使用带有自定义拦截器的 OkHttpClient 做了类似的事情。

Builder builder = Feign
    .builder()
    .client(new OkHttpClient(new okhttp3.OkHttpClient.Builder().addInterceptor(new SessionIdRequestInterceptor()).build()));

OkHttpClient 还支持 Authenticator 接口,但仅在 401 http 状态下调用。由于我的身份验证服务器重定向到具有 200 或 302 的登录页面,因此我必须对身份验证进行自定义验证,因此需要拦截器。这是我的实现:

class SessionIdRequestInterceptor implements okhttp3.Interceptor {

    @Override
    public okhttp3.Response intercept(Chain chain) throws IOException {
        okhttp3.Request authenticatedRequest = authenticateRequest(chain.request());
        okhttp3.Response response = chain.proceed(authenticatedRequest);
        if (isAuthenticated(response)) {
            saveSessionId(response);
        } else if (jSessionId != null) {
            jSessionId = null;
            response.body().close();
            authenticatedRequest = authenticateRequest(authenticatedRequest);
            response = chain.proceed(authenticatedRequest);
        }
        if (!isAuthenticated(response)) {
            throw new PermissionDeniedException("Failed Authentication");
        }
        return response;
    }

    private void saveSessionId(okhttp3.Response response) {
        String setCookie = response.header("Set-Cookie");
        if (setCookie != null) {
            jSessionId = setCookie.split(";")[0].split("=")[1];
        }
    }

    private boolean isAuthenticated(okhttp3.Response response) {
        return !response.request().url().encodedPath().contains("/cas-server/login");
    }

    private okhttp3.Request authenticateRequest(okhttp3.Request request) {
        okhttp3.Request.Builder builder = request.newBuilder();
        if (jSessionId != null) {
            builder.addHeader("Cookie", "JSESSIONID=" + jSessionId);
        } else {
            builder
                .addHeader("Authorization", "Basic " + Base64
                    .getEncoder()
                    .encodeToString((configuration.getUser() + ":" + configuration.getClearPassword()).getBytes()));
        }
        return builder.build();
    }
}

推荐阅读