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(一)

---


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM