在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
封裝規範。
- 首先封裝常量
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]
特性,否則前面做的一切都是徒勞+_+