本文总阅读量:  次 | 文章总字数: 1,227 字

MVC Web API(三):安全验证之使用摘要认证

在前一篇文章中,主要讨论了使用 HTTP 基本认证的方法,因为 HTTP 基本认证的方式决定了它在安全性方面存很大的问题,所以接下来看看另一种验证的方式:digest authentication,即摘要认证。

摘要认证原理

在基本认https://oindk07nf.qnssl.com过一些手段避免了此问题,大大增加了安全性。

下图为摘要验证的验证原理流程图。

下面大致看一下这部分的验证流程:

  1. 客户端请求 /api/employees;
  2. 服务端返回 401 未验证的状态,并且在返回的信息中包含了验证方式 Digest 和 realm的值,QOP (quality of protection) 只设置成 auth,nonce 为一串随机值,在下面的请求中会一直使用到,当过了存活期后服务端将刷新生成一个新的 nonce 值;
  3. 客户端接受到请求返回后,将 username:realm:password 进行 HASH 运算,假设运算后的值为 HA1 。又将请求的路径 /api/employees 进行 HASH 运算,假设运算后的值为 HA2。再将HA1:nonce:nc:cnonce:qop:HA2 进行 HASH 运算,得到的值放在 response 中。这里的 cnonce 为客户端生成的 nonce 值,而 nc 用于统计,假设开始时为 00000001,下次请求后就变成了 00000002,不一定每次都加 1,但是后面请求中的 nc 值肯定大于前一次请求中的 nc 值。
  4. https://oindk07nf.qnssl.com,即第二步的状态。如果没有过期,那么比较 nc 值,如果比前一次 nc 值小或者前一次根本没有存储的 nc 值,那么也将直接返回 401 状态。如果前面的验证都通过,那么服务端也将按照步骤 3 中计算最终HASH值的步骤计算出 HASH 值与客户端的进行比较,然后比较客户端提交过来的 HASH 值与服务端计算出来的 HASH 进行比较,不匹配返回 401,匹配获取请求的数据并返回状态 200。
    摘要验证主要就是通过上面的 HASH 比较的步骤避免掉了基本验证中的安全性问题。
    需要注意的是,如果需要 IIS 支持摘要验证,需要把 IIS 摘要验证的特性勾上。

摘要验证的实现

在理解了摘要验证的原理之后,只需要用代码实现即可。
判断 nonce 是否过期的方法。

public static bool IsValid(string nonce, string nonceCount)
        {
            Tuple cachedNonce = null;
            nonces.TryGetValue(nonce, out cachedNonce);

            if (cachedNonce != null) // nonce is found
            {
                // nonce count is greater than the one in record
                if (Int32.Parse(nonceCount) > cachedNonce.Item1)
                {
                    // nonce has not expired yet
                    if (cachedNonce.Item2 > DateTime.Now)
                    {
                        // update the dictionary to reflect the nonce count just received in this request
                        nonces[nonce] = new Tuple(Int32.Parse(nonceCount),
                                                                                                            cachedNonce.Item2);

                        // Every thing looks ok - server nonce is fresh and nonce count seems to be 
                        // incremented. Does not look like replay.
                        return true;
                    }
                }
            }

            return false;
        }

下面为摘要验证实现的核心方法

namespace DigestAuthentication
{
    public class AuthenticationHandler : DelegatingHandler
    {
        protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                var headers = request.Headers;
                if (headers.Authorization != null)
                {
                    Header header = new Header(request.Headers.Authorization.Parameter,
                                                                                                                      request.Method.Method);

                    if (Nonce.IsValid(header.Nonce, header.NounceCounter))
                    {
                        // Just assuming password is same as username for the purpose of illustration
                        string password = header.UserName;

                        string ha1 = String.Format("{0}:{1}:{2}", header.UserName, header.Realm,
                                                                                                                             password).ToMD5Hash();

                        string ha2 = String.Format("{0}:{1}", header.Method, header.Uri).ToMD5Hash();

                        string computedResponse = String
                                      .Format("{0}:{1}:{2}:{3}:{4}:{5}",
                                            ha1, header.Nonce, header.NounceCounter,
                                                                                         header.Cnonce, "auth", ha2).ToMD5Hash();

                        if (String.CompareOrdinal(header.Response, computedResponse) == 0)
                        {
                            // digest computed matches the value sent by client in the response field.
                            // Looks like an authentic client! Create a principal.
                            var claims = new List
                            {
                                            new Claim(ClaimTypes.Name, header.UserName),
                                            new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password)
                            };

                            var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") });

                            Thread.CurrentPrincipal = principal;

                            if (HttpContext.Current != null)
                                HttpContext.Current.User = principal;
                        }
                    }
                }

                var response = await base.SendAsync(request, cancellationToken);

                if (response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",
                                                                                     Header.UnauthorizedResponseHeader.ToString()));
                }

                return response;
            }
            catch (Exception)
            {
                var response = request.CreateResponse(HttpStatusCode.Unauthorized);
                response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",
                                                                                       Header.UnauthorizedResponseHeader.ToString()));

                return response;
            }
        }
    }

}

实现完成后,使用摘要验证只需要在对应的方法加上 [Authorize] 属性标签即可。

摘要验证的优缺点

摘要验证很好地解决了使用基本验证所担心的安全性问题。
但是永远没有绝对的安全,当用户使用字典进行穷举破解时,还是会存在一些被破解的隐患。

源码下载

编辑器里怎么找不到上传文件的地方了?我上传到了百度网盘里。
源代码下载

EOF

转载须以超链接形式标明文章原始出处和作者信息