【问题标题】:Authenticating .net console applications with .net core web API使用 .net 核心 Web API 对 .net 控制台应用程序进行身份验证
【发布时间】:2021-12-23 19:19:00
【问题描述】:

我有一个 .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] 属性。它在使用 JWT 不记名令牌实现身份验证和授权的 Angular UI 上运行良好。现在问题在于计划任务的集成,它没有获取令牌的逻辑。

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

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

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

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

请帮忙。

【问题讨论】:

  • 您添加了客户端应用程序的源代码,我的意思是验证令牌的源代码。你用什么发行代币?
  • 几个问题,1-您使用的是身份验证提供者吗? 2-您关心登录用户的任务还是只是想确保这是您的应用程序?请记住,应用程序与您的 api 有多种行为方式,要么只是应用程序而不是用户的行为,还是用户的行为? Angular 肯定会影响用户的行为,但服务可能是机器对机器,无需记录,所有身份验证提供者都将具有端点以在机器对机器的情况下生成令牌。寻找你的答案来写一个完整的答案!!
  • 观看this video 了解如何进行身份验证。该视频适用于 WebAssembly,但可以轻松转换为控制台应用程序。 WASM 应用的功能与 React 或 Angular 应用类似。
  • @khaledDehia:对于您提出的问题,1.我没有使用任何第 3 方身份验证提供程序。创建数据库用户表、令牌、角色和权限表。并使用该 2 验证用户、存储和刷新令牌。是的。只想检查请求是否仅来自预期的应用程序。
  • @A_developer 谢谢,对于您的控制台应用程序,您应该遵循机器到机器的流程,您不需要对用户(人类)的行为进行交互

标签: c# authentication asp.net-web-api jwt asp.net-core-webapi


【解决方案1】:
  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 令牌,或者一个给您足够时间的令牌。如果您需要使令牌失效,请遵循此link。在 appsettings.json 上添加 jwt 并读取令牌如下:

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

【讨论】:

    【解决方案2】:

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

    添加 API 密钥中间件(我个人使用 this,它使用简单 - 包名称为 AspNetCore.Authentication.ApiKey)和自定义验证(将 API 密钥与常规用户数据一起存储在数据库中或在配置中,无论您喜欢什么)。修改控制器上的[Authorize]属性,使Bearer和ApiKey授权都可以使用。 Angular 应用继续使用 Bearer 身份验证,并且任何服务/控制台应用(或任何其他客户端,在某些情况下需要时包括 Angular 客户端)发送 X-Api-Key 标头,其中包含分配给该应用的 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,并提供了最简洁的方法来处理使用 JWT 的“常规”客户端和使用固定 API 密钥的服务,紧跟 OAuth middleware 之类的东西。 More details 关于构建自定义身份验证处理程序,如果有人想从头开始构建这样的东西,基本上实现任何类型的身份验证。

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

    【讨论】:

      【解决方案3】:
      1. 不要绕过身份验证。您可以将 appKey(用于识别应用程序实例的密钥)传递给负责识别您的 dotnet 控制台应用程序的 webapi 端点。如果 appkey 是您注册的 appkeys 列表的一部分,请让 webapi 端点通过您的 webapi 身份验证服务的后续身份验证步骤代表控制台应用程序获取令牌,并将 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 对象可以这么简单

      public class Authtoken
      {
        public string JwtToken{ get; set; }
      }
      

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

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

      【讨论】:

      • 服务器端呢?如何同时处理 JWT 和 API 密钥?
      • 我添加了一个服务器端示例
      【解决方案4】:

      我将介绍如何从 WebAssembly 应用程序到 .NET Core API 执行 JWT 身份验证。一切都基于这个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 是一个静态类,每个角色都有字符串属性。

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

      【讨论】:

        【解决方案5】:

        1.IMO,不,这不是个好主意。 2. 是的,您可以在这种情况下使用声明。 使用BackgroundService 运行您的任务并在此类中注入声明原则。

        此示例适用于服务提供商帐户声明: 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; }
        }
        

        【讨论】:

        • 如何在这里使用声明?有样品吗?控制台应用程序和 Web api 需要哪些更改?4
        • @A_developer 检查编辑后的答案。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-04-26
        • 2013-01-09
        • 2021-02-18
        • 2018-05-14
        • 2020-07-20
        • 2020-04-30
        • 1970-01-01
        相关资源
        最近更新 更多