CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
HTTP认证之基本认证——Basic
0
.NetCore
小笨蛋
发布于:2023年01月08日
更新于:2023年01月15日
129
#custom-toc-container
### 一、概述 > Basic认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全。 ### 二、剖析 1.当打开需要认证的页面时,会弹出一个对话框,要求输入用户名和密码 ![图片alt](/uploads/images/20230108/211354-5253da8755d84c15b5e6be5da8e0ef2a.png '代码片段:Www.CodeSnippet.Cn') 2.使用[Fiddler](https://www.telerik.com/download/fiddler "Fiddler")监听请求,可以看到在未进行认证或认证失败的情况下,服务端会返回401 Unauthorized给客户端,并附带Challenge(质询),即在Response Header中添加WWW-Authenticate标头,浏览器识别到Basic后弹出对话框 Realm表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码,用" "包括起来(截图中没有,但最好加上)。 [![](/uploads/images/20230108/211523-afaef6da56564154bf5d82bf0fd99136.jpg)](https://www.codesnippet.cn) 3.输入正确的用户名和密码,认证成功后,浏览器会将凭据信息缓存起来,那么以后再进入时,无需重复手动输入用户名和密码。 查看HTTP请求,可以看到Request Header中添加了Authorization标头,格式为:Authorization:
- 类型为“Basic” - 凭证为“MTIzOjEyMw==”,是通过将“用户名:密码”格式的字符串经过的Base64编码得到的。而Base64不属于加密范畴,可以被逆向解码,等同于明文,因此Basic传输认证信息是不安全的 [![](/uploads/images/20230108/211622-46e83d63eaef4d9398498bcdee1e6f8b.jpg)](https://www.codesnippet.cn) ### 三、缺陷 1.用户名和密码明文(Base64)传输,需要配合HTTPS来保证信息传输的安全。 2.即使密码被强加密,第三方仍可通过加密后的用户名和密码进行重放攻击。 3.没有提供任何针对代理和中间节点的防护措施。 4.假冒服务器很容易骗过认证,诱导用户输入用户名和密码。 以上介绍了Basic认证的工作原理和流程,接下来就赶紧通过代码来实践一下,以下教程基于ASP.NET Core WebApi框架。如有兴趣,[可查看源码](https://gitee.com/homezzm/DotNetStackLibraries/tree/master/DotNetStackLibraries/AspNetCore.Authentication.Basic "可查看源码") ### 四、准备工作 在开始之前,先把最基本的用户名密码校验逻辑准备好,只有一个认证方法: ```csharp public class UserService { public static User Authenticate(string userName, string password) { //用户名、密码不为空且相等时认证成功 if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password) && userName == password) { return new User() { UserName = userName, Password = password }; } return null; } } public class User { public string UserName { get; set; } public string Password { get; set; } } ``` ### 五、编码 1.首先,先确定使用的认证方案为Basic,并提供默认的的Realm, ```csharp public const string AuthenticationScheme = "Basic"; public const string AuthenticationRealm = "Test Realm"; ``` 2.然后,解析HTTP Request获取到Authorization标头 ```csharp private string GetCredentials(HttpRequest request) { string credentials = null; string authorization = request.Headers[HeaderNames.Authorization]; //请求中存在 Authorization 标头且认证方式为 Basic if (authorization?.StartsWith(AuthenticationScheme, StringComparison.OrdinalIgnoreCase) == true) { credentials = authorization.Substring(AuthenticationScheme.Length).Trim(); } return credentials; } ``` 3.接着通过Base64逆向解码,得到要认证的用户名和密码。如果认证失败,则返回401 Unauthorized(不推荐返回403 Forbidden,因为这会导致用户在不刷新页面的情况下无法重新尝试认证);如果认证成功,继续处理请求。 ```csharp public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { //请求允许匿名访问 if (context.Filters.Any(item => item is IAllowAnonymousFilter)) return; var credentials = GetCredentials(context.HttpContext.Request); //已获取到凭证 if(credentials != null) { try { //Base64逆向解码得到用户名和密码 credentials = Encoding.UTF8.GetString(Convert.FromBase64String(credentials)); var data = credentials.Split(':'); if (data.Length == 2) { var userName = data[0]; var password = data[1]; var user = UserService.Authenticate(userName, password); //认证成功 if (user != null) return; } } catch { } } //认证失败返回401 context.Result = new UnauthorizedResult(); //添加质询 AddChallenge(context.HttpContext.Response); } private void AddChallenge(HttpResponse response) => response.Headers.Append(HeaderNames.WWWAuthenticate, $"{ AuthenticationScheme } realm=\"{ AuthenticationRealm }\""); } ``` 4.最后,在需要认证的Action上加上过滤器[AuthorizationFilter],大功告成!自己测试一下吧 ### 六、封装为中间件 [ASP.NET Core](https://docs.microsoft.com/zh-cn/aspnet/?view=aspnetcore-2.2#pivot=core "ASP.NET Core")相比[ASP.NET](https://docs.microsoft.com/zh-cn/aspnet/?view=aspnetcore-2.2#pivot=aspnet "ASP.NET")最大的突破大概就是插件配置化了——通过将各个功能封装成中间件,应用AOP的设计思想配置到应用程序中。以下封装采用[Jwt Bearer](https://github.com/aspnet/AspNetCore/tree/master/src/Security/Authentication/JwtBearer/ "Jwt Bearer")封装规范(.Net Core 2.2 类库)。 > Nuget: Microsoft.AspNetCore.Authentication 1. 首先封装常量 ```csharp public static class BasicDefaults { public const string AuthenticationScheme = "Basic"; } ``` 2. 然后封装Basic认证的Options,包括Realm和事件,继承自Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions。在事件内部,我们定义了认证行为和质询行为,分别用来校验认证是否通过和在HTTP Response中添加质询信息。我们将认证逻辑封装成一个委托,与认证行为独立开来,方便用户使用委托自定义认证规则。 ```csharp public class BasicOptions : AuthenticationSchemeOptions { public string Realm { get; set; } public new BasicEvents Events { get => (BasicEvents)base.Events; set => base.Events = value; } } public class BasicEvents { public Func
OnValidateCredentials { get; set; } = context => Task.CompletedTask; public Func
OnChallenge { get; set; } = context => Task.CompletedTask; public virtual Task ValidateCredentials(ValidateCredentialsContext context) => OnValidateCredentials(context); public virtual Task Challenge(BasicChallengeContext context) => OnChallenge(context); } ///
/// 封装认证参数信息上下文 ///
public class ValidateCredentialsContext : ResultContext
{ public ValidateCredentialsContext(HttpContext context, AuthenticationScheme scheme, BasicAuthenticationOptions options) : base(context, scheme, options) { } public string UserName { get; set; } public string Password { get; set; } } public class BasicChallengeContext : PropertiesContext
{ public BasicChallengeContext( HttpContext context, AuthenticationScheme scheme, BasicOptions options, AuthenticationProperties properties) : base(context, scheme, options, properties) { } ///
/// 在认证期间出现的异常 ///
public Exception AuthenticateFailure { get; set; } ///
/// 指定是否已被处理,如果已处理,则跳过默认认证逻辑 ///
public bool Handled { get; private set; } ///
/// 跳过默认认证逻辑 ///
public void HandleResponse() => Handled = true; } ``` 3. 接下来,就是对认证过程处理的封装了,需要继承自Microsoft.AspNetCore.Authentication.AuthenticationHandler ```csharp public class BasicHandler : AuthenticationHandler
{ public BasicHandler(IOptionsMonitor
options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected new BasicEvents Events { get => (BasicEvents)base.Events; set => base.Events = value; } ///
/// 确保创建的 Event 类型是 BasicEvents ///
///
protected override Task
CreateEventsAsync() => Task.FromResult
(new BasicEvents()); protected override async Task
HandleAuthenticateAsync() { var credentials = GetCredentials(Request); if(credentials == null) { return AuthenticateResult.NoResult(); } try { credentials = Encoding.UTF8.GetString(Convert.FromBase64String(credentials)); var data = credentials.Split(':'); if(data.Length != 2) { return AuthenticateResult.Fail("Invalid credentials, error format."); } var validateCredentialsContext = new ValidateCredentialsContext(Context, Scheme, Options) { UserName = data[0], Password = data[1] }; await Events.ValidateCredentials(validateCredentialsContext); //认证通过 if(validateCredentialsContext.Result?.Succeeded == true) { var ticket = new AuthenticationTicket(validateCredentialsContext.Principal, Scheme.Name); return AuthenticateResult.Success(ticket); } return AuthenticateResult.NoResult(); } catch(FormatException) { return AuthenticateResult.Fail("Invalid credentials, error format."); } catch(Exception ex) { return AuthenticateResult.Fail(ex.Message); } } protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { var authResult = await HandleAuthenticateOnceSafeAsync(); var challengeContext = new BasicChallengeContext(Context, Scheme, Options, properties) { AuthenticateFailure = authResult?.Failure }; await Events.Challenge(challengeContext); //质询已处理 if (challengeContext.Handled) return; var challengeValue = $"{ BasicDefaults.AuthenticationScheme } realm=\"{ Options.Realm }\""; var error = challengeContext.AuthenticateFailure?.Message; if(!string.IsNullOrWhiteSpace(error)) { //将错误信息封装到内部 challengeValue += $" error=\"{ error }\""; } Response.StatusCode = (int)HttpStatusCode.Unauthorized; Response.Headers.Append(HeaderNames.WWWAuthenticate, challengeValue); } private string GetCredentials(HttpRequest request) { string credentials = null; string authorization = request.Headers[HeaderNames.Authorization]; //存在 Authorization 标头 if (authorization != null) { var scheme = BasicDefaults.AuthenticationScheme; if (authorization.StartsWith(scheme, StringComparison.OrdinalIgnoreCase)) { credentials = authorization.Substring(scheme.Length).Trim(); } } return credentials; } } ``` 4. 最后,就是要把封装的接口暴露给用户了,这里使用扩展方法的形式,虽然有4个方法,但实际上都是重载,是同一种行为。 ```csharp public static class BasicExtensions { public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder) => builder.AddBasic(BasicDefaults.AuthenticationScheme, _ => { }); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action
configureOptions) => builder.AddBasic(BasicDefaults.AuthenticationScheme, configureOptions); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action
configureOptions) => builder.AddBasic(authenticationScheme, displayName: null, configureOptions: configureOptions); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action
configureOptions) => builder.AddScheme
(authenticationScheme, displayName, configureOptions); } ``` 5. Basic认证库已经封装好了,我们创建一个ASP.NET Core WebApi程序来测试一下吧。 ```csharp //在 ConfigureServices 中配置认证中间件 public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(BasicDefaults.AuthenticationScheme) .AddBasic(options => { options.Realm = "Test Realm"; options.Events = new BasicEvents { OnValidateCredentials = context => { var user = UserService.Authenticate(context.UserName, context.Password); if (user != null) { //将用户信息封装到HttpContext var claim = new Claim(ClaimTypes.Name, context.UserName); var identity = new ClaimsIdentity(BasicDefaults.AuthenticationScheme); identity.AddClaim(claim); context.Principal = new ClaimsPrincipal(identity); context.Success(); } return Task.CompletedTask; } }; }); } //在 Configure 中启用认证中间件 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication(); } ``` 对了,一定要记得为需要认证的Action添加[Authorize]特性,否则前面做的一切都是徒劳+_+ [查看源码](https://gitee.com/homezzm/DotNetStackLibraries/tree/master/DotNetStackLibraries/AspNetCore.Authentication.Basic "查看源码")
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024