Otentikasi API Web Inti ASP.NET

98

Saya kesulitan dengan cara mengatur otentikasi di layanan web saya. Layanan ini dibangun dengan api web ASP.NET Core.

Semua klien saya (aplikasi WPF) harus menggunakan kredensial yang sama untuk memanggil operasi layanan web.

Setelah beberapa penelitian, saya menemukan otentikasi dasar - mengirim nama pengguna dan kata sandi di header permintaan HTTP. Tetapi setelah berjam-jam penelitian, menurut saya otentikasi dasar bukanlah cara untuk masuk di ASP.NET Core.

Sebagian besar sumber daya yang saya temukan menerapkan otentikasi menggunakan OAuth atau beberapa middleware lainnya. Tapi itu tampaknya terlalu besar untuk skenario saya, serta menggunakan bagian Identitas ASP.NET Core.

Jadi apa cara yang tepat untuk mencapai tujuan saya - otentikasi sederhana dengan nama pengguna dan kata sandi dalam layanan web ASP.NET Core?

Terima kasih sebelumnya!

Felix
sumber

Jawaban:

75

Anda dapat menerapkan middleware yang menangani otentikasi Dasar.

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

Kode ini ditulis dalam versi beta dari inti asp.net. Semoga membantu.

Anuraj
sumber
1
Terima kasih atas jawaban anda! Inilah yang saya cari - solusi sederhana untuk otentikasi dasar.
Felix
1
Ada bug dalam kode ini karena penggunaan credentialstring.Split (':') - itu tidak akan menangani sandi yang mengandung titik dua dengan benar. Kode dalam jawaban Felix tidak mengalami masalah ini.
Phil Dennis
111

Sekarang, setelah saya diarahkan ke arah yang benar, inilah solusi lengkap saya:

Ini adalah kelas middleware yang dijalankan pada setiap permintaan yang masuk dan memeriksa apakah permintaan tersebut memiliki kredensial yang benar. Jika tidak ada kredensial atau jika salah, layanan segera merespons dengan kesalahan 401 Tidak Sah .

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

Ekstensi middleware perlu dipanggil dalam metode Configure dari kelas Startup layanan

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

    app.UseMiddleware<AuthenticationMiddleware>();

    app.UseMvc();
}

Dan itu saja! :)

Sumber daya yang sangat baik untuk middleware di .Net Core dan otentikasi dapat ditemukan di sini: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

Felix
sumber
4
Terima kasih telah memposting solusi lengkap. Namun, saya harus menambahkan baris 'context.Response.Headers.Add ("WWW-Authenticate", "Basic realm = \" realm \ "");' ke bagian 'tajuk tanpa otorisasi' agar browser meminta kredensial.
m0n0ph0n
Seberapa aman otentikasi ini? Bagaimana jika seseorang mengendus header permintaan dan mendapatkan nama pengguna / kata sandi?
Bewar Salah
5
@BewarSalah Anda harus melayani solusi semacam ini melalui https
wal
2
Beberapa pengontrol harus mengizinkan anonim. Solusi middleware ini akan gagal dalam kasus tersebut karena akan memeriksa header otorisasi di setiap permintaan.
Karthik
28

Untuk menggunakan ini hanya untuk pengontrol tertentu misalnya gunakan ini:

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

Saya pikir Anda bisa menggunakan JWT (Json Web Tokens).

Pertama, Anda perlu menginstal paket System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

Anda perlu menambahkan pengontrol untuk pembuatan token dan otentikasi seperti ini:

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

Setelah itu perbarui kelas Startup.cs menjadi seperti di bawah ini:

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

Dan hanya itu, yang tersisa sekarang adalah meletakkan [Authorize] atribut pada Pengontrol atau Tindakan yang Anda inginkan.

Berikut ini tautan tutorial lurus ke depan lengkap.

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

AJ -
sumber
9

Saya telah menerapkan BasicAuthenticationHandleruntuk otentikasi dasar sehingga Anda dapat menggunakannya dengan atribut standar Authorizedan 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;
    }
}

Pendaftaran di Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. Dengan kode ini, Anda dapat membatasi pengontrol apa pun dengan atribut standar Autorize:

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

dan gunakan atribut AllowAnonymousjika Anda menerapkan filter otorisasi pada tingkat aplikasi.

Ivan R.
sumber
1
Saya menggunakan kode Anda, tetapi saya perhatikan tidak peduli apakah Otorisasi (ActiveAuthenticationSchemes = "Basic")] diatur atau tidak di setiap panggilan middleware akan diaktifkan sehingga setiap pengontrol divalidasi juga ketika tidak diinginkan.
CSharper
Saya suka jawaban ini
KTOV
1
contoh kerja di sini: jasonwatmore.com/post/2018/09/08/…
bside
Saya pikir ini adalah jawabannya adalah cara yang harus dilakukan, karena memungkinkan Anda untuk menggunakan atribut otorisasi / allowanonymous standar lebih jauh dalam solusi. Selain itu, seharusnya mudah untuk menggunakan skema otentikasi lain di kemudian hari dalam fase proyek jika diperlukan
Frederik Gheysels
0

Dalam repo Github publik ini https://github.com/boskjoett/BasicAuthWebApi Anda dapat melihat contoh sederhana dari API web ASP.NET Core 2.2 dengan titik akhir yang dilindungi oleh Otentikasi Dasar.

Bo Christian Skjøtt
sumber
Jika Anda ingin menggunakan Authenticated Identity di pengontrol Anda (SecureValuesController), membuat tiket tidak cukup karena objek Request.User kosong. Apakah kita masih perlu menetapkan ClaimsPrincipal ini ke Konteks saat ini di AuthenticationHandler? Itulah cara kami melakukannya di WebApi yang lebih lama ...
pseabury
0

Seperti yang dikatakan oleh posting sebelumnya, salah satu caranya adalah dengan mengimplementasikan middleware otentikasi dasar kustom. Saya menemukan kode kerja terbaik dengan penjelasan di blog ini: Basic Auth dengan custom middleware

Saya merujuk blog yang sama tetapi harus melakukan 2 adaptasi:

  1. Saat menambahkan middleware di file startup -> Configure function, selalu tambahkan middleware khusus sebelum menambahkan app.UseMvc ().
  2. Saat membaca nama pengguna, kata sandi dari file appsettings.json, tambahkan properti hanya baca statis di file Startup. Kemudian baca dari appsettings.json. Terakhir, baca nilai dari mana saja dalam proyek. Contoh:

    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;
    }
    
Palash Roy
sumber
0

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

dan tambahkan atribut ke pengontrol Anda

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

Luca Ziegler
sumber