65.9K
CodeProject 正在变化。 阅读更多。
Home

JWT 安全 第二部分,安全 REST 服务

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2017年8月31日

CPOL

5分钟阅读

viewsIcon

22745

downloadIcon

773

了解如何创建 JWT 并将其与 WebApi、REST 和 MVC 一起使用,所有这些都使用 .NET Core 构建

引言

这是关于 JWT(JSON Web Token)的三篇博文中的第二篇。在第一篇博文中,我解释了如何创建 JWT。在这篇博文中,我们将保护 REST 服务并讨论以下主题:

  • 设置和配置 JWT
  • 默认安全
  • 设置 Swagger 以支持 JWT
  • 带 JWT 的 HTTP 客户端
  • 基于声明的访问
  • 基于声明的内容

通用 JWT 解决方案概述

在深入探讨细节之前,先回顾一下第一部分。JWT 发行者现在已运行。它根据用户凭据颁发 JWT。

设置和配置 JWT

下一步是保护 REST 服务,首先添加 `Microsoft.AspNetCore.Authentication.JwtBearer` 包。JWT 包需要在 *startup.cs* 中进行配置。首先,我们在“*appsettings.json*”中设置参数。

  ...
  "JwtTokenValidationSettings": {
    "ValidIssuer": "JwtServer",
    "ValidateIssuer": true,
    "SecretKey": "@everone:KeepitSecret!"
  },
  ...

`SecretKey` 值必须与 JWT 发行者服务器中的密钥匹配,否则用户将保持未认证状态,访问将被拒绝。DI(依赖注入)提供对配置的访问。

public void ConfigureServices(IServiceCollection services)
{
  ...
  // setup JWT Token validation
  services.Configure<JwtTokenValidationSettings>
  (Configuration.GetSection(nameof(JwtTokenValidationSettings)));
  services.AddSingleton<IJwtTokenValidationSettings, 
  JwtTokenValidationSettingsFactory>();
  ...

`JwtTokenValidationSettingsFactory` 实现接口,并具有 `TokenValidationParameters` 函数。

public class JwtTokenValidationSettings
  {
    public String ValidIssuer { get; set; }
    public Boolean ValidateIssuer { get; set; }

    public String ValidAudience { get; set; }
    public Boolean ValidateAudience { get; set; }

    public String SecretKey { get; set; }
  }

  public interface IJwtTokenValidationSettings
  {
    String ValidIssuer { get; }
    Boolean ValidateIssuer { get; }

    String ValidAudience { get; }
    Boolean ValidateAudience { get; }

    String SecretKey { get; }

    TokenValidationParameters CreateTokenValidationParameters();
  }

  public class JwtTokenValidationSettingsFactory : IJwtTokenValidationSettings
  {
    private readonly JwtTokenValidationSettings settings;

    public String ValidIssuer => settings.ValidIssuer;
    public Boolean ValidateIssuer => settings.ValidateIssuer;
    public String ValidAudience => settings.ValidAudience;
    public Boolean ValidateAudience => settings.ValidateAudience;
    public String SecretKey => settings.SecretKey;

    public JwtTokenValidationSettingsFactory
           (IOptions<JwtTokenValidationSettings> options)
    {
      settings = options.Value;
    }

    public TokenValidationParameters CreateTokenValidationParameters()
    {
      var result = new TokenValidationParameters
      {
        ValidateIssuer = ValidateIssuer,
        ValidIssuer = ValidIssuer,

        ValidateAudience = ValidateAudience,
        ValidAudience = ValidAudience,

        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)),

        RequireExpirationTime = true,
        ValidateLifetime = true,

        ClockSkew = TimeSpan.Zero
      };

      return result;
    }
  }

`TokenValidationParameters` 函数(顾名思义)返回 JWT 验证参数。这些参数在 *startup.cs* 中使用。

public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
   ...
   // Create TokenValidation factory with DI priciple
   var tokenValidationSettings = 
       app.ApplicationServices.GetService<IJwtTokenValidationSettings>();

   // Setup JWT security
   app.UseJwtBearerAuthentication(new JwtBearerOptions
   {
     AutomaticAuthenticate = true,
     AutomaticChallenge = false, // not sure
     TokenValidationParameters = 
          tokenValidationSettings.CreateTokenValidationParameters()
   });
  ...

就是这样!现在您可以使用 `[Authorize]` 属性来保护控制器或其操作。

[Authorize]
[Route("api/[controller]")]
public class EmployeeController : Controller
{
  ...

默认安全

使用 MVC 框架,您可以设置自定义过滤器。默认情况下,保护所有控制器只需在启动时设置一个自定义过滤器。

public void ConfigureServices(IServiceCollection services)
{
  ...
  // Secure all controllers by default
  var authorizePolicy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .Build();

  // Add Mvc with options
  services.AddMvc(config => 
  { config.Filters.Add(new AuthorizeFilter(authorizePolicy)); });
  ...

设置 Swagger 以支持 JWT

Swagger 设置需要对 JWT 进行一些调整。此调整允许您在测试期间添加 JWT。Swagger 将 JWT 添加到请求头中,REST 服务接收令牌并进行安全设置。

public void ConfigureServices(IServiceCollection services)
{
  ...     
  // Register the Swagger generator with JWT support
   services.AddSwaggerGen(c =>
   {
     // Tweak for JWT support
     c.AddSecurityDefinition("Bearer", new Swashbuckle.AspNetCore.Swagger.ApiKeyScheme()
     {
       Description = "Authorization format : Bearer {token}",
       Name = "Authorization",
       In = "header",
       Type = "apiKey"
      });

     c.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info 
                 { Title = "Resources  Api", Version = "v1" });
    });
  ...

其余的 Swagger 设置是正常的。

public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
                      ILoggerFactory loggerFactory)
{
  ...
  // Enable middleware to serve generated Swagger as a JSON endpoint.
  app.UseSwagger();

  // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), 
  // specifying the Swagger JSON endpoint.
  app.UseSwaggerUI(c =>
  {
    //c.ShowRequestHeaders();
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "Resources Api v1");
  });
  ...

是时候测试了

现在一切都已准备好通过 Swagger 进行测试。使用简单的 `LoginStatus` 函数,您可以轻松查看安全是否有效。

[Route("api/[controller]")]
public class EmployeeController : Controller
{
  ...
  [HttpGet("loginstatus")]
  public IActionResult LoginStatus()
  {
    var isAuthenticated = this.HttpContext.User.Identities.Any(u => u.IsAuthenticated);
    var email  = this.User.FindFirst(c => c.Type.ContainsEx("email"))?.Value;

   var result = new
    {
      IsAuthenticated = isAuthenticated,
      Email = email
    };
     return Ok(result);
  }

我启动了 Swagger 并尝试了 `loginstatus`。

响应代码是 **401**,表示“**未授权**”。我没有添加 JWT,所以这是正确的结果!让我们看看如果我们提供 JWT 给 Swagger 会发生什么。

首先,使用“employee@xyz.com”和“password”作为凭据从 JWT 发行者获取 JWT。

Swagger 中的值是“`Bearer `”,响应代码组合在一起。

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJKd3RTZXJ2ZXIiLCJ
zdWIiOiJlbXBsb3llZSIsImVtYWlsIjoiZW1wbG95ZWVAeHl6LmNvbSIsImp0aSI6IjZjNT
QzMzU1LWE4YjItNDI1MC05YTczLTdlZDFmMTNhYzIwZSIsImlhdCI6MTUwNDAxNzc1Miwia
HR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWlt
cy9yb2xlIjoiRW1wbG95ZWUiLCJuYmYiOjE1MDQwMTc3NTIsImV4cCI6MTUwNDAxOTU1Mn0.
0HYDe78_HZ1qVV_uIg5FMqVJujzQC3Pk5dHUX1YvwC0

现在已将 JWT 添加到 Swagger,我们再次尝试。

响应代码为 200(OK),响应体显示身份验证已设置,控制器可以读取声明。太棒了!

带 JWT 的 HTTP 客户端

我添加了一个小型控制台应用程序,它清楚地演示了 JWT 和 HTTP 客户端如何协同工作。

第一步是从发行者检索 JWT。

private static async Task<String> Login(String email, String password)
    {
      var url = "https://:49842/";
      var apiUrl = "/api/security/login/";

      using (var client = new HttpClient() { BaseAddress = new Uri(url) })
      {
        client.DefaultRequestHeaders.Accept.Add
               (new MediaTypeWithQualityHeaderValue("application/json"));

        var loginResource = new
        {
          Email = email,
          Password = password
        };

        var resourceDocument = JsonConvert.SerializeObject(loginResource);

        using (var content = new StringContent
              (resourceDocument, Encoding.UTF8, "application/json"))
        {
          using (var response = await client.PostAsync(apiUrl, content))
          {
            if (response.StatusCode == System.Net.HttpStatusCode.OK)
            {
              var result = await response.Content.ReadAsStringAsync();

              return result;
            }

            else return null;
          }
        }
      }
    }

使用 JWT,我们可以调用 REST 服务。

private static async Task<String> GetEmployees(String jwt)
    {
      var url = "https://:50249/";
      var apiUrl = $"/api/employee/";

      using (var client = new HttpClient() { BaseAddress = new Uri(url) })
      {
        client.BaseAddress = new Uri(url);
        client.DefaultRequestHeaders.Accept.Add
               (new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {jwt}");

        using (var response = await client.GetAsync(apiUrl))
        {
          if (response.StatusCode == System.Net.HttpStatusCode.OK)
            return await response.Content.ReadAsStringAsync();

          else return null;
        }
      }
    }

请注意 `login` 和 `GetEmployees` 的不同 URL,并仔细查看第 10 行。JWT 以与 Swagger 相同的方式添加到请求头中,“`Bearer <token>`”。

运行控制台应用程序

static void Main(String[] args)
    {
      Console.WriteLine("Request Token");
      var jwt = Login("employee@xyz.com", "password").Result;
      Console.WriteLine($"Token : {jwt}");
      Console.WriteLine("");

      var document = GetEmployees(jwt).Result;
      Console.WriteLine($"Employees: {document}");
      Console.WriteLine("");

      Console.WriteLine($"{Environment.NewLine}Ready, press key to close");
      Console.ReadKey();
    }

结果是

CBAC(基于声明的访问)

安全模型支持 CBAC,这与更传统的 RBAC(基于角色的访问)不同。我不会开始讨论 RBAC 或 CBAC 哪个更好。我认为基于声明的模型更通用,更适合与不同系统集成。对于 RBAC 的忠实拥护者,有好消息,您可以完美地将基于角色的系统集成到基于声明的环境中,并且还添加了基于角色的支持以实现向后兼容。

策略要求声明

安全模型引入了策略的概念。策略由一个或多个声明组成,并在启动时注册。

public void ConfigureServices(IServiceCollection services)
{
  ...
  services.AddAuthorization(options =>
      {
        options.AddPolicy("HR Only", policy => policy.RequireRole("HR-Worker"));
        options.AddPolicy("HR-Manager Only", 
        policy => policy.RequireClaim("CeoApproval", "true"));
      });
  ...

策略可以由角色或声明满足。复杂的组合也是可能的,但在此演示中不使用它们。您可以通过角色或策略进行授权。

[HttpGet("RoleBasedDemo")]
[Authorize(Roles = "HR-Worker")]
public IActionResult RoleBasedDemo()
{
  return Ok("I am role based");
}

[HttpDelete("{id}")]
[Authorize(Policy = "HR-Manager Only")]
public async Task<IActionResult> Delete(Guid id)
{
   ...
 }

但是,您不能将声明用作授权属性,必须使用带有声明的策略。

基于声明的内容

有时,资源的内容取决于客户端。例如,任何人都可以查看员工,但只有人力资源部门可以查看工资。您可以通过检查用户声明或角色(如果您愿意)来实现此功能。

 private String RemoveSensitiveFields(EmployeeResource resource)
    {
      // The dynamic Linq Select(params) works only on an IQueryable list
      // that's why 1 one item is added to a list
      var items = new List<EmployeeResource>(new[] { resource }).AsQueryable();

      // Find all property names
      var propertyNames = resource.GetType().GetProperties
      (BindingFlags.Public | BindingFlags.Instance).Select(p => p.Name).ToList();

      // Salary only visible to HR department
      if (!User.HasClaim("Department", "HR"))
        propertyNames.Remove(nameof(EmployeeResource.Salary));

      // Dynamic Linq supports dynamic selector
      var selector = $"new({String.Join(",", propertyNames)})";

      // Create dynamic object with authorized fields 
      var reducedResource = items.Select(selector).First();

      // Create JSON
      var result = JsonConvert.SerializeObject
      (reducedResource, new JsonSerializerSettings() 
      { Formatting = Formatting.Indented });

      return result;
    }

`RemoveSensitiveFields` 中的关键元素位于第 11 和 18 行。该函数依赖于“`System.Linq.Dynamic.Core`”包。该包为 `IQueryable` 项目提供了 `Select(params)` 扩展方法。属性名称通过反射提取,如果客户端不是 HR 部门的成员,则会删除“`Salary”字段。

控制台应用程序演示了基于声明的内容。

private static void ClaimBasedContentDemo()
{
  // each token represents a different identity
  var tokens = new String[]
  {
    Login("hrworker@xyz.com", "password").Result,
   Login("employee@xyz.com", "password").Result
  };

  foreach (var token in tokens)
  {
    Console.WriteLine(GetLoginStatus(token).Result);
    Console.WriteLine(GetEmployee(token, "jadds4z@1688.com").Result);
    Console.WriteLine("");
  }
}

输出

正如屏幕截图所示,只有 HR 成员才能查看工资。

结论

JWT(JSON Web Tokens)是保护 REST 服务的可靠方式。JWT 易于设置,并且与 .NET Core 集成良好。由于其自包含的设计,JWT 保持 REST 服务无状态,无需身份验证服务器。JWT 同时支持传统的基于角色的方法和更现代的基于声明的方法。

Visual Studio 启动项目

有时,Visual Studio 的启动项目会丢失,导致应用程序无法运行。右键单击解决方案并选择“**设置启动项目...**”。

并修复启动设置

根据您的需要选择 `WebApp` 或 `Jwt.ConsoleDemo`。

延伸阅读

版本

  • 1.0 2017 年 8 月 31 日:初始发布
  • 1.1 2017 年 9 月 5 日:源代码已升级为 .NET Core 2.0
© . All rights reserved.