【问题标题】:Basic Authentication in ASP.NET CoreASP.NET Core 中的基本身份验证
【发布时间】:2016-02-09 16:06:02
【问题描述】:

问题

如何在 ASP.NET Core Web 应用程序中使用自定义成员身份实现基本身份验证?

备注

  • 在 MVC 5 中,我使用了 article 中的说明,这需要在 WebConfig 中添加一个模块。

  • 我仍在IIS 上部署我的新MVC Core应用程序,但这种方法似乎不起作用。

  • 我也不想使用内置支持基本身份验证的 IIS,因为它使用 Windows 凭据。

【问题讨论】:

    标签: asp.net-core basic-authentication asp.net-core-mvc asp.net-core-1.0


    【解决方案1】:

    由于其潜在的不安全和性能问题,ASP.NET 安全将不包括基本身份验证中间件。

    如果您需要基本身份验证中间件进行测试,请查看https://github.com/blowdart/idunno.Authentication

    【讨论】:

    • 由于一些兼容性“问题”,我们在工作项目中使用了来自kukkimonsutaOdachi 库。它非常适合我们的需要。
    • 两个链接都已损坏。这就是你想要的:github.com/Kukkimonsuta/Odachi
    • 如果我们尝试编写一个只支持基本身份验证的Web Hook for Visual Studio Team Services,我们该怎么办?
    • 为什么只“用于测试目的”?为什么不能在生产中使用这种方法?
    • @blowdart 想详细说明“潜在的不安全和性能问题”?基于 https 的基本身份验证非常安全,并且广泛用于生产环境(例如 stripe、mailchimp、aws 等)。 “测试目的”只是错误的指导以及上下文不足。除非我们遗漏了在 asp.net 安全本身中实施不佳的某些东西。
    【解决方案2】:

    ASP.NET Core 2.0 对身份验证和身份进行了重大更改。

    在 1.x 上,身份验证提供程序是通过中间件配置的(作为接受的答案的实现)。 在 2.0 上,它基于服务。

    MS doc 的详细信息: https://docs.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

    【讨论】:

      【解决方案3】:

      我对 ASP.NET Core 身份验证中间件设计感到失望。作为一个框架,它应该简化并带来更高的生产力,而这里并非如此。

      无论如何,一种简单而安全的方法是基于授权过滤器,例如IAsyncAuthorizationFilter。请注意,授权过滤器将在其他中间件之后执行,当 MVC 选择某个控制器操作并移动到过滤器处理时。但在过滤器中,首先执行授权过滤器 (details)。

      我只是想评论 Clays 对 Hector 的回答的评论,但不喜欢 Hectors 抛出异常且没有任何挑战机制的示例,所以这是一个可行的示例。

      记住:

      1. 在生产中没有 HTTPS 的基本身份验证非常糟糕。确保您的 HTTPS 设置得到强化(例如,禁用所有 SSL 和 TLS
      2. 如今,基本身份验证的大多数用途是公开受 API 密钥保护的 API(请参阅 Stripe.NET、Mailchimp 等)。使 curl 友好的 API 与服务器上的 HTTPS 设置一样安全。

      考虑到这一点,不要相信任何关于基本身份验证的 FUD。跳过像基本身份验证这样基本的东西是高意见和低实质内容。您可以在 cmets 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://docs.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);
              }
          }
      

      【讨论】:

        【解决方案4】:

        我们使用 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);
            }
        }
        

        之后,您可以使用 Digest 安全性注释要访问的控制器或方法:

        [Route("api/xxxx")]
        [ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
        public class MyController : Controller
        {
            [HttpGet]
            public string Get()
            {
                return "HELLO";
            }
        
        }
        

        要实现基本安全,只需将 DigestAuthenticationFilterAttribute 更改为不使用 SHA1,而是直接对 Authorization 标头进行 Base64 解码。

        【讨论】:

        • 仅供参考。这种方法的缺点是这个过滤器(身份验证)发生得很晚——它在授权过滤器运行之后运行,使授权过滤器无用。
        【解决方案5】:

        .NET Core 中的超简单基本身份验证:

        1。添加这个实用方法:

        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);
        }
        

        2。更新控制器操作以从 Authorization 标头获取用户名和密码:

        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();
            }
        }
        

        【讨论】:

        • 这是一个很好的例子!我希望微软在过去 20 年中只使用它而不是所有不同的身份解决方案。我还会在未授权之前添加一个额外的“WWW-Authenticate”响应标头,以向人们展示如何显示对话框。谢谢!
        • Constants.ISO_8859_1_ENCODING 替换为 static Encoding ISO_8859_1_ENCODING = System.Text.Encoding.GetEncoding("ISO-8859-1") 以使其正常工作。
        猜你喜欢
        • 2019-10-07
        • 2015-05-16
        • 2018-09-06
        • 2019-05-19
        • 2021-11-10
        • 1970-01-01
        • 1970-01-01
        • 2013-12-07
        • 2017-03-30
        相关资源
        最近更新 更多