问题
如何在ASP.NET Core Web应用程序中使用自定义成员身份验证实现基本身份验证?
注
在MVC 5中,我正在使用文章中的指示,需要在
WebConfig
中添加模块。我仍然将我的新的
MVC Core
应用程序部署在IIS
上,但这种方法似乎无效。我也不想使用IIS的内置基本身份验证支持,因为它使用Windows凭据。
问题
如何在ASP.NET Core Web应用程序中使用自定义成员身份验证实现基本身份验证?
注
在MVC 5中,我正在使用文章中的指示,需要在WebConfig
中添加模块。
我仍然将我的新的MVC Core
应用程序部署在IIS
上,但这种方法似乎无效。
我也不想使用IIS的内置基本身份验证支持,因为它使用Windows凭据。
由于基本认证中间件可能存在安全性和性能问题,ASP.NET安全性将不包括基本认证中间件。
如果您需要进行测试而需要基本认证中间件,请查看https://github.com/blowdart/idunno.Authentication
我对ASP.NET Core身份验证中间件的设计感到失望。作为一个框架,它应该简化并提高生产力,但这里不是这种情况。
无论如何,一种简单而安全的方法是基于授权过滤器,例如IAsyncAuthorizationFilter
。请注意,在MVC选择特定控制器操作并移动到过滤器处理时,授权过滤器将在其他中间件之后执行。但是在过滤器中,授权过滤器首先执行(详情)。
我本来只想评论Clay对Hector答案的评论,但不喜欢Hector的示例会抛出异常并且没有任何挑战机制,所以这里有一个可行的示例。
请记住:
在此基础上,不要相信任何有关基本身份验证的FUD。跳过像基本身份验证这样基本的东西是主观的,而不是实质性的。您可以在这里的评论中看到对此设计的失望here。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace BasicAuthFilterDemo
{
public class BasicAuthenticationFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Realm { get; set; }
public const string AuthTypeName = "Basic ";
private const string _authHeaderName = "Authorization";
public BasicAuthenticationFilterAttribute(string realm = null)
{
Realm = realm;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
try
{
var request = context?.HttpContext?.Request;
var authHeader = request.Headers.Keys.Contains(_authHeaderName) ? request.Headers[_authHeaderName].First() : null;
string encodedAuth = (authHeader != null && authHeader.StartsWith(AuthTypeName)) ? authHeader.Substring(AuthTypeName.Length).Trim() : null;
if (string.IsNullOrEmpty(encodedAuth))
{
context.Result = new BasicAuthChallengeResult(Realm);
return;
}
var (username, password) = DecodeUserIdAndPassword(encodedAuth);
// Authenticate credentials against database
var db = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));
var userManager = (UserManager<User>)context.HttpContext.RequestServices.GetService(typeof(UserManager<User>));
var founduser = await db.Users.Where(u => u.Email == username).FirstOrDefaultAsync();
if (!await userManager.CheckPasswordAsync(founduser, password))
{
// writing to the Result property aborts rest of the pipeline
// see https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.0#cancellation-and-short-circuiting
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
// Populate user: adjust claims as needed
var claims = new[] { new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, AuthTypeName) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthTypeName));
context.HttpContext.User = principal;
}
catch
{
// log and reject
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
}
private static (string userid, string password) DecodeUserIdAndPassword(string encodedAuth)
{
var userpass = Encoding.UTF8.GetString(Convert.FromBase64String(encodedAuth));
var separator = userpass.IndexOf(':');
if (separator == -1)
return (null, null);
return (userpass.Substring(0, separator), userpass.Substring(separator + 1));
}
}
}
这些是支持类
public class StatusCodeOnlyResult : ActionResult
{
protected int StatusCode;
public StatusCodeOnlyResult(int statusCode)
{
StatusCode = statusCode;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
return base.ExecuteResultAsync(context);
}
}
public class BasicAuthChallengeResult : StatusCodeOnlyResult
{
private string _realm;
public BasicAuthChallengeResult(string realm = "") : base(StatusCodes.Status401Unauthorized)
{
_realm = realm;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
context.HttpContext.Response.Headers.Add("WWW-Authenticate", $"{BasicAuthenticationFilterAttribute.AuthTypeName} Realm=\"{_realm}\"");
return base.ExecuteResultAsync(context);
}
}
ASP.NET Core 2.0对身份验证和身份的变更产生了破坏性影响。
在1.x中,身份验证提供程序是通过中间件配置的(如被接受答案的实现)。在2.0中,它基于服务。
有关详细信息,请参阅MS文档: https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
我已经为ASP.NET Core 2.0编写了一个基本的身份验证实现并发布到NuGet: https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic
static System.Text.Encoding ISO_8859_1_ENCODING = System.Text.Encoding.GetEncoding("ISO-8859-1");
public static (string, string) GetUsernameAndPasswordFromAuthorizeHeader(string authorizeHeader)
{
if (authorizeHeader == null || !authorizeHeader.Contains("Basic "))
return (null, null);
string encodedUsernamePassword = authorizeHeader.Substring("Basic ".Length).Trim();
string usernamePassword = ISO_8859_1_ENCODING.GetString(Convert.FromBase64String(encodedUsernamePassword));
string username = usernamePassword.Split(':')[0];
string password = usernamePassword.Split(':')[1];
return (username, password);
}
public async Task<IActionResult> Index([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
// Now use username and password with whatever authentication process you want
return View();
}
此示例演示如何在ASP.NET Core Identity中使用此功能。
public class HomeController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[AllowAnonymous]
public async Task<IActionResult> MyApiEndpoint([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
IdentityUser user = await _userManager.FindByNameAsync(username);
bool successfulAuthentication = await _userManager.CheckPasswordAsync(user, password);
if (successfulAuthentication)
return Ok();
else
return Unauthorized();
}
}
Constants.ISO_8859_1_ENCODING
替换为 static Encoding ISO_8859_1_ENCODING = System.Text.Encoding.GetEncoding("ISO-8859-1")
以使其正常工作。 - rlv-danreturn Unauthorized();
上面添加 Response.Headers.WWWAuthenticate = "Basic";
,浏览器将会要求用户输入密码并重试。 - Christian Davén我们通过使用ActionFilter为一个内部服务实现了摘要安全性:
public class DigestAuthenticationFilterAttribute : ActionFilterAttribute
{
private const string AUTH_HEADER_NAME = "Authorization";
private const string AUTH_METHOD_NAME = "Digest ";
private AuthenticationSettings _settings;
public DigestAuthenticationFilterAttribute(IOptions<AuthenticationSettings> settings)
{
_settings = settings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ValidateSecureChannel(context?.HttpContext?.Request);
ValidateAuthenticationHeaders(context?.HttpContext?.Request);
base.OnActionExecuting(context);
}
private void ValidateSecureChannel(HttpRequest request)
{
if (_settings.RequireSSL && !request.IsHttps)
{
throw new AuthenticationException("This service must be called using HTTPS");
}
}
private void ValidateAuthenticationHeaders(HttpRequest request)
{
string authHeader = GetRequestAuthorizationHeaderValue(request);
string digest = (authHeader != null && authHeader.StartsWith(AUTH_METHOD_NAME)) ? authHeader.Substring(AUTH_METHOD_NAME.Length) : null;
if (string.IsNullOrEmpty(digest))
{
throw new AuthenticationException("You must send your credentials using Authorization header");
}
if (digest != CalculateSHA1($"{_settings.UserName}:{_settings.Password}"))
{
throw new AuthenticationException("Invalid credentials");
}
}
private string GetRequestAuthorizationHeaderValue(HttpRequest request)
{
return request.Headers.Keys.Contains(AUTH_HEADER_NAME) ? request.Headers[AUTH_HEADER_NAME].First() : null;
}
public static string CalculateSHA1(string text)
{
var sha1 = System.Security.Cryptography.SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(hash);
}
}
[Route("api/xxxx")]
[ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
public class MyController : Controller
{
[HttpGet]
public string Get()
{
return "HELLO";
}
}