Аутентифікація веб-API ASP.NET Core


98

Я боюся з тим, як налаштувати автентифікацію у своїй веб-службі. Послуга будується за допомогою веб-API ASP.NET Core.

Усі мої клієнти (програми WPF) повинні використовувати однакові облікові дані для виклику операцій веб-служби.

Після деяких досліджень я придумав базову автентифікацію - надсилання імені користувача та пароля в заголовку запиту HTTP. Але після годин досліджень мені здається, що базова автентифікація - це не шлях у ASP.NET Core.

Більшість знайдених ресурсів реалізують автентифікацію за допомогою OAuth або іншого проміжного програмного забезпечення. Але це здається занадто великим для мого сценарію, а також використанням ідентифікаційної частини ASP.NET Core.

Тож яким є правильний спосіб досягнення моєї мети - проста автентифікація за допомогою імені користувача та пароля у веб-службі ASP.NET Core?

Спасибі заздалегідь!

Відповіді:


75

Ви можете застосувати проміжне програмне забезпечення, яке обробляє базову автентифікацію.

public async Task Invoke(HttpContext context)
{
    var authHeader = context.Request.Headers.Get("Authorization");
    if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
    {
        var token = authHeader.Substring("Basic ".Length).Trim();
        System.Console.WriteLine(token);
        var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
        var credentials = credentialstring.Split(':');
        if(credentials[0] == "admin" && credentials[1] == "admin")
        {
            var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
            var identity = new ClaimsIdentity(claims, "Basic");
            context.User = new ClaimsPrincipal(identity);
        }
    }
    else
    {
        context.Response.StatusCode = 401;
        context.Response.Headers.Set("WWW-Authenticate", "Basic realm=\"dotnetthoughts.net\"");
    }
    await _next(context);
}

Цей код написаний у бета-версії ядра asp.net. Сподіваюся, це допоможе.


1
Дякую за вашу відповідь! Це саме те, що я шукав - просте рішення для базової автентифікації.
Фелікс,

1
У цьому коді є помилка через використання credentialstring.Split (':') - він не буде коректно обробляти паролі, які містять двокрапку. Код у відповіді Фелікса не страждає від цього питання.
Філ Денніс,

111

Тепер, після того, як я був направлений у правильному напрямку, ось моє повне рішення:

Це клас проміжного програмного забезпечення, який виконується на кожному вхідному запиті та перевіряє, чи має запит правильні облікові дані. Якщо облікові дані відсутні або вони помилкові, служба негайно відповідає 401 несанкціонованою помилкою.

public class AuthenticationMiddleware
{
    private readonly RequestDelegate _next;

    public AuthenticationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        string authHeader = context.Request.Headers["Authorization"];
        if (authHeader != null && authHeader.StartsWith("Basic"))
        {
            //Extract credentials
            string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));

            int seperatorIndex = usernamePassword.IndexOf(':');

            var username = usernamePassword.Substring(0, seperatorIndex);
            var password = usernamePassword.Substring(seperatorIndex + 1);

            if(username == "test" && password == "test" )
            {
                await _next.Invoke(context);
            }
            else
            {
                context.Response.StatusCode = 401; //Unauthorized
                return;
            }
        }
        else
        {
            // no authorization header
            context.Response.StatusCode = 401; //Unauthorized
            return;
        }
    }
}

Розширення проміжного програмного забезпечення потрібно викликати в методі Налаштування класу запуску служби

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

І це все! :)

Дуже хороший ресурс для проміжного програмного забезпечення в .Net Core та автентифікацію можна знайти тут: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/


4
Дякуємо за розміщення повного рішення. Однак мені довелося додати рядок 'context.Response.Headers.Add ("WWW-Authenticate", "Basic realm = \" realm \ "");' до розділу "немає заголовка авторизації", щоб браузер запитував облікові дані.
m0n0ph0n

Наскільки безпечна ця автентифікація? Що робити, якщо хтось нюхає заголовок запиту та отримує ім’я користувача / пароль?
Бевер Салах

5
@BewarSalah, ви повинні подати таке рішення через https
вівторок,

2
Деякі контролери повинні дозволяти анонімність. Це рішення проміжного програмного забезпечення не вдасться, оскільки воно перевірятиме заголовок авторизації у кожному запиті.
Картік

28

Наприклад, щоб використовувати це лише для певних контролерів:

app.UseWhen(x => (x.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)), 
            builder =>
            {
                builder.UseMiddleware<AuthenticationMiddleware>();
            });

22

Я думаю, що ви можете піти з JWT (Json Web Tokens).

Спочатку потрібно встановити пакет System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Вам потрібно буде додати контролер для генерації токенів та автентифікації, як цей:

public class TokenController : Controller
{
    [Route("/token")]

    [HttpPost]
    public IActionResult Create(string username, string password)
    {
        if (IsValidUserAndPasswordCombination(username, password))
            return new ObjectResult(GenerateToken(username));
        return BadRequest();
    }

    private bool IsValidUserAndPasswordCombination(string username, string password)
    {
        return !string.IsNullOrEmpty(username) && username == password;
    }

    private string GenerateToken(string username)
    {
        var claims = new Claim[]
        {
            new Claim(ClaimTypes.Name, username),
            new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
            new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
        };

        var token = new JwtSecurityToken(
            new JwtHeader(new SigningCredentials(
                new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
                                         SecurityAlgorithms.HmacSha256)),
            new JwtPayload(claims));

        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Після цього оновлення класу Startup.cs виглядатиме нижче:

namespace WebAPISecurity
{   
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = "JwtBearer";
            options.DefaultChallengeScheme = "JwtBearer";
        })
        .AddJwtBearer("JwtBearer", jwtBearerOptions =>
        {
            jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
                ValidateIssuer = false,
                //ValidIssuer = "The name of the issuer",
                ValidateAudience = false,
                //ValidAudience = "The name of the audience",
                ValidateLifetime = true, //validate the expiration and not before values in the token
                ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
            };
        });

    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseAuthentication();

        app.UseMvc();
    }
}

І все, що залишилось зараз - це поставити [Authorize] атрибут на контролерах або діях, які ви хочете.

Ось посилання на повний підручник.

http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/


9

Я застосував BasicAuthenticationHandlerдля базової автентифікації, щоб ви могли використовувати її зі стандартними атрибутами Authorizeта AllowAnonymous.

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authHeader = (string)this.Request.Headers["Authorization"];

        if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
        {
            //Extract credentials
            string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
            Encoding encoding = Encoding.GetEncoding("iso-8859-1");
            string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));

            int seperatorIndex = usernamePassword.IndexOf(':', StringComparison.OrdinalIgnoreCase);

            var username = usernamePassword.Substring(0, seperatorIndex);
            var password = usernamePassword.Substring(seperatorIndex + 1);

            //you also can use this.Context.Authentication here
            if (username == "test" && password == "test")
            {
                var user = new GenericPrincipal(new GenericIdentity("User"), null);
                var ticket = new AuthenticationTicket(user, new AuthenticationProperties(), Options.AuthenticationScheme);
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
            else
            {
                return Task.FromResult(AuthenticateResult.Fail("No valid user."));
            }
        }

        this.Response.Headers["WWW-Authenticate"]= "Basic realm=\"yourawesomesite.net\"";
        return Task.FromResult(AuthenticateResult.Fail("No credentials."));
    }
}

public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
    public BasicAuthenticationMiddleware(
       RequestDelegate next,
       IOptions<BasicAuthenticationOptions> options,
       ILoggerFactory loggerFactory,
       UrlEncoder encoder)
       : base(next, options, loggerFactory, encoder)
    {
    }

    protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
    {
        return new BasicAuthenticationHandler();
    }
}

public class BasicAuthenticationOptions : AuthenticationOptions
{
    public BasicAuthenticationOptions()
    {
        AuthenticationScheme = "Basic";
        AutomaticAuthenticate = true;
    }
}

Реєстрація на Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. За допомогою цього коду ви можете обмежити будь-який контролер стандартним атрибутом Autorize:

[Authorize(ActiveAuthenticationSchemes = "Basic")]
[Route("api/[controller]")]
public class ValuesController : Controller

і використовувати атрибут, AllowAnonymousякщо ви застосовуєте фільтр авторизації на рівні програми.


1
Я використав ваш код, але я помітив, незалежно від того, встановлено авторизацію (ActiveAuthenticationSchemes = "Basic")] чи не під час кожного виклику проміжне програмне забезпечення активується, в результаті чого кожен контролер перевіряється також, коли це не потрібно.
CSharper

Мені подобається така відповідь
KTOV

1
робочий приклад тут: jasonwatmore.com/post/2018/09/08/…
bside

Я думаю, що це відповідь - це шлях, оскільки він дозволяє використовувати стандартні атрибути авторизації / дозволуанонімічного подальшого рішення. Поруч з цим, легко буде використовувати іншу схему автентифікації пізніше на етапі проекту, якщо це буде потрібно
Фредерік Гейзельс

0

У цьому загальнодоступному репозиторії Github https://github.com/boskjoett/BasicAuthWebApi ви можете побачити простий приклад веб-API ASP.NET Core 2.2 із кінцевими точками, захищеними базовою автентифікацією.


Якщо ви хочете використовувати автентифікований ідентифікатор у своєму контролері (SecureValuesController), створення квитка недостатньо, оскільки об’єкт Request.User порожній. Чи все-таки нам потрібно призначити цей ClaimsPrincipal поточному контексту в AuthenticationHandler? Так ми робили це у старих WebApi ...
pseabury

0

Як справедливо сказано в попередніх публікаціях, одним із способів є впровадження власного базового проміжного програмного забезпечення для автентифікації. Я знайшов найкращий робочий код із поясненнями в цьому блозі: Basic Auth із користувацьким проміжним програмним забезпеченням

Я посилався на той самий блог, але мені довелося зробити 2 адаптації:

  1. Додаючи проміжне програмне забезпечення у файл запуску -> Налаштувати функцію, завжди додайте власне проміжне програмне забезпечення перед додаванням app.UseMvc ().
  2. Під час читання імені користувача, пароля з файлу appsettings.json, додайте статичне властивість лише для читання у файл запуску. Потім прочитайте з appsettings.json. Нарешті, прочитайте значення з будь-якої точки проекту. Приклад:

    public class Startup
    {
      public Startup(IConfiguration configuration)
      {
        Configuration = configuration;
      }
    
      public IConfiguration Configuration { get; }
      public static string UserNameFromAppSettings { get; private set; }
      public static string PasswordFromAppSettings { get; private set; }
    
      //set username and password from appsettings.json
      UserNameFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("UserName").Value;
      PasswordFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("Password").Value;
    }
    

0

Ви можете використовувати ActionFilterAttribute

public class BasicAuthAttribute : ActionFilterAttribute
{
    public string BasicRealm { get; set; }
    protected NetworkCredential Nc { get; set; }

    public BasicAuthAttribute(string user,string pass)
    {
        this.Nc = new NetworkCredential(user,pass);
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var req = filterContext.HttpContext.Request;
        var auth = req.Headers["Authorization"].ToString();
        if (!String.IsNullOrEmpty(auth))
        {
            var cred = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(auth.Substring(6)))
                .Split(':');
            var user = new {Name = cred[0], Pass = cred[1]};
            if (user.Name == Nc.UserName && user.Pass == Nc.Password) return;
        }

        filterContext.HttpContext.Response.Headers.Add("WWW-Authenticate",
            String.Format("Basic realm=\"{0}\"", BasicRealm ?? "Ryadel"));
        filterContext.Result = new UnauthorizedResult();
    }
}

і додайте атрибут до свого контролера

[BasicAuth("USR", "MyPassword")]


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.