接續 HTTP 基本認證,本文探討 Digest Authentication (摘要認證),解決基本認證「明文傳輸 Base64 編碼憑證」的安全缺陷。
不傳輸密碼本身,而是傳輸單向雜湊摘要,驗證流程:
WWW-Authenticate: Digest realm="...", nonce="...", qop="auth", ...。HA1 = MD5(username:realm:password)HA2 = MD5(method:digestURI)Response = MD5(HA1:nonce:nc:cnonce:qop:HA2)Authorization 標頭攜帶 username, realm, nonce, uri, response, ...。Response,一致則驗證通過。
nonce (伺服器產生隨機值)、nc (請求計數器)、cnonce (用戶端隨機值) 組合確保每次請求唯一。qop="auth-int" 可包含實體內容雜湊,防篡改。
public class DigestAuthenticationAttribute : AuthorizationFilterAttribute {
private readonly string _realm;
public DigestAuthenticationAttribute(string realm = "DefaultRealm") { _realm = realm; }
public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken) {
// 1. 檢查 Authorization 標頭
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader == null || authHeader.Scheme != "Digest") {
Challenge(actionContext);
return;
}
// 2. 解析 Digest 參數
var paramsDict = ParseDigestParameters(authHeader.Parameter);
if (!paramsDict.ContainsKey("username") || !paramsDict.ContainsKey("response") || !paramsDict.ContainsKey("nonce"))
{
Challenge(actionContext);
return;
}
// 3. 取得使用者密碼 (實際應從資料庫/Identity 讀取)
string username = paramsDict["username"];
string password = GetUserPassword(username); // TODO: 實作
if (password == null) { Challenge(actionContext); return; }
// 4. 計算預期摘要
string expectedResponse = ComputeDigestResponse(
username, _realm, password,
actionContext.Request.Method.Method,
paramsDict["uri"],
paramsDict["nonce"],
paramsDict.ContainsKey("cnonce") ? paramsDict["cnonce"] : null,
paramsDict.ContainsKey("nc") ? paramsDict["nc"] : null,
paramsDict.ContainsKey("qop") ? paramsDict["qop"] : null
);
// 5. 比對 (防時序攻擊用恆定時間比較)
if (!SecureEquals(paramsDict["response"], expectedResponse)) {
Challenge(actionContext);
return;
}
// 6. 驗證通過,設定 Principal
var principal = new GenericPrincipal(new GenericIdentity(username), null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
private void Challenge(HttpActionContext actionContext) {
var nonce = GenerateNonce();
var challenge = string.Format(
"Digest realm=\"{0}\", nonce=\"{1}\", qop=\"auth\", algorithm=MD5",
_realm, nonce);
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
actionContext.Response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest", challenge));
}
// 輔助方法:ParseDigestParameters, ComputeDigestResponse, GenerateNonce, SecureEquals, GetUserPassword
// ... 略,完整實作需處理 nonce 快取、nc 遞增檢查、時序安全比較
}
| 維度 | Basic Auth | Digest Auth |
|---|---|---|
| 密碼傳輸 | Base64 編碼 (可逆) | MD5 摘要 (單向) |
| 防重放 | 無 | Nonce + nc + cnonce |
| 中間人攻擊 | 極易受攻擊 (HTTPS 必須) | 較難,但非絕對 (需 HTTPS) |
| 實作複雜度 | 極低 | 高 (解析、雜湊、Nonce 管理) |
| 效能 | 極佳 | 稍低 (雜湊計算) |
| 瀏覽器支援 | 完美 | 完美 |
| 代理/防火牆相容 | 良好 | 可能被修改標頭 |
AddAuthentication().AddJwtBearer()、AddIdentity、AddOAuth,無需手寫濾波器。algorithm=SHA-256 或 SHA-512-256 (RFC 7616),實作 nonce 快取與過期、nc 單調遞增檢查、恆定時間比較。摘要認證透過挑戰-回應機制與單向雜湊避免密碼明文傳輸,提供基本防重放保護。但在現代架構下,HTTPS + JWT/OAuth2 為首選,Digest Auth 僅適用於無法部署 HTTPS 的舊系統維護或極特定受限環境。理解其原理有助於掌握 HTTP 認證演進脈絡。