HTTP digest RFC2671规范 加密实现(JAVA)

HTTP diest认证

一、介绍:
可用于访问资源时的身份认证。当客户端未通过服务器的认证,服务器返回401,并传递WWW-Authenticate信息,客户端可通过WWW-Authenticate,生成response返回给服务器来实现认证。


二、实例:

1)客户端发起一次请求,报文如下:
引用

GET / HTTP/1.1
Host: 192.168.7.202:7547
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8



2)服务器响应401,报文如下:
引用

HTTP/1.1 401 Unauthorized
Content-Length: 0
Connection: close
WWW-Authenticate: Digest realm="realm@easycwmp",qop="auth",nonce="c10c9897f05a9aee2e2c5fdebf03bb5b0001b1ef",
opaque="328458fab28345ae87ab3210a8513b14eff452a2"



3)客户端根据WWW-Authenticate生成response,报文如下:
引用

GET / HTTP/1.1
Host: 192.168.7.202:7547
Connection: keep-alive
Authorization: Digest username="povodo", realm="realm@easycwmp", qop=auth, nonce="c10c9897f05a9aee2e2c5fdebf03bb5b0001b1ef", opaque="328458fab28345ae87ab3210a8513b14eff452a2", uri="/", response="77d8c3e7ad338e86b8d039867651b65f", nc=00000001, cnonce="d5324153548c43d8"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8



4)服务器响应:报文如下:
引用

HTTP/1.1 200 OK
Content-Length: 0



一次完整的认证完成了。


三、认证方法实现(JAVA):

加密算法:
//我用的是commons-codec-1.9.jar包
import org.apache.commons.codec.binary.Hex;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
public class Digests {
	/**
	 * 加密遵循RFC2671规范
	 * 将相关参数加密生成一个MD5字符串,并返回
	 */
	public static String http_da_calc_HA1(String username, String realm, String password,
								   String nonce, String nc, String cnonce, String qop,
								   String method, String uri, String algorithm){
		String HA1,HA2;
		if ("MD5-sess".equals(algorithm)){
			HA1 = HA1_MD5_sess(username,realm,password,nonce,cnonce);
		} else{
			HA1 = HA1_MD5(username,realm,password);
		}
		byte[] md5Byte = md5(HA1.getBytes());
		HA1 = new String(Hex.encodeHex(md5Byte));

		md5Byte = md5(HA2(method, uri).getBytes());
		HA2 = new String(Hex.encodeHex(md5Byte));

		String original = HA1 + ":" + (nonce +":"+ nc +":"+ cnonce +":"+ qop) + ":" + HA2;

		md5Byte = md5(original.getBytes());
		return new String(Hex.encodeHex(md5Byte));

	}

	/**
	 * algorithm值为MD5时规则
	 */
	private static String HA1_MD5(String username, String realm, String password){
		return username+":"+realm+":"+password;
	}

	/**
	 * algorithm值为MD5-sess时规则
	 */
	private static String HA1_MD5_sess(String username, String realm, String password, String nonce, String cnonce){
//		MD5(username:realm:password):nonce:cnonce

		String s = HA1_MD5(username, realm, password);
		byte[] md5Byte = md5(s.getBytes());
		String smd5 = new String(Hex.encodeHex(md5Byte));

		return smd5+":"+nonce+":"+cnonce;
	}

	private static String HA2(String method, String uri){
		return method+":"+uri;
	}

	/**
	 * 对输入字符串进行md5散列.
	 */
	public static byte[] md5(byte[] input) {
		return digest(input, MD5, null, 1);
	}

	/**
	 * 对字符串进行散列, 支持md5与sha1算法.
	 */
	private static byte[] digest(byte[] input, String algorithm, byte[] salt, int iterations) {
		try {
			MessageDigest digest = MessageDigest.getInstance(algorithm);

			if (salt != null) {
				digest.update(salt);
			}

			byte[] result = digest.digest(input);

			for (int i = 1; i < iterations; i++) {
				digest.reset();
				result = digest.digest(result);
			}
			return result;
		} catch (GeneralSecurityException e) {
			throw new RuntimeException(e);
		}
	}
//测试
	public static void main(String[] args) {
		String s = http_da_calc_HA1("povodo", "realm@easycwmp", "povodo",
				"c10c9897f05a9aee2e2c5fdebf03bb5b0001b1ef", "00000001", "d5324153548c43d8", "auth",
				"GET", "/", "MD5");
		System.out.println("加密后response为:"+s);
	}




四、客户端请求(GET方式示例):
//我这里用的fastjson-1.1.41.jar
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.povodo.map.servlet.StringUtils;
import org.apache.commons.codec.binary.Hex;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
public class HttpRequestUtils {

    static int nc = 0;    //调用次数

    /**
     * 向指定URL发送GET方法的请求
     * @param url      发送请求的URL
     * @param param    请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @param username 验证所需的用户名
     * @param password 验证所需的密码
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param, String username, String password) {

        StringBuilder result = new StringBuilder();
        BufferedReader in = null;
        try {

            String wwwAuth = sendGet(url, param);       //发起一次授权请求
            if (wwwAuth.startsWith("WWW-Authenticate:")) {
                wwwAuth = wwwAuth.replaceFirst("WWW-Authenticate:", "");
            } else {
                return wwwAuth;
            }

            nc ++;

            String urlNameString = url + (StringUtils.isNotEmpty(param) ? "?" + param : "");
            URL realUrl = new URL(urlNameString);

            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();

            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");

            //授权信息
            String authentication = getAuthorization(wwwAuth, realUrl.getPath(), username, password);
            connection.setRequestProperty("Authorization", authentication);

            // 建立实际的连接
            connection.connect();

            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }

            nc = 0;
        } catch (Exception e) {
            nc = 0;
            throw new RuntimeException(e);
        } finally {
            try {
                if (in != null) in.close();
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result.toString();
    }

    /**
     * 生成授权信息
     * @param authorization 上一次调用返回401的WWW-Authenticate数据
     * @param username      用户名
     * @param password      密码
     * @return 授权后的数据, 应放在http头的Authorization里
     * @throws IOException 异常
     */
    private static String getAuthorization(String authorization, String uri, String username, String password) throws IOException {

        uri = StringUtils.isEmpty(uri) ? "/" : uri;
        String temp = authorization.replaceFirst("Digest", "").trim();
        String json = "{\"" + temp.replaceAll("=", "\":").replaceAll(",", ",\"") + "}";

        JSONObject jsonObject = JSON.parseObject(json);
        String cnonce = new String(Hex.encodeHex(Digests.generateSalt(8)));    //客户端随机数
        String ncstr = ("00000000" + nc).substring(Integer.toString(nc).length());     //认证的次数,第一次是1,第二次是2...
        String algorithm = jsonObject.getString("algorithm");

        String response = Digests.http_da_calc_HA1(username, jsonObject.getString("realm"), password,
                jsonObject.getString("nonce"), ncstr, cnonce, jsonObject.getString("qop"),
                "GET", uri, algorithm);

        //组成响应authorization
        authorization = "Digest username=\"" + username + "\"," + temp;
        authorization += ",uri=\"" + uri
                + "\",nc=\"" + ncstr
                + "\",cnonce=\"" + cnonce
                + "\",response=\"" + response+"\"";
        return authorization;
    }

    /**
     * 向指定URL发送GET方法的请求
     *
     * @param url   发送请求的URL
     * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
     * @return URL 所代表远程资源的响应结果
     */
    public static String sendGet(String url, String param) {
        StringBuilder result = new StringBuilder();
        BufferedReader in = null;
        try {

            String urlNameString = url + (StringUtils.isNotEmpty(param) ? "?" + param : "");
            URL realUrl = new URL(urlNameString);
            // 打开和URL之间的连接
            URLConnection connection = realUrl.openConnection();
            // 设置通用的请求属性
            connection.setRequestProperty("accept", "*/*");
            connection.setRequestProperty("connection", "Keep-Alive");
            connection.setRequestProperty("user-agent",
                    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");

            connection.connect();

            //返回401时需再次用用户名和密码请求
            //此情况返回服务器的 WWW-Authenticate 信息
            if (((HttpURLConnection) connection).getResponseCode() == 401) {
                Map<String, List<String>> map = connection.getHeaderFields();
                return "WWW-Authenticate:" + map.get("WWW-Authenticate").get(0);
            }

            in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line;
            while ((line = in.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            throw new RuntimeException("get请求发送失败",e);
        }
        // 使用finally块来关闭输入流
        finally {
            try {
                if (in != null) in.close();
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result.toString();
    }
}



相关代码已上传附件。
如有错误,欢迎指正!


5、参考:
详解HTTP中的摘要认证机制
RFC 2017文档说明




猜你喜欢

转载自fengjianrong.iteye.com/blog/2379764