导航

HTTP认证之基本认证——Basic(一)中介绍了Basic认证的工作原理和流程,接下来就赶紧通过代码来实践一下,以下教程基于ASP.NET Core WebApi框架。如有兴趣,可查看源码

一、准备工作

在开始之前,先把最基本的用户名密码校验逻辑准备好,只有一个认证方法:

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

public const string AuthenticationScheme = "Basic";
public const string AuthenticationRealm = "Test Realm";

2.然后,解析HTTP Request获取到Authorization标头

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,因为这会导致用户在不刷新页面的情况下无法重新尝试认证);如果认证成功,继续处理请求。

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相比ASP.NET最大的突破大概就是插件配置化了——通过将各个功能封装成中间件,应用AOP的设计思想配置到应用程序中。以下封装采用Jwt Bearer封装规范(.Net Core 2.2 类库)。

Nuget: Microsoft.AspNetCore.Authentication

  1. 首先封装常量
public static class BasicDefaults
{
public const string AuthenticationScheme = "Basic";
}

2.然后封装Basic认证的Options,包括Realm和事件,继承自Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions。在事件内部,我们定义了认证行为和质询行为,分别用来校验认证是否通过和在HTTP Response中添加质询信息。我们将认证逻辑封装成一个委托,与认证行为独立开来,方便用户使用委托自定义认证规则。

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<ValidateCredentialsContext, Task> OnValidateCredentials { get; set; } = context => Task.CompletedTask; public Func<BasicChallengeContext, Task> OnChallenge { get; set; } = context => Task.CompletedTask; public virtual Task ValidateCredentials(ValidateCredentialsContext context) => OnValidateCredentials(context); public virtual Task Challenge(BasicChallengeContext context) => OnChallenge(context);
} /// <summary>
/// 封装认证参数信息上下文
/// </summary>
public class ValidateCredentialsContext : ResultContext<BasicAuthenticationOptions>
{
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<BasicOptions>
{
public BasicChallengeContext(
HttpContext context,
AuthenticationScheme scheme,
BasicOptions options,
AuthenticationProperties properties)
: base(context, scheme, options, properties)
{
} /// <summary>
/// 在认证期间出现的异常
/// </summary>
public Exception AuthenticateFailure { get; set; } /// <summary>
/// 指定是否已被处理,如果已处理,则跳过默认认证逻辑
/// </summary>
public bool Handled { get; private set; } /// <summary>
/// 跳过默认认证逻辑
/// </summary>
public void HandleResponse() => Handled = true;
}

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

public class BasicHandler : AuthenticationHandler<BasicOptions>
{
public BasicHandler(IOptionsMonitor<BasicOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
} protected new BasicEvents Events
{
get => (BasicEvents)base.Events;
set => base.Events = value;
} /// <summary>
/// 确保创建的 Event 类型是 BasicEvents
/// </summary>
/// <returns></returns>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new BasicEvents()); protected override async Task<AuthenticateResult> 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个方法,但实际上都是重载,是同一种行为。

public static class BasicExtensions
{
public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder)
=> builder.AddBasic(BasicDefaults.AuthenticationScheme, _ => { }); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, Action<BasicOptions> configureOptions)
=> builder.AddBasic(BasicDefaults.AuthenticationScheme, configureOptions); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, Action<BasicOptions> configureOptions)
=> builder.AddBasic(authenticationScheme, displayName: null, configureOptions: configureOptions); public static AuthenticationBuilder AddBasic(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<BasicOptions> configureOptions)
=> builder.AddScheme<BasicOptions, BasicHandler>(authenticationScheme, displayName, configureOptions);
}

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

//在 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]特性,否则前面做的一切都是徒劳+_+

查看源码

HTTP认证之基本认证——Basic(二)的更多相关文章

  1. Atitit HTTP 认证机制基本验证 (Basic Authentication) 和摘要验证 (Digest Authentication)attilax总结

    Atitit HTTP认证机制基本验证 (Basic Authentication) 和摘要验证 (Digest Authentication)attilax总结 1.1. 最广泛使用的是基本验证 ( ...

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

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

  3. HTTP认证之摘要认证——Digest(二)

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

  4. aspnetcore 认证相关类简要说明二

    能过<aspnetcore 认证相关类简要说明一>我们已经了解如何将AuthenticationOptions注入到我们依赖注入系统.接下来,我们将了解一下IAuthenticationS ...

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

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

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

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

  7. java https单向认证(忽略认证)并支持http基本认证

    https单向认证(忽略认证)并支持http基本认证, 温馨提示 1,jar包要导入对 2,有匿名类编译要注意 3,欢迎提问,拿走不谢!背景知识 Https访问的相关知识中,主要分为单向验证和双向验证 ...

  8. tomcat------https单向认证和双向认证

     一.https分为单向认证和双向认证: 单向认证就是说,只有客户端使用ssl时对服务器端的证书进行认证,也就是说,客户端在请求建立之前,服务器端会向客户端发送一个证书,一般情况下,这种证书都是由自己 ...

  9. asp.net权限认证:Windows认证

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

随机推荐

  1. HTTP状态码完整版

    HTTP 状态代码的完整列表   1xx(临时响应) 用于表示临时响应并需要请求者执行操作才能继续的状态代码. 代码 说明 100(继续) 请求者应当继续提出请求.服务器返回此代码则意味着,服务器已收 ...

  2. MyBatis学习总结(三)---映射文件及引入方式

    MyBatis的强大,主要原于它强大映射功能,相对其它的jdbc,使用MyBatis,你会发现省掉很多代码.上一篇已经简单做出一个实例.今天就了解一下MyBatis的映射xml文件. 了解上一篇fri ...

  3. Java基础(Scanner、Random、流程控制语句)

    第3天 Java基础语法 今日内容介绍 u 引用数据数据类型(Scanner.Random) u 流程控制语句(if.for.while.dowhile.break.continue) 第1章 引用数 ...

  4. Kendo MVVM 数据绑定(十) Source

    Kendo MVVM 数据绑定(十) Source Source 绑定可以把 ViewModel 的值和由 Kendo 模板定义的目标元素绑定,如果 ViewModel 的值发生变化,被绑定的目标元素 ...

  5. Arduino ESP8266编程深入要点

    Arduino for ESP8266的话,如果不修改代码,默认没有办法进入轻睡眠的省电模式,只能进入Modem Sleep,也就是说Wifi可以暂时睡眠但是CPU没法睡,Modem Sleep最低功 ...

  6. JavaScript_3_输出

    1. JavaScript通常用于操作HTML元素,可以使用getElementById(id)方法. JavaScript由Web浏览器来执行. 2. document.write()仅仅向文档输出 ...

  7. URAL 2027 URCAPL, Episode 1 (模拟)

    题意:给你一个HxW的矩阵,每个点是一个指令,根据指令进行一系列操作. 题解:模拟 #include<cstdio> #include<algorithm> using nam ...

  8. Android(java)学习笔记131:关于构造代码块,构造函数的一道面试题(华为面试题)

    1. 代码实例: package text; public class TestStaticCon { public static int a = 0; static { a = 10; System ...

  9. [神经网络]一步一步使用Mobile-Net完成视觉识别(三)

    1.环境配置 2.数据集获取 3.训练集获取 4.训练 5.调用测试训练结果 6.代码讲解 本文是第三篇,获取tfboard训练集. 前面我们拿到了所有图片对应的标注信息的xml文件,现在我们需要先把 ...

  10. 苹果市值破万亿,iPhone 会涨价吗?

    今日导读 苹果教父乔布斯曾经说过:“活着就是为了改变世界.”虽然他在 56 岁时就遗憾离世,但他极具创新和变革的精神早已深埋进苹果公司的企业文化里,影响着一代又一代的人.就在最近,这家一直努力“改变世 ...