• HTTP的几种认证方式之DIGEST 认证(摘要认证)


    HTTP/1.1 使用的认证方式有

      1)BASIC 认证(基本认证);

      2)DIGEST 认证(摘要认证);

      3)SSL 客户端认证;

      4)FormBase 认证(基于表单认证);

     本文目录:

    1、DIGEST 认证的步骤
    2、Digest 认证涉及到的参数的含义
    3、校验 response 的算法
    4、Java + SpringBoot实现 DIGEST 认证
    5、测试及结果分析
    6、注意事项

    1、DIGEST 认证的步骤    <-- 返回目录

      为弥补 BASIC 认证存在的弱点,从 HTTP/1.1 起就有了 DIGEST 认证。 DIGEST 认证同样使用质询 / 响应的方式(challenge/response),但不会像 BASIC 认证那样直接发送明文密码。
      所谓质询响应方式是指,一开始一方会先发送认证要求给另一方,接着使用从另一方那接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证的方式。

      DIGEST 认证的步骤:

       步骤 1: 请求需认证的资源时,服务器会随着状态码 401Authorization Required,返回带WWW-Authenticate 首部字段的响应。该字段内包含质问响应方式认证所需的临时质询码(随机数,nonce)。首部字段 WWW-Authenticate 内必须包含realm 和nonce 这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。nonce 是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由Base64 编码的十六进制数的组成形式,但实际内容依赖服务器的具体实现。

      步骤 2:接收到401状态码的客户端,返回的响应中包含 DIGEST 认证必须的首部字段 Authorization 信息。首部字段 Authorization 内必须包含 username、realm、nonce、uri 和response的字段信息。其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。
      username是realm 限定范围内可进行认证的用户名。uri(digest-uri)即Request-URI的值,但考虑到经代理转发后Request-URI的值可能被修改因此事先会复制一份副本保存在 uri内。

      response 也可叫做 Request-Digest,存放经过 MD5 运算后的密码字符串,形成响应码。

      步骤 3:接收到包含首部字段 Authorization 请求的服务器,会确认认证信息的正确性。认证通过后则返回包含 Request-URI 资源的响应。并且这时会在首部字段 Authentication-Info 写入一些认证成功的相关信息。(不过我下面的例子没有去写这个Authentication-Info,而是直接返回的数据。因为我实在session里缓存的认证结果)。

    2、Digest 认证涉及到的参数的含义    <-- 返回目录

    WWW-Authentication:用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源
    realm:表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
    qop:保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
    nonce:服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击
    nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
    cnonce:客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
    response:这是由用户代理软件计算出的一个字符串,以证明用户知道口令
    Authorization-Info:用于返回一些与授权会话相关的附加信息
    nextnonce:下一个服务端随机数,使客户端可以预先发送正确的摘要
    rspauth:响应摘要,用于客户端对服务端进行认证
    stale:当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了

    3、校验 response 的算法    <-- 返回目录
      浏览器 Authorization 的内容举例:

    Digest username="q", realm="test", nonce="T53sV+xXH3FrrER4YZwpFQ==", uri="/portal/applications", 
    response="f80492644b0700b404f2fb3f4d62861e", qop=auth, nc=00000001, cnonce="25c980f9f95fd544"

      其中 response 是根据如下算法计算得到:

    response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))

      所以,服务器的校验逻辑就包括校验 Authorization 请求头里面的 response 的值。可以看到,response 算法里面有个 password 参数,浏览器通过弹框用户输入密码的值得到,服务器是通过用户名从数据库查到的。摘要验证主要就是通过上面的HASH比较的步骤避免掉了基本验证中的安全性问题

    4、Java + SpringBoot实现 DIGEST 认证    <-- 返回目录

         application.properties

    server.port=8089
    server.servlet.context-path=/BootDemo
    View Code

      自定义注解 RequireAuth

    package com.oy.interceptor;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    // can be used to method
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface RequireAuth {
        
    }
    View Code

      拦截器 RequireAuthInterceptor

    package com.oy.interceptor;
    
    import java.text.MessageFormat;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    import com.oy.model.DigestAuthInfo;
    import com.oy.util.DigestUtils;
    
    public class RequireAuthInterceptor extends HandlerInterceptorAdapter {
        
        // 为了 测试Digest nc 值每次请求增加
        private int nc = 0;
    
        @Override
        public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) throws Exception {
            // 请求目标为 method of controller,需要进行验证
            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                Object object = handlerMethod.getMethodAnnotation(RequireAuth.class);
    
                /* 方法没有 @RequireAuth 注解, 放行 */
                if (object == null) {
                    return true; // 放行
                }
    
                /* 方法有 @RequireAuth 注解,需要拦截校验 */
                // 没有 Authorization 请求头,或者 Authorization 认证信息验证不通过,拦截
                if (!isAuth(req, res)) {
                    // 验证不通过,拦截
                    return false;
                }
    
                // 验证通过,放行
                return true;
            }
    
            // 请求目标不是 mehod of controller, 放行
            return true;
        }
    
        private boolean isAuth(HttpServletRequest req, HttpServletResponse res) {
            String authStr = req.getHeader("Authorization");
            System.out.println("请求 Authorization 的内容:" + authStr);
            if (authStr == null || authStr.length() <= 7) {
                // 没有 Authorization 请求头,开启质询
                return challenge(res);
            }
    
            DigestAuthInfo authObject = DigestUtils.getAuthInfoObject(authStr);
            // System.out.println(authObject);
    
            /*
             * 生成 response 的算法:
             *  response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))
             */
            // 这里密码固定为 123456, 实际应用需要根据用户名查询数据库或缓存获得
            String HA1 = DigestUtils.MD5(authObject.getUsername() + ":" + authObject.getRealm() + ":123456");
            String HD = String.format(authObject.getNonce() + ":" + authObject.getNc() + ":" + authObject.getCnonce() + ":"
                    + authObject.getQop());
            String HA2 = DigestUtils.MD5(req.getMethod() + ":" + authObject.getUri());
            String responseValid = DigestUtils.MD5(HA1 + ":" + HD + ":" + HA2);
    
            // 如果 Authorization 中的 response(浏览器生成的) 与期望的 response(服务器计算的) 相同,则验证通过
            System.out.println("Authorization 中的 response: " + authObject.getResponse());
            System.out.println("期望的 response: " + responseValid);
            if (responseValid.equals(authObject.getResponse())) {
                /* 判断 nc 的值,用来防重放攻击 */
                // 判断此次请求的 Authorization 请求头里面的 nc 值是否大于之前保存的 nc 值
                // 大于,替换旧值,然后 return true
                // 否则,return false
                
                // 测试代码 start
                int newNc = Integer.parseInt(authObject.getNc(), 16);
                System.out.println("old nc: " + this.nc + ", new nc: " + newNc);
                if (newNc > this.nc) {
                    this.nc = newNc;
                    return true;
                }
                return false;
                // 测试代码 end
            }
    
            // 验证不通过,重复质询
            return challenge(res);
        }
    
        /**
         * 质询:返回状态码 401 和 WWW-Authenticate 响应头
         * 
         * @param res 返回false,则表示拦截器拦截请求
         */
        private boolean challenge(HttpServletResponse res) {
            // 质询前,重置或删除保存的与该用户关联的 nc 值(nc:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量)
            // 将 nc 置为初始值 0, 这里代码省略
            
            // 测试代码 start
            this.nc = 0;
            // 测试代码 end
            
            res.setStatus(401);
            String str = MessageFormat.format("Digest realm={0},nonce={1},qop={2}", ""no auth"",
                    """ + DigestUtils.generateToken() + """, ""auth"");
            res.addHeader("WWW-Authenticate", str);
            return false;
        }
    
    }
    View Code

      注册拦截器 WebConfig

    package com.oy;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    import com.oy.interceptor.RequireAuthInterceptor;
    
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            RequireAuthInterceptor requireAuthInterceptor = new RequireAuthInterceptor();
            registry.addInterceptor(requireAuthInterceptor);
        }
        
    }
    View Code

      DIGEST认证信息model类 DigestAuthInfo

    package com.oy.model;
    
    public class DigestAuthInfo {
        private String username;
        private String realm;
        private String nonce;
        private String uri;
        private String response;
        private String qop;
        private String nc;
        public String cnonce;
        
        public String getUsername() {
            return username;
        }
        public void setUsername(String username) {
            this.username = username;
        }
        public String getRealm() {
            return realm;
        }
        public void setRealm(String realm) {
            this.realm = realm;
        }
        public String getNonce() {
            return nonce;
        }
        public void setNonce(String nonce) {
            this.nonce = nonce;
        }
        public String getUri() {
            return uri;
        }
        public void setUri(String uri) {
            this.uri = uri;
        }
        public String getResponse() {
            return response;
        }
        public void setResponse(String response) {
            this.response = response;
        }
        public String getQop() {
            return qop;
        }
        public void setQop(String qop) {
            this.qop = qop;
        }
        public String getNc() {
            return nc;
        }
        public void setNc(String nc) {
            this.nc = nc;
        }
        public String getCnonce() {
            return cnonce;
        }
        public void setCnonce(String cnonce) {
            this.cnonce = cnonce;
        }
        @Override
        public String toString() {
            return "DigestAuthInfo [username=" + username + ", realm=" + realm + ", nonce=" + nonce + ", uri=" + uri
                    + ", response=" + response + ", qop=" + qop + ", nc=" + nc + ", cnonce=" + cnonce + "]";
        }
        
    }
    View Code

      DIGEST认证的工具类 DigestUtils

    package com.oy.util;
    
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.util.Base64;
    import java.util.Random;
    
    import org.junit.Test;
    
    import com.oy.model.DigestAuthInfo;
    
    public class DigestUtils {
    
        /**
         * 根据当前时间戳生成一个随机字符串
         * @return
         */
        public static String generateToken() {
            String s = String.valueOf(System.currentTimeMillis() + new Random().nextInt());
    
            try {
                MessageDigest messageDigest = MessageDigest.getInstance("md5");
                byte[] digest = messageDigest.digest(s.getBytes());
    
                return Base64.getEncoder().encodeToString(digest);
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException();
            }
        }
    
        @Test
        public void testGenerateToken() {
            // heL2WICEml8/UGfAQsS9mQ==
            System.out.println(generateToken());
        }
    
        public static String MD5(String inStr) {
            MessageDigest md5 = null;
    
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (Exception e) {
                System.out.println(e.toString());
                e.printStackTrace();
                return "";
            }
    
            char[] charArray = inStr.toCharArray();
            byte[] byteArray = new byte[charArray.length];
    
            for (int i = 0; i < charArray.length; i++) {
                byteArray[i] = (byte) charArray[i];
            }
    
            byte[] md5Bytes = md5.digest(byteArray);
            StringBuffer hexValue = new StringBuffer();
    
            for (int i = 0; i < md5Bytes.length; i++) {
                int val = ((int) md5Bytes[i]) & 0xff;
                if (val < 16)
                    hexValue.append("0");
                hexValue.append(Integer.toHexString(val));
            }
    
            return hexValue.toString();
        }
        
        /**
         * 该方法用于将 Authorization 请求头的内容封装成一个对象。
         * 
         * Authorization 请求头的内容为:
         *     Digest username="aaa", realm="no auth", nonce="b2b74be03ff44e1884ba0645bb961b53",
         *     uri="/BootDemo/login", response="90aff948e6f2207d69ecedc5d39f6192", qop=auth,
         *     nc=00000002, cnonce="eb73c2c68543faaa"
         */
        public static DigestAuthInfo getAuthInfoObject(String authStr) {
            if (authStr == null || authStr.length() <= 7)
                return null;
    
            if (authStr.toLowerCase().indexOf("digest") >= 0) {
                // 截掉前缀 Digest
                authStr = authStr.substring(6);
            }
    
            // 将双引号去掉
            authStr = authStr.replaceAll(""", "");
    
            DigestAuthInfo digestAuthObject = new DigestAuthInfo();
            String[] authArray = new String[8];
            authArray = authStr.split(",");
            // System.out.println(java.util.Arrays.toString(authArray));
    
            for (int i = 0, len = authArray.length; i < len; i++) {
                String auth = authArray[i];
                String key = auth.substring(0, auth.indexOf("=")).trim();
                String value = auth.substring(auth.indexOf("=") + 1).trim();
                switch (key) {
                    case "username":
                        digestAuthObject.setUsername(value);
                        break;
                    case "realm":
                        digestAuthObject.setRealm(value);
                        break;
                    case "nonce":
                        digestAuthObject.setNonce(value);
                        break;
                    case "uri":
                        digestAuthObject.setUri(value);
                        break;
                    case "response":
                        digestAuthObject.setResponse(value);
                        break;
                    case "qop":
                        digestAuthObject.setQop(value);
                        break;
                    case "nc":
                        digestAuthObject.setNc(value);
                        break;
                    case "cnonce":
                        digestAuthObject.setCnonce(value);
                        break;
                }
            }
            return digestAuthObject;
        }
        
    }
    View Code

      测试接口类 IndexController

    package com.oy.controller;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import com.oy.interceptor.RequireAuth;
    
    @Controller
    public class IndexController {
    
        @RequireAuth
        @RequestMapping("/login")
        @ResponseBody
        public String login(HttpServletRequest req, HttpServletResponse res) {
            return "{code: 0, data: {username:"test"}}";
        }
    
        @RequireAuth
        @RequestMapping("/index")
        @ResponseBody
        public String index(HttpServletRequest req, HttpServletResponse res) {
            return "{code: 0, data: {xxx:"xxx"}}";
        }
    
    }
    View Code

    5、测试及结果分析    <-- 返回目录

      测试流程:

      1)打开浏览器,输入http://localhost:8089/BootDemo/login,回车后弹出对话框;

      2)第一次输入错误密码(比如123),确定后会再次弹出对话框;

      3)第二次输入正确密码(123456),回车,返回状态码 200 和数据;

      4)然后刷新两次页面;

      后台控制台打印结果及解释:

    // 打开浏览器,输入http://localhost:8089/BootDemo/login
    // 此时,浏览器发起的请求中没有带 Authorization 请求头
    请求 Authorization 的内容:null
    
    // 第一次质询,测试时故意密码输入错误
    // nonce 由服务器生成的,浏览器在 Authorization 请求头中带回
    // 服务器质询,会响应 401 和 请求头 WWW-Authenticate
    // WWW-Authenticate: Digest realm="no auth",nonce="rULh6M3A6O2N8jjzxr6vJg==",qop="auth"
    请求 Authorization 的内容:Digest username="aaa", realm="no auth", nonce="rULh6M3A6O2N8jjzxr6vJg==", uri="/BootDemo/login", 
    response="488d501c80ff7ac9a02bdf0b78125b6e", qop=auth, nc=00000001, cnonce="2ba7cc66c38cb785" Authorization 中的 response: 488d501c80ff7ac9a02bdf0b78125b6e 期望的 response: 8c53f6738244a07d1b36a37aa0c9566a // 第二次质询,输入正确的密码 // 重新质询,服务器重新生成一个 nonce, 浏览器在下次请求的 Authorization 请求头中带回 请求 Authorization 的内容:Digest username="aaa", realm="no auth", nonce="VeW1RtC+B6eQeehbgPFjEA==", uri="/BootDemo/login",
    response="4f3d8b1db57f1f216397ab68dcbccf38", qop=auth, nc=00000001, cnonce="4e7ac593c141cc68" Authorization 中的 response: 4f3d8b1db57f1f216397ab68dcbccf38 期望的 response: 4f3d8b1db57f1f216397ab68dcbccf38 old nc: 0, new nc: 1 // 再次请求,不会再质询,浏览器之间发送 Authorization 请求头 // 此次请求,nonce 的值为上次质询成功保存的值,nc 为 00000002 请求 Authorization 的内容:Digest username="aaa", realm="no auth", nonce="VeW1RtC+B6eQeehbgPFjEA==", uri="/BootDemo/login",
    response="daf87926d28ac8629c572444b218fc35", qop=auth, nc=00000002, cnonce="41d0d6316bec00e1" Authorization 中的 response: daf87926d28ac8629c572444b218fc35 期望的 response: daf87926d28ac8629c572444b218fc35 old nc: 1, new nc: 2 // 再次请求,不会再质询,浏览器之间发送 Authorization 请求头 // 此次请求,nonce 的值为上次质询成功保存的值,nc 为 00000003 请求 Authorization 的内容:Digest username="aaa", realm="no auth", nonce="VeW1RtC+B6eQeehbgPFjEA==", uri="/BootDemo/login",
    response="7342c2fe0ab2366d89940f3f1d204de7", qop=auth, nc=00000003, cnonce="2250ac0265250e7e" Authorization 中的 response: 7342c2fe0ab2366d89940f3f1d204de7 期望的 response: 7342c2fe0ab2366d89940f3f1d204de7 old nc: 2, new nc: 3

    6、注意事项    <-- 返回目录

      1)nonce 由后台生成传给浏览器的,浏览器会在 Authorization 请求头中带回;

      2)Authorization 请求头中nc的含义:nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量,用来防重复攻击;

      3)生成response的算法:response = MD5(MD5(username:realm:password):nonce:nc:cnonce:qop:MD5(<request-method>:url))

        - 浏览器不发送 password 的值,而是发送 response,可以防止密码在传输过程中被窃取;

    参考:

      1)《图解HTTP》

      2)HTTP BASIC认证和DIGEST认证 (案例)

      3)HTTP认证之摘要认证——Digest(一)

    ---

  • 相关阅读:
    第51天 [js] 字符串相连有哪些方式?哪种最好?为什么?
    第52天 [js] 什么是事件委托?它有什么好处?能简单的写一个例子吗?
    np.ndarray与Eigen::Matrix之间的互相转换
    C++向assert加入错误信息
    CeiT:训练更快的多层特征抽取ViT
    CoAtNet: 90.88% Paperwithcode榜单第一,层层深入考虑模型设计
    正式启用Danube 官方站点
    go 编译报错 package embed is not in GOROOT (/usr/local/go/src/embed)
    cloudreve兼容acme.sh脚本
    Git的交叉编译
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/12609387.html
Copyright © 2020-2023  润新知