导航

HTTP认证之摘要认证——Digest(一)中介绍了Digest认证的工作原理和流程,接下来就赶紧通过代码来实践一下,以下教程使用默认的MD5摘要算法、auth策略,基于ASP.NET Core WebApi框架。如有兴趣,可查看源码

一、准备工作

在开始之前,先把最基本的业务逻辑准备好,只有一个根据用户名获取密码的方法:

public class UserService
{
public static string GetPassword(string userName) => userName;
}

还有MD5加密的一些扩展方法

public static class MD5HashExtensions
{
public static string ToMD5Hash(this string input) => MD5Helper.Encrypt(input);
} public class MD5Helper
{
public static string Encrypt(string plainText) => Encrypt(plainText, Encoding.UTF8); public static string Encrypt(string plainText, Encoding encoding)
{
var bytes = encoding.GetBytes(plainText);
return Encrypt(bytes);
} public static string Encrypt(byte[] bytes)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(bytes);
return FromHash(hash);
}
} private static string FromHash(byte[] hash)
{
var sb = new StringBuilder();
foreach (var t in hash)
{
sb.Append(t.ToString("x2"));
} return sb.ToString();
}
}

二、编码

以下代码书写在自定义授权过滤器中,继承自Attribute, IAuthorizationFilter

1.首先,先确定使用的认证方案为Digest,并指定Realm,设置Qop的策略为auth,这里我们采用的预处理方式为在一定时间段内可以重用nonce,指定过期时间为10s

public const string AuthenticationScheme = "Digest";
public const string AuthenticationRealm = "http://localhost:32435";
public const string Qop = "auth";
//设置 nonce 过期时间为10s
public const int MaxNonceAgeSeconds = 10;

2.接着,我们再把常用的常量封装一下

public static class AuthenticateHeaderNames
{
public const string UserName = "username";
public const string Realm = "realm";
public const string Nonce = "nonce";
public const string ClientNonce = "cnonce";
public const string NonceCounter = "nc";
public const string Qop = "qop";
public const string Response = "response";
public const string Uri = "uri";
public const string RspAuth = "rspauth";
public const string Stale = "stale";
} public static class QopValues
{
public const string Auth = "auth";
public const string AuthInt = "auth-int";
}

3.在没有进行认证或认证失败时,服务端需要返回401 Unauthorized,并对客户端发出质询,一下是质询需要包含的内容(“stale”参数指示nonce是否过期)

private void AddChallenge(HttpResponse response, bool stale)
{
var partList = new List<ValueTuple<string, string, bool>>()
{
(AuthenticateHeaderNames.Realm, AuthenticationRealm, true),
(AuthenticateHeaderNames.Qop, Qop, true),
(AuthenticateHeaderNames.Nonce, GetNonce(), true),
}; var value = $"{AuthenticationScheme} {string.Join(", ", partList.Select(part => FormatHeaderPart(part)))}";
if (stale)
{
value += $", {FormatHeaderPart((AuthenticateHeaderNames.Stale, "true", false))}";
}
response.Headers.Append(HeaderNames.WWWAuthenticate, value);
} private string GetNonce(DateTimeOffset? timestamp = null)
{
var privateKey = "test private key";
var timestampStr = timestamp?.ToString() ?? DateTimeOffset.UtcNow.ToString();
return Convert.ToBase64String(_encoding.GetBytes($"{ timestampStr } {$"{timestampStr} : {privateKey}".ToMD5Hash()}"));
} private string FormatHeaderPart((string Name, string Value, bool ShouldQuote) part)
=> part.ShouldQuote ? $"{part.Name}=\"{part.Value}\"" : $"{part.Name}={part.Value}";

4.客户端请求认证后,服务端需要使用HTTP Request中Authorization标头的参数进行摘要计算,所以我们需要将这些参数解析出来并封装成一个类对象AuthorizationHeader

private AuthorizationHeader GetAuthenticationHeader(HttpRequest request)
{
try
{
var credentials = GetCredentials(request);
if (credentials != null)
{
var authorizationHeader = new AuthorizationHeader()
{
RequestMethod = request.Method,
};
var nameValueStrs = credentials.Replace("\"", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim());
foreach (var nameValueStr in nameValueStrs)
{
var index = nameValueStr.IndexOf('=');
var name = nameValueStr.Substring(0, index);
var value = nameValueStr.Substring(index + 1); switch (name)
{
case AuthenticateHeaderNames.UserName:
authorizationHeader.UserName = value;
break;
case AuthenticateHeaderNames.Realm:
authorizationHeader.Realm = value;
break;
case AuthenticateHeaderNames.Nonce:
authorizationHeader.Nonce = value;
break;
case AuthenticateHeaderNames.ClientNonce:
authorizationHeader.ClientNonce = value;
break;
case AuthenticateHeaderNames.NonceCounter:
authorizationHeader.NonceCounter = value;
break;
case AuthenticateHeaderNames.Qop:
authorizationHeader.Qop = value;
break;
case AuthenticateHeaderNames.Response:
authorizationHeader.Response = value;
break;
case AuthenticateHeaderNames.Uri:
authorizationHeader.Uri = value;
break;
}
} return authorizationHeader;
}
}
catch { } return null;
} private string GetCredentials(HttpRequest request)
{
string credentials = null; string authorization = request.Headers[HeaderNames.Authorization];
//请求中存在 Authorization 标头且认证方式为 Digest
if (authorization?.StartsWith(AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == true)
{
credentials = authorization.Substring(AuthenticationScheme.Length).Trim();
} return credentials;
} public class AuthorizationHeader
{
public string UserName { get; set; }
public string Realm { get; set; }
public string Nonce { get; set; }
public string ClientNonce { get; set; }
public string NonceCounter { get; set; }
public string Qop { get; set; }
public string Response { get; set; }
public string RequestMethod { get; set; }
public string Uri { get; set; }
}

5.进行摘要计算的参数信息已经齐备了,不过别着急,先来校验一下nonce的有效性。

/// <summary>
/// 验证Nonce是否有效
/// </summary>
/// <param name="nonce"></param>
/// <returns>true:验证通过;false:验证失败;null:随机数过期</returns>
private bool? ValidateNonce(string nonce)
{
try
{
var plainNonce = _encoding.GetString(Convert.FromBase64String(nonce));
var timestamp = DateTimeOffset.Parse(plainNonce.Substring(0, plainNonce.LastIndexOf(' ')));
//验证Nonce是否被篡改
var isValid = nonce == GetNonce(timestamp); //验证是否过期
if (Math.Abs((timestamp - DateTimeOffset.UtcNow).TotalSeconds) > MaxNonceAgeSeconds)
{
return isValid ? (bool?)null : false;
} return isValid;
}
catch
{
return false;
}
}

6.好,接下来就来进行摘要计算吧,其实就是套用公式,如果不记得了,可以重温一下第一节。

private static string GetComputedResponse(AuthorizationHeader authorizationHeader, string password)
{
var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash();
var a2Hash = $"{authorizationHeader.RequestMethod}:{authorizationHeader.Uri}".ToMD5Hash();
return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash();
}

7.如果认证通过,我们通过Authorization-Info返回一些授权会话的信息。

private void AddAuthorizationInfo(HttpResponse response, AuthorizationHeader authorizationHeader, string password)
{
var partList = new List<ValueTuple<string, string, bool>>()
{
(AuthenticateHeaderNames.Qop, authorizationHeader.Qop, true),
(AuthenticateHeaderNames.RspAuth, GetRspAuth(authorizationHeader, password), true),
(AuthenticateHeaderNames.ClientNonce, authorizationHeader.ClientNonce, true),
(AuthenticateHeaderNames.NonceCounter, authorizationHeader.NonceCounter, false)
};
response.Headers.Append("Authorization-Info", string.Join(", ", partList.Select(part => FormatHeaderPart(part))));
} private string GetRspAuth(AuthorizationHeader authorizationHeader, string password)
{
var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash();
var a2Hash = $":{authorizationHeader.Uri}".ToMD5Hash();
return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash();
}

8.我们把整个认证流程整理一下

public void OnAuthorization(AuthorizationFilterContext context)
{
//请求允许匿名访问
if (context.Filters.Any(item => item is IAllowAnonymousFilter)) return; var authorizationHeader = GetAuthenticationHeader(context.HttpContext.Request);
var stale = false;
if(authorizationHeader != null)
{
var isValid = ValidateNonce(authorizationHeader.Nonce);
//随机数过期
if(isValid == null)
{
stale = true;
}
else if(isValid == true)
{
var password = UserService.GetPassword(authorizationHeader.UserName);
string computedResponse = null;
switch (authorizationHeader.Qop)
{
case QopValues.Auth:
computedResponse = GetComputedResponse(authorizationHeader, password);
break;
default:
context.Result = new BadRequestObjectResult($"qop指定策略必须为\"{QopValues.Auth}\"");
break;
} if (computedResponse == authorizationHeader.Response)
{
AddAuthorizationInfo(context.HttpContext.Response, authorizationHeader, password);
return;
}
}
} context.Result = new UnauthorizedResult();
AddChallenge(context.HttpContext.Response, stale);
}

9.最后,在需要认证的Action上加上自定义过滤器特性,大功告成!自己测试一下吧!

三、封装为中间件

照例,接下来我们将摘要认证封装为ASP.NET Core中间件,便于使用和扩展。以下封装采用Jwt Bearer封装规范。以下代码较长,推荐直接去看源码。

  1. 首先封装常量(之前提到过的就不说了)
public static class DigestDefaults
{
public const string AuthenticationScheme = "Digest";
}

2.然后封装Basic认证的Options,包括Realm、Qop、Private key和事件,继承自Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions。在事件内部,我们定义了获取密码行为和质询行为,分别用来根据用户名获取密码和在HTTP Response中添加质询信息。要注意的是,获取密码行为要求必须由用户实现,毕竟我们内部是不知道密码的。

public class DigestOptions : AuthenticationSchemeOptions
{
public const string DefaultQop = QopValues.Auth;
public const int DefaultMaxNonceAgeSeconds = 10; public string Realm { get; set; }
public string Qop { get; set; } = DefaultQop;
public int MaxNonceAgeSeconds { get; set; } = DefaultMaxNonceAgeSeconds;
public string PrivateKey { get; set; } public new DigestEvents Events
{
get => (DigestEvents)base.Events;
set => base.Events = value;
}
} public class DigestEvents
{
public DigestEvents(Func<GetPasswordContext, Task<string>> onGetPassword)
{
OnGetPassword = onGetPassword;
} public Func<GetPasswordContext, Task<string>> OnGetPassword { get; set; } = context => throw new NotImplementedException($"{nameof(OnGetPassword)} must be implemented!"); public Func<DigestChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask; public virtual Task<string> GetPassword(GetPasswordContext context) => OnGetPassword(context); public virtual Task Challenge(DigestChallengeContext context) => OnChallenge(context);
} public class GetPasswordContext : ResultContext<DigestOptions>
{
public GetPasswordContext(
HttpContext context,
AuthenticationScheme scheme,
DigestOptions options)
: base(context, scheme, options)
{
} public string UserName { get; set; }
} public class DigestChallengeContext : PropertiesContext<DigestOptions>
{
public DigestChallengeContext(
HttpContext context,
AuthenticationScheme scheme,
DigestOptions options,
AuthenticationProperties properties)
: base(context, scheme, options, properties)
{
} /// <summary>
/// 在认证期间出现的异常
/// </summary>
public Exception AuthenticateFailure { get; set; } public bool Stale { get; set; } /// <summary>
/// 指定是否已被处理,如果已处理,则跳过默认认证逻辑
/// </summary>
public bool Handled { get; private set; } /// <summary>
/// 跳过默认认证逻辑
/// </summary>
public void HandleResponse() => Handled = true;
}

3.接下来,就是对认证过程处理的封装了,需要继承自Microsoft.AspNetCore.Authentication.AuthenticationHandler

public class DigestHandler : AuthenticationHandler<DigestOptions>
{
private static readonly Encoding _encoding = Encoding.UTF8; public DigestHandler(
IOptionsMonitor<DigestOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
} protected new DigestEvents Events
{
get => (DigestEvents)base.Events;
set => base.Events = value;
} /// <summary>
/// 确保创建的 Event 类型是 DigestEvents
/// </summary>
/// <returns></returns>
protected override Task<object> CreateEventsAsync() => throw new NotImplementedException($"{nameof(Events)} must be created"); protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authorizationHeader = GetAuthenticationHeader(Context.Request);
if (authorizationHeader == null)
{
return AuthenticateResult.NoResult();
} try
{
var isValid = ValidateNonce(authorizationHeader.Nonce);
//随机数过期
if (isValid == null)
{
var properties = new AuthenticationProperties();
properties.SetParameter(AuthenticationHeaderNames.Stale, true);
return AuthenticateResult.Fail(string.Empty, properties);
}
else if (isValid == true)
{
var getPasswordContext = new GetPasswordContext(Context, Scheme, Options)
{
UserName = authorizationHeader.UserName
};
var password = await Events.GetPassword(getPasswordContext);
string computedResponse = null;
switch (authorizationHeader.Qop)
{
case QopValues.Auth:
computedResponse = GetComputedResponse(authorizationHeader, password);
break;
default:
return AuthenticateResult.Fail($"qop指定策略必须为\"{QopValues.Auth}\"");
} if (computedResponse == authorizationHeader.Response)
{
var claim = new Claim(ClaimTypes.Name, getPasswordContext.UserName);
var identity = new ClaimsIdentity(DigestDefaults.AuthenticationScheme);
identity.AddClaim(claim); var ticket = new AuthenticationTicket(new ClaimsPrincipal(identity), Scheme.Name);
AddAuthorizationInfo(Context.Response, authorizationHeader, password);
return AuthenticateResult.Success(ticket);
}
} return AuthenticateResult.NoResult();
}
catch (Exception ex)
{
return AuthenticateResult.Fail(ex.Message);
} } protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var authResult = await HandleAuthenticateOnceSafeAsync();
var challengeContext = new DigestChallengeContext(Context, Scheme, Options, properties)
{
AuthenticateFailure = authResult.Failure,
Stale = authResult.Properties?.GetParameter<bool>(AuthenticationHeaderNames.Stale) ?? false
};
await Events.Challenge(challengeContext);
//质询已处理
if (challengeContext.Handled) return; var challengeValue = GetChallengeValue(challengeContext.Stale);
var error = challengeContext.AuthenticateFailure?.Message;
if (!string.IsNullOrWhiteSpace(error))
{
//将错误信息封装到内部
challengeValue += $", error=\"{ error }\"";
} Response.StatusCode = (int)HttpStatusCode.Unauthorized;
Response.Headers.Append(HeaderNames.WWWAuthenticate, challengeValue);
} private AuthorizationHeader GetAuthenticationHeader(HttpRequest request)
{
try
{
var credentials = GetCredentials(request);
if (credentials != null)
{
var authorizationHeader = new AuthorizationHeader()
{
RequestMethod = request.Method,
};
var nameValueStrs = credentials.Replace("\"", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim());
foreach (var nameValueStr in nameValueStrs)
{
var index = nameValueStr.IndexOf('=');
var name = nameValueStr.Substring(0, index);
var value = nameValueStr.Substring(index + 1); switch (name)
{
case AuthenticationHeaderNames.UserName:
authorizationHeader.UserName = value;
break;
case AuthenticationHeaderNames.Realm:
authorizationHeader.Realm = value;
break;
case AuthenticationHeaderNames.Nonce:
authorizationHeader.Nonce = value;
break;
case AuthenticationHeaderNames.ClientNonce:
authorizationHeader.ClientNonce = value;
break;
case AuthenticationHeaderNames.NonceCounter:
authorizationHeader.NonceCounter = value;
break;
case AuthenticationHeaderNames.Qop:
authorizationHeader.Qop = value;
break;
case AuthenticationHeaderNames.Response:
authorizationHeader.Response = value;
break;
case AuthenticationHeaderNames.Uri:
authorizationHeader.Uri = value;
break;
}
} return authorizationHeader;
}
}
catch { } return null;
} private string GetCredentials(HttpRequest request)
{
string credentials = null; string authorization = request.Headers[HeaderNames.Authorization];
//请求中存在 Authorization 标头且认证方式为 Digest
if (authorization?.StartsWith(DigestDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == true)
{
credentials = authorization.Substring(DigestDefaults.AuthenticationScheme.Length).Trim();
} return credentials;
} /// <summary>
/// 验证Nonce是否有效
/// </summary>
/// <param name="nonce"></param>
/// <returns>true:验证通过;false:验证失败;null:随机数过期</returns>
private bool? ValidateNonce(string nonce)
{
try
{
var plainNonce = _encoding.GetString(Convert.FromBase64String(nonce));
var timestamp = DateTimeOffset.Parse(plainNonce.Substring(0, plainNonce.LastIndexOf(' ')));
//验证Nonce是否被篡改
var isValid = nonce == GetNonce(timestamp); //验证是否过期
if (Math.Abs((timestamp - DateTimeOffset.UtcNow).TotalSeconds) > Options.MaxNonceAgeSeconds)
{
return isValid ? (bool?)null : false;
} return isValid;
}
catch
{
return false;
}
} private static string GetComputedResponse(AuthorizationHeader authorizationHeader, string password)
{
var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash();
var a2Hash = $"{authorizationHeader.RequestMethod}:{authorizationHeader.Uri}".ToMD5Hash();
return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash();
} private void AddAuthorizationInfo(HttpResponse response, AuthorizationHeader authorizationHeader, string password)
{
var partList = new List<ValueTuple<string, string, bool>>()
{
(AuthenticationHeaderNames.Qop, authorizationHeader.Qop, true),
(AuthenticationHeaderNames.RspAuth, GetRspAuth(authorizationHeader, password), true),
(AuthenticationHeaderNames.ClientNonce, authorizationHeader.ClientNonce, true),
(AuthenticationHeaderNames.NonceCounter, authorizationHeader.NonceCounter, false)
};
response.Headers.Append("Authorization-Info", string.Join(", ", partList.Select(part => FormatHeaderPart(part))));
} private string GetChallengeValue(bool stale)
{
var partList = new List<ValueTuple<string, string, bool>>()
{
(AuthenticationHeaderNames.Realm, Options.Realm, true),
(AuthenticationHeaderNames.Qop, Options.Qop, true),
(AuthenticationHeaderNames.Nonce, GetNonce(), true),
}; var value = $"{DigestDefaults.AuthenticationScheme} {string.Join(", ", partList.Select(part => FormatHeaderPart(part)))}";
if (stale)
{
value += $", {FormatHeaderPart((AuthenticationHeaderNames.Stale, "true", false))}";
}
return value;
} private string GetRspAuth(AuthorizationHeader authorizationHeader, string password)
{
var a1Hash = $"{authorizationHeader.UserName}:{authorizationHeader.Realm}:{password}".ToMD5Hash();
var a2Hash = $":{authorizationHeader.Uri}".ToMD5Hash();
return $"{a1Hash}:{authorizationHeader.Nonce}:{authorizationHeader.NonceCounter}:{authorizationHeader.ClientNonce}:{authorizationHeader.Qop}:{a2Hash}".ToMD5Hash();
} private string GetNonce(DateTimeOffset? timestamp = null)
{
var privateKey = Options.PrivateKey;
var timestampStr = timestamp?.ToString() ?? DateTimeOffset.UtcNow.ToString();
return Convert.ToBase64String(_encoding.GetBytes($"{ timestampStr } {$"{timestampStr} : {privateKey}".ToMD5Hash()}"));
} private string FormatHeaderPart((string Name, string Value, bool ShouldQuote) part)
=> part.ShouldQuote ? $"{part.Name}=\"{part.Value}\"" : $"{part.Name}={part.Value}";

4.最后,就是要把封装的接口暴露给用户了。

public static class DigestExtensions
{
public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder)
=> builder.AddDigest(DigestDefaults.AuthenticationScheme, _ => { }); public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder, Action<DigestOptions> configureOptions)
=> builder.AddDigest(DigestDefaults.AuthenticationScheme, configureOptions); public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder, string authenticationScheme, Action<DigestOptions> configureOptions)
=> builder.AddDigest(authenticationScheme, displayName: null, configureOptions: configureOptions); public static AuthenticationBuilder AddDigest(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<DigestOptions> configureOptions)
=> builder.AddScheme<DigestOptions, DigestHandler>(authenticationScheme, displayName, configureOptions);
}

5.Digest认证库已经封装好了,我们创建一个ASP.NET Core WebApi程序来测试一下吧。

//在 ConfigureServices 中配置认证中间件
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(DigestDefaults.AuthenticationScheme)
.AddDigest(options =>
{
options.Realm = "http://localhost:44550";
options.PrivateKey = "test private key";
options.Events = new DigestEvents(context => Task.FromResult(context.UserName));
});
} //在 Configure 中启用认证中间件
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}

最后,一定要记得为需要认证的Action添加[Authorize]特性,否则前面做的一切都是徒劳+_+

查看源码

HTTP认证之摘要认证——Digest(二)的更多相关文章

  1. asp.net权限认证:摘要认证(digest authentication)

    asp.net权限认证系列 asp.net权限认证:Forms认证 asp.net权限认证:HTTP基本认证(http basic) asp.net权限认证:Windows认证 asp.net权限认证 ...

  2. HTTP认证之摘要认证——Digest(一)

    导航 HTTP认证之基本认证--Basic(一) HTTP认证之基本认证--Basic(二) HTTP认证之摘要认证--Digest(一) HTTP认证之摘要认证--Digest(二) 一.概述 Di ...

  3. [转]asp.net权限认证:摘要认证(digest authentication)

    本文转自:http://www.cnblogs.com/lanxiaoke/p/6357501.html 摘要认证简单介绍 摘要认证是对基本认证的改进,即是用摘要代替账户密码,从而防止明文传输中账户密 ...

  4. RTSP鉴权认证之基础认证和摘要认证区别

    RTSP认证类型 基本认证(basic authentication):http 1.0提出的认证方案,其消息传输不经过加密转换因此存在严重的安全隐患.: 摘要认证(digest authentica ...

  5. EasyDarwin开源流媒体服务器支持basic基本认证和digest摘要认证解析

    本文转自EasyDarwin开源团队成员ss的博客:http://blog.csdn.net/ss00_2012/article/details/52262621 RTSP认证作为RTSP标准协议的一 ...

  6. 详解HTTP中的摘要认证机制(转)

    Basic认证方式是存在很多缺陷的,具体表现如下: 1,  Basic认证会通过网络发送用户名和密码,并且是以base64的方式对用户名和密码进行简单的编码后发送的,而base64编码本身非常容易被解 ...

  7. HTTP认证之基本认证——Basic(二)

    导航 HTTP认证之基本认证--Basic(一) HTTP认证之基本认证--Basic(二) HTTP认证之摘要认证--Digest(一) HTTP认证之摘要认证--Digest(二) 在HTTP认证 ...

  8. 前端学HTTP之摘要认证

    前面的话 上一篇介绍的基本认证便捷灵活,但极不安全.用户名和密码都是以明文形式传送的,也没有采取任何措施防止对报文的篡改.安全使用基本认证的唯一方式就是将其与SSL配合使用 摘要认证与基本认证兼容,但 ...

  9. HTTP认证之基本认证——Basic(一)

    导航 HTTP认证之基本认证--Basic(一) HTTP认证之基本认证--Basic(二) HTTP认证之摘要认证--Digest(一) HTTP认证之摘要认证--Digest(二) 一.概述 Ba ...

随机推荐

  1. 动态时间规整DTW

    动态时间规整DTW 1 概述 动态时间规整是一个计算时间序列之间距离的算法,是为了解决语音识别领域中语速不同的情况下如何计算距离相似度的问题. 相对于用经典的欧式距离来计算相似度而言,DTW在数据点个 ...

  2. babel7中 corejs 和 corejs2 的区别

    babel7中 corejs 和 corejs2 的区别 最近在给项目升级 webpack4 和 babel7,有一些改变但是变化不大.具体过程可以参考这篇文章 webpack4:连奏中的进化.只是文 ...

  3. display:none和visibility:hidden v-show和v-if的区别

    隐藏元素display:none 和 visibility:hidden的区别visibility:hidden可以隐藏某个元素,但是隐藏的元素仍要占据空间,仍要影响布局display:none不会占 ...

  4. GitHub上优秀Android 开源项目

    GitHub在中国的火爆程度无需多言,越来越多的开源项目迁移到GitHub平台上.更何况,基于不要重复造轮子的原则,了解当下比较流行的Android与iOS开源项目很是必要.利用这些项目,有时能够让你 ...

  5. C#调用C++接口返回字符串的做法

    作者:朱金灿 来源:http://blog.csdn.net/clever101 现在有这样一种情景,假如C#调用C++接口需要返回一个字符串.因为字符串是不定长的,因此传递一个定长的字符串进去是不合 ...

  6. Android用RecyclerView实现的二维Excel效果组件

    excelPanel 二维RecyclerView.不仅可以加载历史数据,而且可以加载未来的数据.   包括在您的项目中 excelPanel 二维RecyclerView.不仅可以加载历史数据,而且 ...

  7. Elasticsearch-基本操作2

    Elasticsearch版本:6.0 为了避免并发修改的冲突问题,数据库中,经常用悲观锁和乐观锁来控制并发问题,而Elasticsearch使用乐观锁.如果源数据在读写过程中被修改,更新将失败,应用 ...

  8. Outlook 2016 自动发送/接收无法正常工作

    如果您的自动/发送接收由于某种原因停止工作,可能会非常令人沮丧,因为您必须记住手动执行发送/接收(F9).如果您遇到Outlook无法自动发送或接收电子邮件的问题,可以尝试以下几项操作. #1 发送/ ...

  9. Python+selenium之键盘事件

    keuys()类提供键盘上所有的按键方法.send_keys()方法可以用来模拟键盘输入. from selenium import webdriver from selenium.webdriver ...

  10. UVALive 3523 Knights of the Round Table 圆桌骑士 (无向图点双连通分量)

    由于互相憎恨的骑士不能相邻,把可以相邻的骑士连上无向边,会议要求是奇数,问题就是求不在任意一个简单奇圈上的结点个数. 如果不是二分图,一定存在一个奇圈,同一个双连通分量中其它点一定可以加入奇圈.很明显 ...