导航

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. 利用Vagrant and VirtualBox搭建core os环境

    利用Vagrant and VirtualBox搭建core os环境 系统环境 ubuntu 14.04 x64 vagrant 1.7.4 virtualbox 4.3.10 git 1.9.1 ...

  2. java类及编写public类的基础点

    1.一个java文件中只能有一个public类.且公共类名称必须与java文件名一致,否则会出现错误提示.与其他面向对象编程语言的一样,在利用java分析问题时,基本思路即为将问题的属性(静)与行为( ...

  3. AngularJS(六):表单-复选框

    本文也同步发表在我的公众号“我的天空” 复选框 复选框只有两个值:true或者false,因此在AngularJS中,一般都是将复选框的ng-model绑定为一个布尔值属性,通过这两个布尔值来决定其勾 ...

  4. vuejs 学习旅程之 vue-resource

    如上图,所有的数据是从php获取过来的.所以就引出了vuejs 与php通信之说.百度了一下需要使用到一个vue插件 就是今天的主题 vuejs 学习旅程之 vue-resource vue-reso ...

  5. 似水流年 ? Chrome调试大全

    http://www.360doc.com/content/12/1107/20/7851074_246467307.shtml   作为一名前端开发者,打交道最多的可能是和浏览器.市面上各种浏览器多 ...

  6. 洛谷 P1903 【模板】分块/带修改莫队(数颜色)

    题目描述 墨墨购买了一套N支彩色画笔(其中有些颜色可能相同),摆成一排,你需要回答墨墨的提问.墨墨会像你发布如下指令: 1. Q L R代表询问你从第L支画笔到第R支画笔中共有几种不同颜色的画笔. 2 ...

  7. 如何选择Web开发框架

    下面先来看看为什么要使用Web开发框架一 使用框架的必然性框架,即framework.其实就是某种应用的半成品,把不同应用程序中有共性的一些东西抽取出来,做成一个半成品程序,这样的半成品就是所谓的程序 ...

  8. java面试题(杨晓峰)---第五讲String、StringBuffer、StringBuilder有什么区别?

    线程 字符 操作频繁度 1 String (1)String的创建机制 由于String在java世界中使用过于频繁,java为了避免在一个系统中产生大量重复的String对象,引入了字符串常量池,其 ...

  9. 前端面试题总结(二)CSS篇

    前端面试题总结(二)CSS篇 一.link和@import的区别? link属于HTML标签,可以引入出css以外的事务,如RSS,而@import是css提供的,只能加载css文件. link会在页 ...

  10. Android(java)学习笔记136:利用谷歌API对数据库增删改查(推荐使用)

    接下来我们通过项目案例来介绍:这个利用谷歌API对数据库增删改查 1. 首先项目图: 2. 这里的布局文件activity_main.xml: <LinearLayout xmlns:andro ...