Дайджест-аутентификация в Android с использованием HttpURLConnection
Как говорится в вопросе allready, я пытаюсь сделать дайджест-аутентификацию в android.
До сих пор я использовал DefaultHttpClient и это метод аутентификации (используя UsernamePasswordCredentials и так далее), но он устарел с Android 5 и будет удален в Android 6.
Поэтому я собираюсь переключиться с DefaultHttpClient на HttpUrlConnection.
Теперь я пытаюсь добиться дайджест-аутентификации, которая должна работать довольно просто, как описано здесь :
Authenticator.setDefault(new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
Но getPasswordAuthentication никогда не вызывается для некоторых причина.
Во время поиска этой проблемы я нашел разные сообщения, в которых говорилось, что дайджест-аутентификация не поддерживается HttpUrlConnection в android, но эти сообщения относятся к 2010-2012 годам, поэтому я не уверен, что это все еще верно. Кроме того, мы используем HttpUrlConnection с дайджест-аутентификацией в нашем настольном java-приложении, где она действительно работает.
Я также нашел несколько сообщений, говорящих о OkHttp. OkHttp, по-видимому, используется Android under the hood (если быть более точным, то HttpUrlConnectionImpl). Но это HttpUrlConnectionImpl немного странно, он даже не показан в иерархии типов Eclipse, и я не могу его отладить. Также это должно быть com.squareup.okhttp.internal.huc.HttpUrlConnectionImpl, в то время как в android это com.android.okhttp.internal.http.HttpUrlConnectionImpl.
Так что я просто не в состоянии сделать дайджест-аутентификацию с этим HttpUrlConnection в android.
Может ли кто-нибудь сказать мне, как это сделать без внешних библиотек?
Редактировать:
Сервер запрашивает дайджест-аутентификацию:
WWW-Authenticate: Digest realm="Realm Name",domain="/domain",nonce="nonce",algorithm=MD5,qop="auth"
Таким образом, базовая аутентификация не должна работать, так как сервер запрашивает дайджест.
5 ответов:
Ответ таков, что
HttpUrlConnectionне поддерживает digest.Поэтому вы должны реализовать RFC2617 самостоятельно.
В качестве базовой реализации можно использовать следующий код: HTTP Digest Auth для Android .Шаги включают (см. RFC2617 Для справки):
- Если вы получаете ответ 401, повторите все заголовки
WWW-Authenticateи проанализируйте их:
- проверьте, является ли алгоритм MD5 или неопределенным, (необязательно выберите
authqop option), в противном случае проигнорируйте вызов и перейдите к следующему заголовку.- получите учетные данные, используя
Authenticator.requestPasswordAuthentication.- вычислите H (A1), используя имя пользователя, область и пароль.
- храните канонический корневой URL, область, HA1, имя пользователя, nonce (+необязательно алгоритм, непрозрачный и выбранный клиентом параметр qop, если он присутствует).
- повторите запрос.
- при каждом запросе повторите все области, для которых у вас есть информация о сеансе, сохраненная каноническим корнем URL-АДРЕС:
- вычислите H (A2), используя метод запроса и путь.
- вычислите H(A3), используя HA1, nonce (+ необязательно nc, cnonce, qop) и HA2.
- создайте и добавьте заголовок
Authorizationв свойHttpUrlConnection.- реализуйте своего рода сеансовую обрезку.
Используя
Authenticator, Вы можете убедиться, что как толькоHttpUrlConnectionподдерживает дайджест изначально, ваш код больше не используется (потому что вы не получите 401 в первую очередь).Это всего лишь краткое резюме о том, как его реализовать, чтобы у вас появилась идея.
Если вы хотите пойти дальше, вы, вероятно, хотели бы также реализовать SHA256: RFC7616
Правильно, что
HttpUrlConnectionне поддерживает дайджест-аутентификацию. Если ваш клиент должен аутентифицироваться с помощью Digest, у вас есть несколько вариантов:
- напишите свою собственную реализацию http Digest. Это может быть хорошим вариантом, если вы знаете, какие серверы вам нужно аутентифицировать, и можете игнорировать части спецификации The digest, которые вам не нужны. Вот пример, в котором реализовано подмножество дайджеста: https://gist.github.com/slightfoot/5624590 .
- Используйте внешний libbare-bones-digest , который является дайджестом lib для Android. Вы можете использовать его для анализа проблем дайджеста и генерирования ответов на них. Он поддерживает общие случаи использования дайджеста и некоторые из редко используемых и может использоваться поверх
HttpURLConnection.- Используйте Ohttp вместе с ohttp-digest, который является плагином, добавляющим поддержку HTTP Digest в Ohttp. Поддерживая дайджест с OkHttp это легко, просто добавьте
okhttp-digestв качестве аутентификатора, и у вас будет прозрачная поддержка HTTP digest. Если вы уже используете Ohttp или не возражаете перейти на него, это может быть привлекательным вариантом.- Используйте Apache
HttpClient, который поддерживает Digest. Вопрос явно указывает, чтоHttpClientне является вариантом, поэтому я включаю его в основном для завершения. Google не рекомендует использоватьHttpClientи устарел.
Вы пытались установить заголовок вручную, как:
String basic = "Basic " + new String(Base64.encode("username:password".getBytes(),Base64.NO_WRAP )); connection.setRequestProperty ("Authorization", basic);Также имейте в виду некоторые проблемы в Jellybeans и ошибку при попытке выполнить post-запрос: проблема с базовой аутентификацией HTTP на Android Jelly Bean 4.1 с помощью HttpURLConnection
EDIT: для дайджест-аутентификации
Взгляните сюда https://code.google.com/p/android/issues/detail?id=9579
Особенно это может сработать:
try { HttpClient client = new HttpClient( new MultiThreadedHttpConnectionManager()); client.getParams().setAuthenticationPreemptive(true); Credentials credentials = new UsernamePasswordCredentials("username", "password"); client.getState().setCredentials(AuthScope.ANY, credentials); List<String> authPrefs = new ArrayList<String>(2); authPrefs.add(AuthPolicy.DIGEST); authPrefs.add(AuthPolicy.BASIC); client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs); GetMethod getMethod = new GetMethod("your_url"); getMethod.setRequestHeader("Accept", "application/xml"); client.executeMethod(getMethod); int status = getMethod.getStatusCode(); getMethod.setDoAuthentication(true); System.out.println("status: " + status); if (status == HttpStatus.SC_OK) { String responseBody = getMethod.getResponseBodyAsString(); String resp = responseBody.replaceAll("\n", " "); System.out.println("RESPONSE \n" + resp); } } catch (Exception e) { e.printStackTrace(); }
Я, наконец, заменил устаревший
DefaultHttpClientсвоей собственной реализациейHttpUrlConnectionи реализовалdigest atuhenticationсам, используяЭтот в качестве шаблона.
Окончательный код выглядит примерно так:// requestMethod: "GET", "POST", "PUT" etc. // Headers: A map with the HTTP-Headers for the request // Data: Body-Data for Post/Put int statusCode = this.requestImpl(requestMethod, headers, data); if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED && hasUserNameAndPassword) { String auth = getResponseHeaderField("WWW-Authenticate"); // Server needs Digest authetication if(auth.startsWith("Digest")){ // Parse the auth Header HashMap<String, String> authFields = parseWWWAuthenticateHeader(auth); // Generate Auth-Value for request String requestAuth = generateDigestAuth(authFields); headers.put("Authorization", authStr); statusCode = this.requestImpl(requestMethod, headers, data); } }Поэтому в основном я делаю запрос, и если он возвращает 401, я смотрю, если сервер хочет
digest authenticationи если у меня есть имя пользователя и пароль. Если это так, я разбираю заголовок auth ответа, который содержит всю необходимую информацию о проверке подлинности.
Чтобы разобрать заголовок auth я использую какой-тоStateMachine, который описан здесь.
После разбора заголовка auth ответа я генерирую заголовок auth запроса, используя информацию из ответа:String digestAuthStr = null; String uri = getURL().getPath(); String nonce = authFields.get("nonce"); String realm = authFields.get("realm"); String qop = authFields.get("qop"); String algorithm = authFields.get("algorithm"); String cnonce = generateCNonce(); String nc = "1"; String ha1 = toMD5DigestString(concatWithSeparator(":", username, realm, password)); String ha2 = toMD5DigestString(concatWithSeparator(":", requestMethod, uri)); String response = null; if (!TextUtils.isEmpty(ha1) && !TextUtils.isEmpty(ha2)) response = toMD5DigestString(concatWithSeparator(":", ha1, nonce, nc, cnonce, qop, ha2)); if (response != null) { StringBuilder sb = new StringBuilder(128); sb.append("Digest "); sb.append("username").append("=\"").append(username).append("\", "); sb.append("realm").append("=\"").append(realm).append("\", "); sb.append("nonce").append("=\"").append(nonce).append("\", "); sb.append("uri").append("=\"").append(uri).append("\", "); sb.append("qop").append("=\"").append(qop).append("\", "); sb.append("nc").append("=\"").append(nc).append("\", "); sb.append("cnonce").append("=\"").append(cnonce).append("\""); sb.append("response").append("=\"").append(response).append("\""); sb.append("algorithm").append("=\"").append(algorithm).append("\""); digestAuthStr = sb.toString(); }Для генерации клиент-Nonce я использую следующий код:
Я надеюсь, что это кому-то поможет. Если код содержит какие-либо ошибки, пожалуйста, дайте мне знать, чтобы я мог исправить это. Но прямо сейчас это, кажется, работает.private static String generateCNonce() { String s = ""; for (int i = 0; i < 8; i++) s += Integer.toHexString(new Random().nextInt(16)); return s; }
Для Android я обнаружил, что библиотека bare-bones-digest работает хорошо: https://github.com/al-broco/bare-bones-digest
- Добавьте одну строку для построения.gradle
- Используйте пример кода по указанному выше url
Работает!
Comments