NET 核心 Web API 身份验证

我正在努力解决如何在我的 Web 服务中设置身份验证的问题。 该服务是使用 ASP.NET 核心 web api 构建的。

我的所有客户端(WPF 应用程序)都应该使用相同的凭据来调用 Web 服务操作。

经过一些研究,我想出了基本的身份验证——在 HTTP 请求的头部发送用户名和密码。 但是经过几个小时的研究,在我看来,基本的身份验证不是 ASP.NET 核心的方式。

我找到的大多数资源都是使用 OAuth 或其他中间件实现身份验证。但是对于我的场景来说,这似乎有点过大了,而且还使用了 ASP.NET 核心的 Identity 部分。

那么,实现我的目标——在 ASP.NET 核心 Web 服务中使用用户名和密码进行简单身份验证——的正确方法是什么呢?

先谢谢你!

125182 次浏览

You can implement a middleware which handles Basic authentication.

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

This code is written in a beta version of asp.net core. Hope it helps.

Now, after I was pointed in the right direction, here's my complete solution:

This is the middleware class which is executed on every incoming request and checks if the request has the correct credentials. If no credentials are present or if they are wrong, the service responds with a 401 Unauthorized error immediately.

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

The middleware extension needs to be called in the Configure method of the service Startup class

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


app.UseMiddleware<AuthenticationMiddleware>();


app.UseMvc();
}

And that's all! :)

A very good resource for middleware in .Net Core and authentication can be found here: https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/

To use this only for specific controllers for example use this:

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

I have implemented BasicAuthenticationHandler for basic authentication so you can use it with standart attributes Authorize and 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;
}
}

Registration at Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. With this code, you can restrict any controller with standart attribute Autorize:

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

and use attribute AllowAnonymous if you apply authorize filter on application level.

I think you can go with JWT (Json Web Tokens).

First you need to install the package System.IdentityModel.Tokens.Jwt:

$ dotnet add package System.IdentityModel.Tokens.Jwt

You will need to add a controller for token generation and authentication like this one:

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

After that update Startup.cs class to look like below:

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

And that's it, what is left now is to put [Authorize] attribute on the Controllers or Actions you want.

Here is a link of a complete straight forward tutorial.

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

ASP.NET Core 2.0 with Angular

https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login

Make sure to use type of authentication filter

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

In this public Github repo https://github.com/boskjoett/BasicAuthWebApi you can see a simple example of a ASP.NET Core 2.2 web API with endpoints protected by Basic Authentication.

As rightly said by previous posts, one of way is to implement a custom basic authentication middleware. I found the best working code with explanation in this blog: Basic Auth with custom middleware

I referred the same blog but had to do 2 adaptations:

  1. While adding the middleware in startup file -> Configure function, always add custom middleware before adding app.UseMvc().
  2. While reading the username, password from appsettings.json file, add static read only property in Startup file. Then read from appsettings.json. Finally, read the values from anywhere in the project. Example:

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

You can use an 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();
}
}

and add the attribute to your controller

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