参考:https://ask.csdn.net/questions/1021067
需要注意的是第二次请求中Authorization的值qop和nc不需要带引号,其他值都需要带引号。(qop=auth, nc=00000001, cnonce=“36969806a92de5d1”)
type RequestArg struct {
Host string //http://localhost
Uri string
Method string
Header map[string]string //请求头
PostBody []byte
Username string //用户名
Password string //密码
}
// DigestAuthRequest digestAuth 请求
func DigestAuthRequest(arg *RequestArg) (bool, string) {
url := arg.Host + arg.Uri
req, err := http.NewRequest(arg.Method, url, nil)
client := &http.Client{
Timeout: time.Second * 5,
}
resp, err := client.Do(req)
if err != nil {
return false, ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
log.Printf("Recieved status code '%v' auth skipped", resp.StatusCode)
resStr, _ := ioutil.ReadAll(resp.Body)
return true, string(resStr)
}
digestParts := digestParts(resp)
digestParts["uri"] = arg.Uri
digestParts["method"] = arg.Method
digestParts["username"] = arg.Username
digestParts["password"] = arg.Password
req, err = http.NewRequest(arg.Method, url, bytes.NewBuffer(arg.PostBody))
req.Header.Set("Authorization", getDigestAuthorization(digestParts))
for k, v := range arg.Header {
req.Header.Set(k, v)
}
resp, err = client.Do(req)
if err != nil {
return false, ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, ""
}
log.Println("response body: ", string(body))
return false, string(body)
}
all, _ := ioutil.ReadAll(resp.Body)
return true, string(all)
}
// 第一次请求的响应header 获取第二次请求所需要的信息 Www-Authenticate
func digestParts(resp *http.Response) map[string]string {
result := map[string]string{}
if len(resp.Header["Www-Authenticate"]) > 0 {
wantedHeaders := []string{"nonce", "realm", "qop", "opaque"}
responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",")
for _, r := range responseHeaders {
for _, w := range wantedHeaders {
if strings.Contains(r, w) {
result[w] = strings.Split(r, `"`)[1]
}
}
}
}
return result
}
// 第二次请求的请求header Authorization 值
func getDigestAuthorization(digestParts map[string]string) string {
d := digestParts
//ha1=md5(username:realm:password)
ha1 := getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"])
//ha2=md5(method:uri)
ha2 := getMD5(d["method"] + ":" + d["uri"])
nonceCount := "00000001"
cnonce := getCnonce()
//response=md5(ha1:nonce:nc:cnonce:qop:ha2)
response := getMD5(fmt.Sprintf("%s:%s:%s:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2))
//qop和nc的值不能加引号
authorization := fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", opaque="%s", qop=%s, nc=%s, cnonce="%s"`,
d["username"], d["realm"], d["nonce"], d["uri"], response, d["opaque"], d["qop"], nonceCount, cnonce)
return authorization
}
// 字符串MD5加密
func getMD5(text string) string {
hash := md5.New()
hash.Write([]byte(text))
return hex.EncodeToString(hash.Sum(nil))
}
// 获取 cnonce
// 客户端提供的不透明带引号的字符串值,客户端和服务器都使用它来避免选定的明文攻击、提供相互身份验证以及提供一些消息完整性保护
func getCnonce() string {
b := make([]byte, 8)
io.ReadFull(rand.Reader, b)
return fmt.Sprintf("%x", b)[:16]
}
客户端第一次请求(不携带用户信息)
服务端响应:
Status:401 Unauthorized Header:[Www-Authenticate:[Digest realm=“XXXX”, qop=“auth”, nonce=“61A5642D”, opaque=“806bbbfc343f5be32c639949d87b206de7f3f74e”]]
客户端第二次请求(携带用户信息)
Header:
key:Authorization
value:Digest username=“admin”, realm=“Login to BB0B3706FD4A756398B490B522145650”, nonce=“AB6E0758”, uri=“/cgi-bin/accessControl.cgi?action=openDoor&channel=1&UserID=101”, response=“0110d20bedf4feff2389f37d003245ef”, opaque=“806bbbfc343f5be32c639949d87b206de7f3f74e”, qop=auth, nc=00000001, cnonce=“36969806a92de5d1”