美文网首页
摘要认证

摘要认证

作者: 盗生一 | 来源:发表于2020-12-16 14:39 被阅读0次

    摘要认证(digest authentication)

    原文:https://www.jianshu.com/p/73115db79198

    摘要认证(Digest authentication)用来提供比基础认证更高级别的安全。在RFC2617中有关于它的描述,摘要认证是一种基于挑战-应答模式的认证模型。
    摘要认证与基础认证的工作原理很相似,用户先发出一个没有认证证书的请求,Web服务器回复一个带有WWW-Authenticate头的响应,指明访问所请求的资源需要证书。但是和基础认证发送以Base 64编码的用户名和密码不同,在摘要认证中服务器让客户选一个随机数(称作”nonce“),然后浏览器使用一个单向的加密函数生成一个消息摘要(message digest),该摘要是关于用户名、密码、给定的nonce值、HTTP方法,以及所请求的URL。消息摘要函数也被称为散列算法,是一种在一个方向上很容易计算,反方向却不可行的加密算法。与基础认证对比,解码基础认证中的Base 64是很容易办到的。在服务器口令中,可以指定任意的散列算法。在RFC 2617中,描述了以MD5散列函数作为默认算法。

    理解

    上面的一大段话是从百度上复制过来的,简单的来说,分为如下三点:

    • 第一次向接口发起空请求

    返回一个401Unauthorized未授权的信息,请求头中会带有如下的参数:
    HTTP /1.1 401 Unauthorized
    WWW-Authenticate:Digest
    realm= ”test realm”
    qop=auth,auth-int”
    nonce=”66C4EF58DA7CB956BD04233FBB64E0A4”
    opaque=“5ccc069c403ebaf9f0171e9517f40e41”
    对上面的参数做一下说明:
    • realm的值是一个简单的字符串
    • qop是认证的(校验)方式
    • nonce是随机数, 可以用GUID
    • opaque是个随机字符串,它只是透传而已,即客户端还会原样返回过来。
    • algorithm 是个字符串,用来指示用来产生分类及校验和的算法对。如果该域没指定,则认为是“MD5“算法。

    • 客户端需要进行认证

    弹出让用户输入用户名和密码的认证窗口,客户端选择一个算法,计算出密码和其他数据的摘要(response),将摘要放到Authorization的请求头中发送给服务器,如果客户端要对服务器也进行认证,这个时候,可以发送客户端随机数cnonce。

    • 服务接受摘要,选择算法,获取数据库用户名密码,重新计算新的摘要跟客户端传输的摘要进行比较,验证是否匹配。
      200 OK
    • 一般使用MD5加密算法。

    POSTMAN中的操作

    • 第一次发起空请求

      image
    image
    • 第二次请求将第一次返回的数据,和账号密码,进行加密,再次发起请求,成功

      image

    java代码实现:

    • HttpRequestUtils
    public class HttpRequestUtils {
    
        private static final Logger logger = LoggerFactory.getLogger(HttpRequestUtils.class);
    
        static int nc = 0;    //调用次数
        private static final String GET = "GET";
        private static final String POST = "POST";
        private static final String PUT = "PUT";
        private static final String DELETE = "DELETE";
    
        /**
         * 向指定URL发送DELETE方法的请求
         * @param url                                   发送请求的URL
         * @param param                                 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
         * @param username                              验证所需的用户名
         * @param password                              验证所需的密码
         * @param json                                  请求json字符串
         * @param type                                  返回xml和json格式数据,默认xml,传入json返回json数据
         * @return URL                                  所代表远程资源的响应结果
         */
        public static String sendDelete(String url, String param, String username, String password, String json, String type) {
    
            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之间的连接
                HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
    
                // 设置是否向connection输出,因为这个是post请求,参数要放在
                // http正文内,因此需要设为true
                connection.setDoOutput(true);
                // Read from the connection. Defaultis true.
                connection.setDoInput(true);
                // 默认是 GET方式
                connection.setRequestMethod(DELETE);
    
                // 设置通用的请求属性
                setRequestProperty(connection, wwwAuth, realUrl, username, password, DELETE, type);
    
                if (!StringUtils.isEmpty(json)) {
                    byte[] writebytes = json.getBytes();
                    connection.setRequestProperty("Content-Length", String.valueOf(writebytes.length));
                    OutputStream outwritestream = connection.getOutputStream();
                    outwritestream.write(json.getBytes());
                    outwritestream.flush();
                    outwritestream.close();
                }
    
                if (connection.getResponseCode() == 200) {
                    in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    String line;
                    while ((line = in.readLine()) != null) {
                        result.append(line);
                    }
                } else {
                    String errResult = formatResultInfo(connection, type);
                    logger.info(errResult);
                    return errResult;
                }
    
                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();
        }
    
        /**
         * 向指定URL发送PUT方法的请求
         * @param url                                      发送请求的URL
         * @param param                                    请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
         * @param username                                 验证所需的用户名
         * @param password                                 验证所需的密码
         * @param json                                     请求json字符串
         * @param type                                     返回xml和json格式数据,默认xml,传入json返回json数据
         * @return URL                                     所代表远程资源的响应结果
         */
        public static String sendPUT(String url, String param, String username, String password, String json, String type) {
    
            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之间的连接
                HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
    
                // 设置是否向connection输出,因为这个是post请求,参数要放在
                // http正文内,因此需要设为true
                connection.setDoOutput(true);
                // Read from the connection. Defaultis true.
                connection.setDoInput(true);
                // 默认是 GET方式
                connection.setRequestMethod(PUT);
                // Post 请求不能使用缓存
                connection.setUseCaches(false);
    
                // 设置通用的请求属性
                setRequestProperty(connection, wwwAuth, realUrl, username, password, PUT, type);
    
                if (!StringUtils.isEmpty(json)) {
                    byte[] writebytes = json.getBytes();
                    connection.setRequestProperty("Content-Length", String.valueOf(writebytes.length));
                    OutputStream outwritestream = connection.getOutputStream();
                    outwritestream.write(json.getBytes());
                    outwritestream.flush();
                    outwritestream.close();
                }
    
                if (connection.getResponseCode() == 200) {
                    in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    String line;
                    while ((line = in.readLine()) != null) {
                        result.append(line);
                    }
                } else {
                    String errResult = formatResultInfo(connection, type);
                    logger.info(errResult);
                    return errResult;
                }
    
                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();
        }
    
        /**
         * 向指定URL发送POST方法的请求
         * @param url                                       发送请求的URL
         * @param param                                     请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
         * @param username                                  验证所需的用户名
         * @param password                                  验证所需的密码
         * @param json                                      请求json字符串
         * @param type                                      返回xml和json格式数据,默认xml,传入json返回json数据
         * @return URL 所代表远程资源的响应结果
         */
        public static String sendPost(String url, String param, String username, String password, String json, String type) {
    
            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之间的连接
                HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
    
                // 设置是否向connection输出,因为这个是post请求,参数要放在
                // http正文内,因此需要设为true
                connection.setDoOutput(true);
                // Read from the connection. Defaultis true.
                connection.setDoInput(true);
                // 默认是 GET方式
                connection.setRequestMethod(POST);
                // Post 请求不能使用缓存
                connection.setUseCaches(false);
    
                // 设置通用的请求属性
                setRequestProperty(connection, wwwAuth, realUrl, username, password, POST, type);
    
                if (!StringUtils.isEmpty(json)) {
                    byte[] writebytes = json.getBytes();
                    connection.setRequestProperty("Content-Length", String.valueOf(writebytes.length));
                    OutputStream outwritestream = connection.getOutputStream();
                    outwritestream.write(json.getBytes());
                    outwritestream.flush();
                    outwritestream.close();
                }
                if (connection.getResponseCode() == 200 || connection.getResponseCode() == 201) {
                    in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                    String line;
                    while ((line = in.readLine()) != null) {
                        result.append(line);
                    }
                } else {
                    String errResult = formatResultInfo(connection, type);
                    logger.info(errResult);
                    return errResult;
                }
    
                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();
        }
    
        /**
         * 向指定URL发送GET方法的请求
         * @param url                                       发送请求的URL
         * @param param                                     请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
         * @param username                                  验证所需的用户名
         * @param password                                  验证所需的密码
         * @param type                                      返回xml和json格式数据,默认xml,传入json返回json数据
         * @return URL 所代表远程资源的响应结果
         */
        public static String sendGet(String url, String param, String username, String password, String type) {
    
            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之间的连接
                HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
                // 设置通用的请求属性
                setRequestProperty(connection, wwwAuth, realUrl, username, password, GET, type);
                // 建立实际的连接
                // 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, String method) throws IOException {
    
            uri = StringUtils.isEmpty(uri) ? "/" : uri;
            // String temp = authorization.replaceFirst("Digest", "").trim();
            String temp = authorization.replaceFirst("Digest", "").trim().replace("MD5", "\"MD5\"");
            // String json = "{\"" + temp.replaceAll("=", "\":").replaceAll(",", ",\"") + "}";
            String json = withdrawJson(authorization);
            // String json = "{ \"realm\": \"Wowza\", \" domain\": \"/\", \" nonce\": \"MTU1NzgxMTU1NzQ4MDo2NzI3MWYxZTZkYjBiMjQ2ZGRjYTQ3ZjNiOTM2YjJjZA==\", \" algorithm\": \"MD5\", \" qop\": \"auth\" }";
    
            JSONObject jsonObject = JSON.parseObject(json);
            // String cnonce = new String(Hex.encodeHex(com.lys.util.Digests.generateSalt(8)));    //客户端随机数
            String cnonce = Digests.generateSalt2(8);
            String ncstr = ("00000000" + nc).substring(Integer.toString(nc).length());     //认证的次数,第一次是1,第二次是2...
            // String algorithm = jsonObject.getString("algorithm");
            String algorithm = jsonObject.getString("algorithm");
            String qop = jsonObject.getString("qop");
            String nonce = jsonObject.getString("nonce");
            String realm = jsonObject.getString("realm");
    
            String response = Digests.http_da_calc_HA1(username, realm, password,
                    nonce, ncstr, cnonce, qop,
                    method, uri, algorithm);
    
            //组成响应authorization
            authorization = "Digest username=\"" + username + "\"," + temp;
            authorization += ",uri=\"" + uri
                    + "\",nc=\"" + ncstr
                    + "\",cnonce=\"" + cnonce
                    + "\",response=\"" + response + "\"";
            return authorization;
        }
    
        /**
         * 将返回的Authrization信息转成json
         * @param authorization                                     authorization info
         * @return 返回authrization json格式数据 如:String json = "{ \"realm\": \"Wowza\", \" domain\": \"/\", \" nonce\": \"MTU1NzgxMTU1NzQ4MDo2NzI3MWYxZTZkYjBiMjQ2ZGRjYTQ3ZjNiOTM2YjJjZA==\", \" algorithm\": \"MD5\", \" qop\": \"auth\" }";
         */
        private static String withdrawJson(String authorization) {
            String temp = authorization.replaceFirst("Digest", "").trim().replaceAll("\"", "");
            // String noncetemp = temp.substring(temp.indexOf("nonce="), temp.indexOf("uri="));
            // String json = "{\"" + temp.replaceAll("=", "\":").replaceAll(",", ",\"") + "}";
            String[] split = temp.split(",");
            Map<String, String> map = new HashMap<>();
            Arrays.asList(split).forEach(c -> {
                String c1 = c.replaceFirst("=", ":");
                String[] split1 = c1.split(":");
                map.put(split1[0].trim(), split1[1].trim());
            });
            return JSONObject.toJSONString(map);
        }
    
        /**
         * 向指定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();
        }
    
        /**
         * HTTP set request property
         *
         * @param connection                            HttpConnection
         * @param wwwAuth                               授权auth
         * @param realUrl                               实际url
         * @param username                              验证所需的用户名
         * @param password                              验证所需的密码
         * @param method                                请求方式
         * @param type                                  返回xml和json格式数据,默认xml,传入json返回json数据
         */
        private static void setRequestProperty(HttpURLConnection connection, String wwwAuth, URL realUrl, String username, String password, String method, String type)
                throws IOException {
    
            if (type != null && type.equals("json")) {
                // 返回json
                connection.setRequestProperty("User-Identify", "3502067100000030220902001100452020052700000001");
                connection.setRequestProperty("accept", "application/json;charset=UTF-8");
                connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
                connection.setRequestProperty("connection", "Keep-Alive");
                connection.setRequestProperty("user-agent",
                        "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
            } else {
                // 返回xml
                if (!method.equals(GET)) {
                    connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
                }
                connection.setRequestProperty("accept", "*/*");
                connection.setRequestProperty("connection", "Keep-Alive");
                // connection.setRequestProperty("Cache-Control", "no-cache");
                connection.setRequestProperty("user-agent",
                        "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
    
            }
            //授权信息
            String authentication = getAuthorization(wwwAuth, realUrl.getPath(), username, password, method);
            connection.setRequestProperty("Authorization", authentication);
        }
    
        /**
         * 格式化请求返回信息,支持json和xml格式
         * @param connection                                HttpConnection
         * @param type                                      指定返回数据格式,json、xml,默认xml
         * @return 返回数据
         */
        private static String formatResultInfo(HttpURLConnection connection, String type) throws IOException {
            String result = "";
            if ("json".equals(type)) {
                result = String.format("{\"errCode\":%s, \"message\":%s}", connection.getResponseCode(), connection.getResponseMessage());
            } else {
                result = String.format(" <?xml version=\"1.0\" encoding=\"UTF-8\" ?> "
                        + " <wmsResponse>"
                        + " <errCode>%d</errCode>"
                        + " <message>%s</message>"
                        + " </wmsResponse>", connection.getResponseCode(), connection.getResponseMessage());
            }
            return result;
        }
    
    }
    
    
    • Digests
    /**
     * Http Digest
     *
     * @author 刘岩松
     * @date 2020/6/23 9:37
     */
    public class Digests {
    
        private static SecureRandom random = new SecureRandom();
    
        /**
         * 加密遵循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);
            }
        }
    
        /**
         * 随机生成numBytes长度数组
         * @param numBytes
         * @return
         */
        public static byte[] generateSalt(int numBytes) {
            Validate.isTrue(numBytes > 0, "numBytes argument must be a positive integer (1 or larger)", (long) numBytes);
            byte[] bytes = new byte[numBytes];
            random.nextBytes(bytes);
            return bytes;
        }
    
        @Deprecated
        public static String generateSalt2(int length) {
            String val = "";
            Random random = new Random();
            //参数length,表示生成几位随机数
            for (int i = 0; i < length; i++) {
                String charOrNum = random.nextInt(2) % 2 == 0 ? "char" : "num";
                //输出字母还是数字
                if ("char".equalsIgnoreCase(charOrNum)) {
                    //输出是大写字母还是小写字母
                    int temp = random.nextInt(2) % 2 == 0 ? 65 : 97;
                    val += (char) (random.nextInt(26) + temp);
                } else if ("num".equalsIgnoreCase(charOrNum)) {
                    val += String.valueOf(random.nextInt(10));
                }
            }
            return val.toLowerCase();
        }
    }
    
    
    • 使用:调用HttpRequestUtils.sendXxx()方法,即可。

    </article>

    相关文章

      网友评论

          本文标题:摘要认证

          本文链接:https://www.haomeiwen.com/subject/mwgngktx.html