使用 .net 核心 web 验证 .net 控制台应用程序 API

Authenticating .net console applications with .net core web API

我有一个 .net core 3.1 web API ,它是用 JWT 身份验证 构建的,它与Angular UI 并且按预期工作。

以下是我的JWT认证中间件


services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

// Adding Jwt Bearer
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.IncludeErrorDetails = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = false,
        ValidateAudience = false,       
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))    
    };
    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
            {
                context.Response.Headers.Add("Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
});

现在我需要创建更多 Web API 方法,这些方法将被 Angular UI 以及一些现有的 为内部操作创建的计划任务.net 控制台应用程序 将使用 Web api 方法),并且将 运行 在后台。

我的 API 控制器装饰有 [Authorize] 属性。它与 Angular UI 一起工作正常,其中身份验证和授权是使用 JWT 承载实现的 token.The 现在问题是计划任务的集成没有获取令牌的逻辑。

如何在身份验证方面将这些控制台应用程序与 .net core web API 集成?最简单的选择(我认为)是使用用户名“servicetask”创建用户登录并根据该用户名获取令牌并执行 API 操作(但这需要更多努力,因为 no.of 控制台应用程序越来越多,还有一些来自其他项目的应用程序。

在这种情况下有什么方法可以处理身份验证吗?

  1. 从控制台应用程序传递一些 API 密钥并在 Web API 中传递身份验证是好的做法吗?那可能吗 ?那么如何在.net core web中处理请求api?

  2. 是否可以为这些服务帐户创建任何 JWT 角色或声明并验证它们?

请帮忙。

1.IMO,不,这不是个好主意。 2. 是的,您可以针对此场景使用声明。 使用 BackgroundService 来 运行 您的任务并在此 class.

上注入声明原则

此示例用于服务提供商帐户声明: serviceAccountPrincipleProvider.cs

 public class ServiceAccountPrincipalProvider : IClaimsPrincipalProvider
{
    private readonly ITokenProvider tokenProvider;

    public ServiceAccountPrincipalProvider(ITokenProvider tokenProvider)
    {
        this.tokenProvider = tokenProvider;
    } 

    public ClaimsPrincipal CurrentPrincipal
    {
        get
        {
            var accessToken = tokenProvider.GetAccessTokenAsync().GetAwaiter().GetResult();
            if (accessToken == null)
                return null;

            var identity = new ClaimsIdentity(AuthenticationTypes.Federation);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, accessToken.Subject));
            identity.AddClaim(new Claim(AppClaimTypes.Issuer, accessToken.Issuer));
            identity.AddClaim(new Claim(AppClaimTypes.AccessToken, accessToken.RawData));

            return new ClaimsPrincipal(identity);
        }
    }
}

这是您的 IClaimsProvider 接口:

public interface IClaimsPrincipalProvider
{
    ClaimsPrincipal CurrentPrincipal { get; }
}
  1. 不要绕过身份验证。您可以将 appKey(用于标识应用程序实例的密钥)传递给负责标识您的 dotnet 控制台应用程序的 webapi 端点。如果 appkey 是你注册的 appkeys 列表的一部分,让 webapi 端点通过随后的身份验证步骤代表控制台应用程序获取令牌,使用你的 webapi 身份验证服务和 return 控制台应用程序的 JWT 令牌。 就我而言,我在 dotnet 4.5 上有控制台应用程序 运行,我提到这一点是因为 HttpClient 在以前的版本中不可用。使用 HttpClient,您可以在控制台应用程序中执行以下操作。
HttpClient client = new HttpClient(); 

client.BaseAddress = new Uri("localhost://mywebapi/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/text"));

HttpResponseMessage response= client.GetAsync("api/appidentityendpoint/appkey").GetAwaiter().GetResult();

var bytarr = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
string responsemessage = Encoding.UTF8.GetString(bytarr);

res = JsonConvert.DeserializeObject<Authtoken>(responsemessage);

Authtoken object可以像

一样简单
public class Authtoken
{
  public string JwtToken{ get; set; }
}

获得令牌后,将其添加到 HttpClient headers,以便后续调用受保护的端点

  client.DefaultRequestHeaders.Add("Authorization", "Bearer " + res.JwtToken);

  client.GetAsync("api/protectedendpoint").GetAwaiter().GetResult();

显然需要错误处理来处理令牌过期等情况下的重新认证

在服务器端,一个简化的例子如下

 [Produces("application/json")]
 [Route("api/Auth")]
 public class AuthController : Controller
 { 
    
    private readonly IAppRegService _regAppService;
    public AuthController(IAppRegService regAppService){
       _regAppService = regAppService;
    };
   //api/auth/console/login/585
   [HttpGet, Route("console/login/{appkey}")]
   public async Task<IActionResult> Login(string appkey)
   {
      
      // write logic to check in your db if appkey is the key of a registered console app.
      // _regAppService has methods to connect to db or read file to check if key exists from your repository of choice
       var appkeyexists = _regAppService.CheckAppByAppKey(appkey);
       if(appkeyexists){       
       //create claims list
       List<Claim> claims = new List<Claim>();
       claims.Add(new Claim("appname", "console",ClaimValueTypes.String));
       claims.Add(new Claim("role","daemon",ClaimValueTypes.String));

      //create a signing secret
       var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("yoursecretkey"));
       var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);  
       //create token options
       var tokenOptions = new JwtSecurityToken(
                            issuer: "serverurl",
                            audience:"consoleappname",
                            claims: claims,
                            expires: DateTime.Now.AddDays(5),
                            signingCredentials: signinCredentials
                        );
        //create token
        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
      //return token
       return new OkObjectResult(new Authtoken { JwtToken= tokenString });
                       
       } else {
          return Unauthorized();
       }
   }
 }
  1. 您可以在 appsettings、db 或其他地方创建登录密码配置以发送令牌 (web api)。

Worker.cs(控制台应用程序)

public struct UserLogin
{
  public string user;
  public string password;
}
// ...
private async Task<string> GetToken(UserLogin login)
{
  try {
    string token;
    var content = new StringContent(JsonConvert.SerializeObject(login), Encoding.UTF8, "application/json");
    using (var httpClient = new HttpClient())
      using (var response = await httpClient.PostAsync($"{api}/login", content))
      {
        var result = await response.Content.ReadAsStringAsync();
        var request = JsonConvert.DeserializeObject<JObject>(result);
        token = request["token"].ToObject<string>();
      }
    return token;
  }
  catch (Exception e)
  {
    throw new Exception(e.Message);
  }      
}
  1. 给你的控制台一个没有有效期的 jwt 令牌,或者一个给你足够时间的令牌。如果您需要使令牌无效,请遵循此 。在appsettings.json上添加jwt,读取token如下:

appsettings.json

{
  //...
  "Worker" : "dotnet",
  "Token": "eyJhbGciOiJIUzI1Ni...",
  "ApiUrl": "http://localhost:3005",
  //...
}

Worker.cs

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;

public Worker(ILogger<Worker> _logger, IConfiguration _cfg)
{
  logger = _logger;
  //...
  api = _cfg["ApiUrl"];
  token = _cfg["Token"];
}

private async Task SendResult(SomeModel JobResult)
{
  var content = new StringContent(JsonConvert.SerializeObject(JobResult), Encoding.UTF8, "application/json");
  using (var httpClient = new HttpClient())
  {
    httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
    using (var response = await httpClient.PostAsync($"{api}/someController", content))
    {
      var result = await response.Content.ReadAsStringAsync();
      var rs = JsonConvert.DeserializeObject(result);
      Console.WriteLine($"API response {response.ReasonPhrase}");
    }
  }
}

更新:

如果需要控制请求:

Startup.cs

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  .AddJwtBearer(options =>
  {
    options.TokenValidationParameters = new TokenValidationParameters()
    {
      // ...
    };
    options.Events = new JwtBearerEvents
    {
      OnTokenValidated = TokenValidation
    };
  });
private static Task TokenValidation(TokenValidatedContext context)
{
  // your custom validation
  var hash = someHashOfContext();
  if (context.Principal.FindFirst(ClaimTypes.Hash).Value != hash)
  {
    context.Fail("You cannot access here");
  }
  return Task.CompletedTask;
}

我将介绍如何从 WebAssembly 应用程序到 .NET Core 执行 JWT 身份验证 API。一切都基于这个YouTube video。它解释了你需要知道的一切。下面是视频中的代码示例,可让您了解必须执行的操作。

这是我的授权控制器:

// A bunch of usings

namespace Server.Controllers.Authentication
{
    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class AuthenticateController : ControllerBase
    {
        private readonly UserManager<ApplicationUser> userManager;
        private readonly RoleManager<IdentityRole> roleManager;
        private readonly IConfiguration _configuration;
        private readonly AppContext appContext;

        public AuthenticateController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration, AppContext appContext)
        {
            this.userManager = userManager;
            this.roleManager = roleManager;
            this._configuration = configuration;
            this.appContext = appContext;
        }

        [HttpPost]
        [Route("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
        {
            ApplicationUser user = await userManager.FindByNameAsync(loginModel.Username);

            if ((user is not null) && await userManager.CheckPasswordAsync(user, loginModel.Password))
            {
                IList<string> userRoles = await userManager.GetRolesAsync(user);

                List<Claim> authClaims = new()
                {
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(ClaimTypes.NameIdentifier, user.Id),
                    new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(ClaimTypes.AuthenticationMethod, "pwd")
                };

                foreach (string role in userRoles)
                {
                    authClaims.Add(new Claim(ClaimTypes.Role, role));
                }

                SymmetricSecurityKey authSigningKey = new(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
                //SymmetricSecurityKey authSigningKey = Startup.SecurityAppKey;

                JwtSecurityToken token = new(
                    issuer: _configuration["JWT:ValidIssuer"],
                    //audience: _configuration["JWT:ValidAudience"],
                    expires: DateTime.Now.AddHours(3),
                    claims: authClaims,
                    signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                    );

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token),
                    expiration = token.ValidTo
                });
            }

            return Unauthorized();
        }

        [HttpPost]
        [Route("register")]
        [AllowAnonymous]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            ApplicationUser userExists = await userManager.FindByNameAsync(model.Username);

            if (userExists != null)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User already exists!" });
            }

            ApplicationUser user = new()
            {
                Email = model.Email,
                SecurityStamp = Guid.NewGuid().ToString(),
                UserName = model.Username
            };

            IdentityResult result = await userManager.CreateAsync(user, model.Password);

            if (!result.Succeeded)
            {
                return StatusCode(StatusCodes.Status500InternalServerError, new Response { Status = "Error", Message = "User creation failed! Please check user details and try again." });
            }

            await userManager.AddToRoleAsync(user, UserRoles.User);

            return Ok(new Response { Status = "Success", Message = "User created successfully!" });
        }
    }
}

用户注册后会自动添加到 User Role。您应该做的是为您的每个控制台应用程序创建帐户,甚至为所有内部应用程序创建一个全局帐户,然后将其分配给自定义角色。

之后,在只能由您的内部应用程序访问的所有 API 个端点上添加此属性:[Authorize(Roles = UserRoles.Internal)]

UserRoles 是一个静态 class,每个角色都有字符串属性。

可以找到有关基于角色的授权的更多信息here

最好的方法是同时允许不记名令牌和 API 密钥授权,特别是因为您允许用户和(内部)服务访问。

添加API密钥中间件(我个人使用this,使用起来很简单-包名为AspNetCore.Authentication.ApiKey)自定义验证(在数据库中存储API密钥连同常规用户数据或在配置中,无论你喜欢什么)。修改控制器上的 [Authorize] 属性,以便可以使用 Bearer 和 ApiKey 授权。 Angular 应用程序继续使用 Bearer 身份验证,任何 service/console 应用程序(或任何其他客户端,如果在某些情况下需要,包括 Angular 客户端)发送 X-Api-Key header 包含API 分配给该应用程序的密钥。

中间件配置应如下所示:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddApiKeyInHeader(ApiKeyDefaults.AuthenticationScheme, options =>
{
    options.KeyName = "X-API-Key";
    options.SuppressWWWAuthenticateHeader = true;
    options.Events = new ApiKeyEvents
    {
        // A delegate assigned to this property will be invoked just before validating the api key. 
        OnValidateKey = async (context) =>
        {
            var apiKey = context.ApiKey.ToLower();
            // custom code to handle the api key, create principal and call Success method on context. apiUserService should look up the API key and determine is it valid and which user/service is using it
            var apiUser = apiUserService.Validate(apiKey);
            if (apiUser != null)
            {
                ... fill out the claims just as you would for user which authenticated using Bearer token...
                var claims = GenerateClaims();
                context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
                context.Success();
             }
             else
             {
                 // supplied API key is invalid, this authentication cannot proceed
                 context.NoResult();
             }
         }
        };
})
// continue with JwtBearer code you have
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, x => ...

这整理了 Startup.cs 部分。

现在,在您想要同时启用 Bearer 和 ApiKey 身份验证的控制器中,修改属性,使其看起来像这样:

[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = "ApiKey, Bearer")]
public class SomeController : ControllerBase

现在,Angular 客户端仍将以相同的方式工作,但控制台应用程序可能会像这样调用 API:

using (HttpClient client = new HttpClient())
{
    // header must match definition in middleware
    client.DefaultRequestHeaders.Add("X-API-Key", "someapikey");
    client.BaseAddress = new Uri(url);
    using (HttpResponseMessage response = await client.PostAsync(url, q))
    {
        using (HttpContent content =response.Content)
        {
            string mycontent = await content.ReadAsStringAsync();              
        }        
    }
}

我认为这种方法充分利用了 AuthenticationHandler and offers cleanest approach of handling both "regular" clients using JWT and services using fixed API keys, closely following something like OAuth middleware. More details 关于构建自定义身份验证处理程序,如果有人想从头开始构建类似这样的东西,基本上实现任何类型的身份验证。

缺点当然是这些 API 密钥的安全性,即使您仅将它们用于内部服务。通过使用声明限制那些 API 密钥的访问范围,而不是对多个服务使用相同的 API 密钥并定期更改它们,可以稍微解决这个问题。此外,如果不使用 SSL,API 密钥容易被拦截 (MITM),因此请注意这一点。