如何使用HttpClient认证机制

本文介绍HttpClient的认证机制,并给出示例代码。

author: ZJ 07-11-21



英文版(无代码示例): ​​http://hc.apache.org/httpclient-3.x/authentication.html​



<!--[if !vml]--><!--[endif]-->     HttpClient三种不同的认证方案: Basic, Digest and NTLM. 这些方案可用于服务器或代理对客户端的认证,简称服务器认证或代理认证。

1.服务器认证(Server Authentication) HttpClient处理服务器认证几乎是透明的,仅需要开发人员提供登录信息(login credentials)。登录信息保存在HttpState类的实例中,可以通过 setCredentials(String realm, Credentials cred)和getCredentials(String realm)来获取或设置。 HttpClient内建的自动认证,可以通过HttpMethod类的setDoAuthentication(boolean doAuthentication)方法关闭,而且这次关闭只影响HttpMethod当前的实例。


1.1抢先认证(Preemptive Authentication) 在这种模式时,HttpClient会主动将basic认证应答信息传给服务器,即使在某种情况下服务器可能返回认证失败的应答,这样做主要是为了减少连接的建立。使用该机制如下所示:

client.getParams().setAuthenticationPreemptive(true);

抢先认证模式也提供对于特定目标或代理的缺省认证。如果没有提供缺省的认证信息,则该模式会失效。

Credentials defaultcreds = new UsernamePasswordCredentials("username", "password"); client.getState().setCredentials(new AuthScope("myhost", 80, AuthScope.ANY_REALM), defaultcreds);

Httpclient实现的抢先认证遵循rfc2617.

A client SHOULD assume that all paths at or deeper than the depth of the last symbolic element in the path field of the Request-URI also are within the protection space specified by the Basic realm value of the current challenge. A client MAY preemptively send the corresponding Authorization header with requests for resources in that space without receipt of another challenge from the server. Similarly, when a client sends a request to a proxy, it may reuse a userid and password in the Proxy-Authorization header field without receiving another challenge from the proxy server.



1.2服务器认证的安全方面考虑 当需要与不被信任的站点或web应用通信时,应该谨慎使用缺省的认证机制。当启动(activate)抢先认证模式,或者认证中没有明确给出认证域,主机的HttpClient将使用缺省的认证机制去试图获得目标站点的授权。 如果你提供的认证信息是敏感的,你应该指定认证域。不推荐将认证域指定为AuthScope.ANY。(只有在debugging情况下,才使用)

// To be avoided unless in debug mode Credentials defaultcreds = new UsernamePasswordCredentials("username", "password"); client.getState().setCredentials(AuthScope.ANY, defaultcreds);

2.代理认证(proxy authentication) 除了登录信息需单独存放以外,代理认证与服务器认证几乎一致。用 setProxyCredentials(String realm, Credentials cred)和 getProxyCredentials(String realm)设、取登录信息。 3.认证方案(authentication schemes) 3.1Basic 是HTTP中规定最早的也是最兼容的方案,遗憾的是也是最不安全的一个方案,因为它以明码传送用户名和密码。它要求一个UsernamePasswordCredentials实例,可以指定服务器端的访问空间或采用默认的登录信息。


3.2 Digest 是在HTTP1.1 中增加的一个方案,虽然不如Basic得到的软件支持多,但还是有广泛的使用。Digest方案比Basic方案安全得多,因它根本就不通过网络传送实际的密码,传送的是利用这个密码对从服务器传来的一个随机数(nonce)的加密串。 它要求一个UsernamePasswordCredentials实例,可以指定服务器端的访问空间或采用默认的登录信息。


3.3 NTLM 这是HttpClient支持的最复杂的认证协议。它Microsoft设计的一个私有协议,没有公开的规范说明。一开始由于设计的缺陷,NTLM的安全性比 Digest差,后来经过一个ServicePack补丁后,安全性则比较Digest高。 NTLM需要一个NTCredentials实例。 注意,由于NTLM不使用访问空间(realms)的概念,HttpClient利用服务器的域名作访问空间的名字。还需要注意,提供给 NTCredentials的用户名,不要用域名的前缀 - 如: "adrian" 是正确的,而 "DOMAIN\adrian" 则是错的。 NTLM认证的工作机制与basic和digest有很大的差别。这些差别一般由HttpClient处理,但理解这些差别有助避免在使用NTLM认证时出现错误。 [1] 从HttpClientAPI的角度来看,NTLM与其它认证方式一样的工作,差别是需要提供'NTCredentials'实例而不是'UsernamePasswordCredentials'(其实,前者只是扩展了后者) [2] 对NTLM认证,访问空间是连接到的机器的域名,这对多域名主机会有一些麻烦。只有HttpClient连接中指定的域名才是认证用的域名。建议将realm设为null以使用默认的设置。 [3] NTLM只是认证了一个连接而不是一请求,所以每当一个新的连接建立就要进行一次认证,且在认证的过程中保持连接是非常重要的。 因此,NTLM不能同时用于代理认证和服务器认证,也不能用于HTTP1.0连接或服务器不支持持久连接(keep-alives)的情况。 关于NTLM认证机制更详细的研究,可参考​​http://davenport.sourceforge.net/ntlm.html​​ 。


3.4选择认证 一些服务器支持多种认证方案。假设一次只能使用一种认证方案,HttpClient必须选择使用哪种。HttpClient选择是基于NTLM, Digest, Basic顺序的。 在具体情况下,可以更改该顺序。可通过参数'http.auth.scheme-priority'来实现,该参数值应该被存放在一个String类型的List中。选择优先级是按插入顺序确定的。

HttpClient client = new HttpClient(); List authPrefs = new ArrayList(2); authPrefs.add(AuthPolicy.DIGEST); authPrefs.add(AuthPolicy.BASIC); // This will exclude the NTLM authentication scheme client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);



3.5定制认证方案 HttpClient本身支持basic, digest, and NTLM这三种认证方案。同时,它也提供了加载额外定制的认证方案的功能(通过​​AuthScheme​​​接口实现)。需要使用定制的认证方案,必须实现下面的步骤: [1]实现AuthScheme接口。 [2]通过​​AuthPolicy.registerAuthScheme()​​ 注册定制的AuthScheme。 [3]将定制的AuthScheme加入到AuthPolicy.AUTH_SCHEME_PRIORITY中。 4.示例 4.1Basic authentication

import org.apache.commons.httpclient.HttpClient;


import org.apache.commons.httpclient.UsernamePasswordCredentials;


import org.apache.commons.httpclient.auth.AuthScope;


import org.apache.commons.httpclient.methods.GetMethod;




/**


* A simple example that uses HttpClient to perform a GET using Basic


* Authentication. Can be run standalone without parameters.


*


* You need to have JSSE on your classpath for JDK prior to 1.4


*


* @author Michael Becke


*/


public class BasicAuthenticationExample {




/**


* Constructor for BasicAuthenticatonExample.


*/


public BasicAuthenticationExample() {


super();


}




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


HttpClient client = new HttpClient();




// pass our credentials to HttpClient, they will only be used for


// authenticating to servers with realm "realm" on the host


// "www.verisign.com", to authenticate against


// an arbitrary realm or host change the appropriate argument to null.


client.getState().setCredentials(


new AuthScope("www.verisign.com", 443, "realm"),


new UsernamePasswordCredentials("username", "password")


);




// create a GET method that reads a file over HTTPS, we're assuming


// that this file requires basic authentication using the realm above.


GetMethod get = new GetMethod("https://www.verisign.com/products/index.html");




// Tell the GET method to automatically handle authentication. The


// method will use any appropriate credentials to handle basic


// authentication requests. Setting this value to false will cause


// any request for authentication to return with a status of 401.


// It will then be up to the client to handle the authentication.


get.setDoAuthentication( true );




try {


// execute the GET


int status = client.executeMethod( get );




// print the status and response


System.out.println(status + "\n" + get.getResponseBodyAsString());




} finally {


// release any connection resources used by the method


get.releaseConnection();


}


}


}



4.2 Alternate authentication

import java.util.ArrayList;


import java.util.List;




import org.apache.commons.httpclient.HttpClient;


import org.apache.commons.httpclient.UsernamePasswordCredentials;


import org.apache.commons.httpclient.auth.AuthPolicy;


import org.apache.commons.httpclient.auth.AuthScope;


import org.apache.commons.httpclient.methods.GetMethod;




/**


* <p>A simple example that uses alternate authentication scheme selection


* if several authentication challenges are returned.


* </p>


*


* <p>Per default HttpClient picks the authentication challenge in the following


* order of preference: NTLM, Digest, Basic. In certain cases it may be desirable to


* force the use of a weaker authentication scheme.


* </p>


*


* @author Oleg Kalnichevski


*/


public class AlternateAuthenticationExample {




/**


* Constructor for BasicAuthenticatonExample.


*/


public AlternateAuthenticationExample() {


super();


}




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


HttpClient client = new HttpClient();


client.getState().setCredentials(


new AuthScope("myhost", 80, "myrealm"),


new UsernamePasswordCredentials("username", "password"));


// Suppose the site supports several authetication schemes: NTLM and Basic


// Basic authetication is considered inherently insecure. Hence, NTLM authentication


// is used per default




// This is to make HttpClient pick the Basic authentication scheme over NTLM & Digest


List<String> authPrefs = new ArrayList<String>(3);


authPrefs.add(AuthPolicy.BASIC);


authPrefs.add(AuthPolicy.NTLM);


authPrefs.add(AuthPolicy.DIGEST);


client.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);




GetMethod httpget = new GetMethod("http://myhost/protected/auth-required.html");




try {


int status = client.executeMethod(httpget);


// print the status and response


System.out.println(status);


System.out.println(httpget.getStatusLine());


System.out.println(httpget.getResponseBodyAsString());


} finally {


// release any connection resources used by the method


httpget.releaseConnection();


}


}


}



4.3 Custom authentication

import java.util.ArrayList;


import java.util.Collection;




import org.apache.commons.httpclient.Credentials;


import org.apache.commons.httpclient.HttpMethod;


import org.apache.commons.httpclient.auth.AuthPolicy;


import org.apache.commons.httpclient.auth.AuthScheme;


import org.apache.commons.httpclient.auth.AuthenticationException;


import org.apache.commons.httpclient.auth.MalformedChallengeException;


import org.apache.commons.httpclient.params.DefaultHttpParams;


import org.apache.commons.httpclient.params.HttpParams;




/**


* A simple custom AuthScheme example. The included auth scheme is meant


* for demonstration purposes only. It does not actually implement a usable


* authentication method.


*/


public class CustomAuthenticationExample {




@SuppressWarnings("unchecked")


public static void main(String[] args) {




// register the auth scheme


AuthPolicy.registerAuthScheme(SecretAuthScheme.NAME, SecretAuthScheme.class);




// include the scheme in the AuthPolicy.AUTH_SCHEME_PRIORITY preference,


// this can be done on a per-client or per-method basis but we'll do it


// globally for this example


HttpParams params = DefaultHttpParams.getDefaultParams();


ArrayList<String> schemes = new ArrayList<String>();


schemes.add(SecretAuthScheme.NAME);


schemes.addAll( (Collection) params.getParameter(AuthPolicy.AUTH_SCHEME_PRIORITY));


params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);




// now that our scheme has been registered we can execute methods against


// servers that require "Secret" authentication...


}




/**


* A custom auth scheme that just uses "Open Sesame" as the authentication


* string.


*/


private class SecretAuthScheme implements AuthScheme {




public static final String NAME = "Secret";




public SecretAuthScheme() {


// All auth schemes must have a no arg constructor.


}


public String authenticate(Credentials credentials, HttpMethod method)


throws AuthenticationException {


return "Open Sesame";


}


public String authenticate(Credentials credentials, String method,


String uri) throws AuthenticationException {


return "Open Sesame";


}


public String getID() {


return NAME;


}


public String getParameter(String name) {


// this scheme does not use parameters, see RFC2617Scheme for an example


return null;


}


public String getRealm() {


// this scheme does not use realms


return null;


}


public String getSchemeName() {


return NAME;


}


public boolean isConnectionBased() {


return false;


}


public void processChallenge(String challenge)


throws MalformedChallengeException {


// Nothing to do here, this is not a challenge based


// auth scheme. See NTLMScheme for a good example.


}


public boolean isComplete() {


// again we're not a challenge based scheme so this is always true


return true;


}


}


}



4.4 Interactive authentication

import java.io.BufferedReader;


import java.io.IOException;


import java.io.InputStreamReader;




import org.apache.commons.httpclient.Credentials;


import org.apache.commons.httpclient.HttpClient;


import org.apache.commons.httpclient.NTCredentials;


import org.apache.commons.httpclient.UsernamePasswordCredentials;


import org.apache.commons.httpclient.auth.AuthScheme;


import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;


import org.apache.commons.httpclient.auth.CredentialsProvider;


import org.apache.commons.httpclient.auth.NTLMScheme;


import org.apache.commons.httpclient.auth.RFC2617Scheme;


import org.apache.commons.httpclient.methods.GetMethod;




/**


* A simple example that uses HttpClient to perform interactive


* authentication.


*


* @author Oleg Kalnichevski


*/


public class InteractiveAuthenticationExample {




/**


* Constructor for InteractiveAuthenticationExample.


*/


public InteractiveAuthenticationExample() {


super();


}




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




InteractiveAuthenticationExample demo = new InteractiveAuthenticationExample();


demo.doDemo();


}




private void doDemo() throws IOException {




HttpClient client = new HttpClient();


client.getParams().setParameter(


CredentialsProvider.PROVIDER, new ConsoleAuthPrompter());


GetMethod httpget = new GetMethod("http://target-host/requires-auth.html");


httpget.setDoAuthentication(true);


try {


// execute the GET


int status = client.executeMethod(httpget);


// print the status and response


System.out.println(status);


System.out.println(httpget.getStatusLine().toString());


System.out.println(httpget.getResponseBodyAsString());


} finally {


// release any connection resources used by the method


httpget.releaseConnection();


}


}




public class ConsoleAuthPrompter implements CredentialsProvider {




private BufferedReader in = null;


public ConsoleAuthPrompter() {


super();


this.in = new BufferedReader(new InputStreamReader(System.in));


}




private String readConsole() throws IOException {


return this.in.readLine();


}




public Credentials getCredentials(


final AuthScheme authscheme,


final String host,


int port,


boolean proxy)


throws CredentialsNotAvailableException


{


if (authscheme == null) {


return null;


}


try{


if (authscheme instanceof NTLMScheme) {


System.out.println(host + ":" + port + " requires Windows authentication");


System.out.print("Enter domain: ");


String domain = readConsole();


System.out.print("Enter username: ");


String user = readConsole();


System.out.print("Enter password: ");


String password = readConsole();


return new NTCredentials(user, password, host, domain);


} else


if (authscheme instanceof RFC2617Scheme) {


System.out.println(host + ":" + port + " requires authentication with the realm '"


+ authscheme.getRealm() + "'");


System.out.print("Enter username: ");


String user = readConsole();


System.out.print("Enter password: ");


String password = readConsole();


return new UsernamePasswordCredentials(user, password);


} else {


throw new CredentialsNotAvailableException("Unsupported authentication scheme: " +


authscheme.getSchemeName());


}


} catch (IOException e) {


throw new CredentialsNotAvailableException(e.getMessage(), e);


}


}


}


}