Автоматическая аутентификация

Я хочу использовать Apache HttpClient 4+ для отправки аутентифицированных запросов на HTTP-сервер (фактически, мне это нужно для разных реализаций сервера) И для аутентификации (или повторной аутентификации) автоматически ТОЛЬКО, когда это требуется, когда токен аутентификации отсутствует или он мертв.

Для аутентификации мне нужно отправить запрос POST с JSON, содержащим учетные данные пользователя.

В случае, если токен аутентификации не указан в файле cookie, один сервер возвращает код состояния 401, еще один 500 с текстом AUTH_REQUIRED в теле ответа.

Я много играл с разными версиями HttpClient, установив CredentialsProvider с правильным Credentials, пытаясь реализовать собственный AuthScheme и зарегистрировав его и отменив регистрацию остальных стандартных.

Я также попытался установить собственный AuthenticationHandler. Когда вызывается isAuthenticationRequested, я анализирую HttpResponse, который передается как аргумент метода, и решил, что возвращать, анализируя код состояния и тело ответа. Я ожидал, что это (isAuthenticationRequested() == true) - это то, что заставляет клиент аутентифицироваться, вызывая AuthScheme.authenticate (моя реализация AuthScheme, которая возвращается AuthenticationHandler.selectScheme), но вместо вызова AuthScheme.authenticate я могу видеть AuthenticationHandler.getChallenges. Я действительно не знаю, что я должен вернуть этим методом, поэтому я просто возвращаю new HashMap<>().

Вот результат отладки, который у меня есть в результате

DEBUG org.apache.http.impl.client.DefaultHttpClient - Authentication required
DEBUG org.apache.http.impl.client.DefaultHttpClient - example.com requested authentication
DEBUG com.test.httpclient.MyAuthenticationHandler - MyAuthenticationHandler.getChallenges()
DEBUG org.apache.http.impl.client.DefaultHttpClient - Response contains no authentication challenges

Что мне делать дальше? Я двигаюсь в правильном направлении?

UPDATE

Я почти достиг того, что мне нужно. К сожалению, я не могу предоставить полностью рабочие источники проекта, потому что я не могу предоставить публичный доступ к моему серверу. Вот мой упрощенный пример кода:

MyAuthScheme.java

public class MyAuthScheme implements ContextAwareAuthScheme {

    public static final String NAME = "myscheme";

    @Override
    public Header authenticate(Credentials credentials, 
                           HttpRequest request, 
                           HttpContext context) throws AuthenticationException {

        HttpClientContext clientContext = ((HttpClientContext) context);        
        String name = clientContext.getTargetAuthState().getState().name();

        // Hack #1: 
        // I've come to this check. I don't like it, but it allows to authenticate
        // first request and don't repeat authentication procedure for further 
        // requests
        if(name.equals("CHALLENGED") && clientContext.getResponse() == null) {

            //
            // auth procedure must be here but is omitted in current example
            //

            // Hack #2: first request won't be present with auth token cookie set via cookie store 
            request.setHeader(new BasicHeader("Cookie", "MYAUTHTOKEN=bru99rshi7r5ucstkj1wei4fshsd"));

            // this works for second and subsequent requests
            BasicClientCookie authTokenCookie = new BasicClientCookie("MYAUTHTOKEN", "bru99rshi7r5ucstkj1wei4fshsd");
            authTokenCookie.setDomain("example.com");
            authTokenCookie.setPath("/");

            BasicCookieStore cookieStore = (BasicCookieStore) clientContext.getCookieStore();
            cookieStore.addCookie(authTokenCookie);
        }

        // I can't return cookie header here, otherwise it will clear 
        // other cookies, right?
        return null;
    }

    @Override
    public void processChallenge(Header header) throws MalformedChallengeException {

    }

    @Override
    public String getSchemeName() {
        return NAME;
    }

    @Override
    public String getParameter(String name) {
        return null;
    }

    @Override
    public String getRealm() {
        return null;
    }

    @Override
    public boolean isConnectionBased() {
        return false;
    }

    @Override
    public boolean isComplete() {
        return true;
    }

    @Override
    public Header authenticate(Credentials credentials, 
                           HttpRequest request) throws AuthenticationException {        
        return null;
    }

}

MyAuthStrategy.java

public class MyAuthStrategy implements AuthenticationStrategy {

    @Override
    public boolean isAuthenticationRequested(HttpHost authhost, 
                                         HttpResponse response, 
                                         HttpContext context) {

        return response.getStatusLine().getStatusCode() == 401;
    }

    @Override
    public Map<String, Header> getChallenges(HttpHost authhost, 
                                         HttpResponse response, 
                                         HttpContext context) throws MalformedChallengeException {

        Map<String, Header> challenges = new HashMap<>();
        challenges.put(MyAuthScheme.NAME, new BasicHeader(
                "WWW-Authentication", 
                "Myscheme realm=\"My SOAP authentication\""));

        return challenges;
    }

    @Override
    public Queue<AuthOption> select(Map<String, Header> challenges, 
                                HttpHost authhost, 
                                HttpResponse response, 
                                HttpContext context) throws MalformedChallengeException {

        Credentials credentials = ((HttpClientContext) context)
                .getCredentialsProvider()
                .getCredentials(new AuthScope(authhost));

        Queue<AuthOption> authOptions = new LinkedList<>();
        authOptions.add(new AuthOption(new MyAuthScheme(), credentials));

        return authOptions;
    }

    @Override
    public void authSucceeded(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}

    @Override
    public void authFailed(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}

}

MyApp.java

public class MyApp {

    public static void main(String[] args) throws IOException {

        CredentialsProvider credsProvider = new BasicCredentialsProvider();
        Credentials credentials = new UsernamePasswordCredentials("[email protected]", "secret");
        credsProvider.setCredentials(AuthScope.ANY, credentials);

        HttpClientContext context = HttpClientContext.create();
        context.setCookieStore(new BasicCookieStore());
        context.setCredentialsProvider(credsProvider);

        CloseableHttpClient client = HttpClientBuilder.create()
                // my server requires this header otherwise it returns response with code 500
                .setDefaultHeaders(Collections.singleton(new BasicHeader("x-requested-with", "XMLHttpRequest"))) 
                .setTargetAuthenticationStrategy(new MyAuthStrategy())
                .build();

        String url = "https://example.com/some/resource";
        String url2 = "https://example.com/another/resource";

        // ======= REQUEST 1 =======

        HttpGet request = new HttpGet(url);
        HttpResponse response = client.execute(request, context);
        String responseText = EntityUtils.toString(response.getEntity());
        request.reset();

        // ======= REQUEST 2 =======

        HttpGet request2 = new HttpGet(url);
        HttpResponse response2 = client.execute(request2, context);
        String responseText2 = EntityUtils.toString(response2.getEntity());
        request2.reset();

        // ======= REQUEST 3 =======

        HttpGet request3 = new HttpGet(url2);
        HttpResponse response3 = client.execute(request3, context);
        String responseText3 = EntityUtils.toString(response3.getEntity());
        request3.reset();

        client.close();

    }

}

Версия

httpcore: 4.4.6
httpclient: 4.5.3

Возможно, это не лучший код, но по крайней мере он работает.

Пожалуйста, просмотрите мои комментарии в методе MyAuthScheme.authenticate().

Ответ 1

Это работает как ожидалось для меня с Apache HttpClient 4.2

Примечание. Хотя он компилируется и выполняется с помощью httpclient 4.5, его выполнение попадает в цикл навсегда.

MyAuthScheme.java

public class MyAuthScheme implements ContextAwareAuthScheme {

    public static final String NAME = "myscheme";
    private static final String REQUEST_BODY = "{\"login\":\"%s\",\"password\":\"%s\"}";

    private final URI loginUri;

    public MyAuthScheme(URI uri) {
        loginUri = uri;
    }

    @Override
    public Header authenticate(Credentials credentials, 
                           HttpRequest request, 
                           HttpContext context) throws AuthenticationException {

        BasicCookieStore cookieStore = (BasicCookieStore) context.getAttribute(ClientContext.COOKIE_STORE);

        DefaultHttpClient client = new DefaultHttpClient();

        // authentication cookie is set automatically when 
        // login response arrived
        client.setCookieStore(cookieStore);

        HttpPost loginRequest = new HttpPost(loginUri);
        String requestBody = String.format(
                REQUEST_BODY, 
                credentials.getUserPrincipal().getName(), 
                credentials.getPassword());
        loginRequest.setHeader("Content-Type", "application/json");

        try {
            loginRequest.setEntity(new StringEntity(requestBody));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        try {
            HttpResponse response = client.execute(loginRequest);
            int code = response.getStatusLine().getStatusCode();
            EntityUtils.consume(response.getEntity());
            if(code != 200) {
                throw new IllegalStateException("Authentication problem");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            loginRequest.reset();            
        }

        return null;
    }

    @Override
    public void processChallenge(Header header) throws MalformedChallengeException {}

    @Override
    public String getSchemeName() {
        return NAME;
    }

    @Override
    public String getParameter(String name) {
        return null;
    }

    @Override
    public String getRealm() {
        return null;
    }

    @Override
    public boolean isConnectionBased() {
        return false;
    }

    @Override
    public boolean isComplete() {
        return false;
    }

    @Override
    public Header authenticate(Credentials credentials, 
                           HttpRequest request) throws AuthenticationException {
        // not implemented
        return null;
    }

}

MyAuthSchemeFactory.java

public class MyAuthSchemeFactory implements AuthSchemeFactory {

    private final URI loginUri;

    public MyAuthSchemeFactory(URI uri) {
        this.loginUri = uri;
    }

    @Override
    public AuthScheme newInstance(HttpParams params) {
        return new MyAuthScheme(loginUri);
    }

}

MyAuthStrategy.java

public class MyAuthStrategy implements AuthenticationStrategy {

    @Override
    public boolean isAuthenticationRequested(HttpHost authhost, 
                                             HttpResponse response, 
                                             HttpContext context) {

        return response.getStatusLine().getStatusCode() == 401;
    }

    @Override
    public Map<String, Header> getChallenges(HttpHost authhost, 
                                             HttpResponse response, 
                                             HttpContext context) throws MalformedChallengeException {

        Map<String, Header> challenges = new HashMap<>();
        challenges.put("myscheme", new BasicHeader("WWW-Authenticate", "myscheme"));

        return challenges;
    }

    @Override
    public Queue<AuthOption> select(Map<String, Header> challenges, 
                                    HttpHost authhost, 
                                    HttpResponse response, 
                                    HttpContext context) throws MalformedChallengeException {

        AuthSchemeRegistry registry = (AuthSchemeRegistry) context.getAttribute(ClientContext.AUTHSCHEME_REGISTRY);
        AuthScheme authScheme = registry.getAuthScheme(MyAuthScheme.NAME, new BasicHttpParams());
        CredentialsProvider credsProvider = (CredentialsProvider) context.getAttribute(ClientContext.CREDS_PROVIDER);
        Credentials credentials = credsProvider.getCredentials(new AuthScope(authhost));

        Queue<AuthOption> options = new LinkedList<>();
        options.add(new AuthOption(authScheme, credentials));

        return options;
    }

    @Override
    public void authSucceeded(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}

    @Override
    public void authFailed(HttpHost authhost, AuthScheme authScheme, HttpContext context) {}

}

App.java

public class App {

    public static void main(String[] args) throws IOException, URISyntaxException {

        URI loginUri = new URI("https://example.com/api/v3/users/login");

        AuthSchemeRegistry schemeRegistry = new AuthSchemeRegistry();
        schemeRegistry.register(MyAuthScheme.NAME, new MyAuthSchemeFactory(loginUri));

        BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
                new AuthScope("example.com", 8065), 
                new UsernamePasswordCredentials("[email protected]", "secret"));

        DefaultHttpClient client = new DefaultHttpClient();
        client.setCredentialsProvider(credentialsProvider);
        client.setTargetAuthenticationStrategy(new MyAuthStrategy());
        client.setAuthSchemes(schemeRegistry);
        client.setCookieStore(new BasicCookieStore());


        String getResourcesUrl = "https://example.com:8065/api/v3/myresources/";

        HttpGet getResourcesRequest = new HttpGet(getResourcesUrl);
        getResourcesRequest.setHeader("x-requested-with", "XMLHttpRequest");

        try {
            HttpResponse response = client.execute(getResourcesRequest);
            // consume response
        } finally {
            getResourcesRequest.reset();
        }   

        // further requests won't call MyAuthScheme.authenticate()

        HttpGet getResourcesRequest2 = new HttpGet(getResourcesUrl);
        getResourcesRequest2.setHeader("x-requested-with", "XMLHttpRequest");

        try {
            HttpResponse response2 = client.execute(getResourcesRequest);
            // consume response
        } finally {
            getResourcesRequest2.reset();
        }   

        HttpGet getResourcesRequest3 = new HttpGet(getResourcesUrl);
        getResourcesRequest3.setHeader("x-requested-with", "XMLHttpRequest");

        try {
            HttpResponse response3 = client.execute(getResourcesRequest);
            // consume response
        } finally {
            getResourcesRequest3.reset();
        }   

    }

}