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

JWT 安全 第一部分 - 创建令牌

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (11投票s)

2017年8月31日

CPOL

8分钟阅读

viewsIcon

50985

downloadIcon

2838

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

引言

JWT (JSON Web Token) 作为一种安全网站和 REST 服务的标准,正变得越来越受欢迎。我将讨论如何在所有使用 .NET Core 构建的 REST 服务和 MVC Web 应用程序中实现 JWT 安全。我将 JWT 安全分为 3 篇博客

  1. 创建 JWT
  2. 使用 JWT 保护 REST 服务
  3. 使用 JWT 保护 Web 应用程序

这是三篇博客中的第一篇,我将从一个简单的 JWT 解释开始。

JWT 入门

JWT (JSON Web Tokens) 是一种开放的安全协议,用于在两个方之间安全地交换声明。服务器生成或颁发一个令牌,并用一个密钥对其进行签名。客户端也知道这个密钥,并且可以验证令牌的真实性。令牌包含用于身份验证和授权的声明。身份验证只是验证某人是否确实是他/她声称的那个人。授权是指用户被授予访问资源或执行特定任务的权限。例如,用户 A 可以查看付款,用户 B 可以执行付款。JWT 是自包含的。由于 JWT 是一种协议而不是框架,因此它适用于 .NET、Java、Python 等多种语言。JWT 通常通过将 JWT 添加到请求的标头中进行传输,但也可以用作 URL 中的参数。这种传输使 JWT 具有无状态性。

JWT 结构

JWT 有三个部分

  1. 标题
  2. Payload(载荷)
  3. 签名

各部分之间用点分隔。

aaaa.bbbb.cccc

标题

标头和有效载荷包含一个或多个键值对。标头包含令牌类型('typ')和哈希算法('alg')SHA256。

{
"alg":"HS256",
"typ":"JWT"
}

标头和有效载荷部分都经过 base64 编码,这使得标头部分

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload(载荷)

有效载荷部分是最有趣的部分,因为它包含所有声明。有三种声明类型:注册声明、公共声明和私有声明。

注册声明

注册声明是 JWT 标准的一部分,在所有实现中都具有相同的目的。为了使 JWT 大小保持较小,键的长度总是 3 个字符。以下是简短列表

  • iss 签发者 标识 JWT 的签发者。
  • sub 主题 标识 JWT 的主体(通常是用户)。
  • aud 受众 标识 JWT intended for 的接收者。
  • exp 过期时间 设置过期日期,过期后必须拒绝 JWT。
  • nbf not before。设置 JWT 之前不能使用的日期。
  • iat issued at。设置创建 JWT 的日期。
  • jti JWT 的唯一标识符。用于一次性令牌并防止令牌重放。

所有注册声明的日期都采用 Unix Epoch 日期格式,表示自 1970 年 1 月 1 日 UTC 时间以来的秒数。

公共声明

公共声明包含更通用的信息,例如“name”。公共声明也需要注册以防止与其他声明发生冲突。

私有声明

私有声明在签发者和受众之间达成一致。务必检查私有声明是否与现有声明没有冲突。声明“role”是我们稍后将使用的私有声明示例。

有效载荷示例

{
  "iss": "JwtServer",
  "sub": "hrmanager",
  "email": "hrmanager@xyz.com",
  "jti": "e971bd9c-7655-41d5-9c49-fabc054dc466",
  "iat": 1503922683,
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
    "Employee",
    "HR-Worker",
    "HR-Manager"
  ],
  "Department": [
    "HR",
    "HR"
  ],
  "nbf": 1503922683,
  "exp": 1503924483
}

将导致

eyJpc3MiOiJKd3RTZXJ2ZXIiLCJzdWIiOiJocm1hbmFnZXIiLCJlbWFpbCI6ImhybWFuYWdlck
B4eXouY29tIiwianRpIjoiZTk3MWJkOWMtNzY1NS00MWQ1LTljNDktZmFiYzA1NGRjNDY2Iiwi
aWF0IjoxNTAzOTIyNjgzLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMD
YvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsiRW1wbG95ZWUiLCJIUi1Xb3JrZXIiLCJIUi1NYW5h
Z2VyIl0sIkRlcGFydG1lbnQiOlsiSFIiLCJIUiJdLCJuYmYiOjE1MDM5MjI2ODMsImV4cCI6MTUwMzkyNDQ4M30

签名

到目前为止,JWT 没有什么安全可言。所有数据都经过 base64 编码,虽然不易读,但很容易将其解码为可读文本。签名就在这里起作用。通过签名,我们可以验证 JWT 的真实性并且未被篡改。签名是通过标头、有效载荷和一个密钥计算得出的。

var headerAndPayload = base64Encode(header) + "." + base64Encode(payload);

var secretkey = "@everone:KeepitSecret!";

signature = HMACSHA256(headerAndPayload, secretkey);

密钥是对称的,签发者和客户端都知道。不用说,要小心存储密钥的位置!

整合起来

下面的屏幕截图是在 https://jwt.net.cn/ 的帮助下构建的,您可以在那里测试和调试 JWT 声明。左侧窗格包含 JWT,另一个窗格显示提取的标头和有效载荷。如果添加密钥,该页面还会验证签名。

一般 JWT 安全概述

图 1 解决方案概述

解决方案概述显示了三个独立的服务器:Web 应用程序、RESTful 服务和 JWT 签发服务器。它们可以托管在一个服务器和一个项目中,但我为它们创建了三个项。这样,每个服务器的配置方式就更清楚了。由于 JWT 是自包含的,因此 JWT 签发者和 REST 服务之间不需要任何连接来验证 JWT 声明。

一般 JWT 流

基本的 JWT 流非常简单

  • 用户在 Web 应用程序上输入登录凭据。
  • Web 应用程序将登录凭据发送给 JWT 签发者,并请求 JWT 声明。
  • JWT 签发者使用用户数据库验证登录凭据。
  • JWT 签发者根据用户数据库中的声明和角色创建 JWT,并添加“exp”(过期)声明以限制生命周期(30 分钟)。
  • JWT 签发者将 JWT 发送给 Web 应用程序。
  • Web 应用程序接收 JWT 并将其存储在身份验证 cookie 中。
  • Web 应用程序验证 JWT 并解析有效载荷以进行身份验证和授权。
  • Web 应用程序将 JWT 添加到 REST 服务调用中。

优点和缺点

优点

  • 相对简单。无论您选择什么,安全从来都不是一件容易的事。JWT 的设计非常巧妙,结合 .NET 库可以轻松实现 JWT。
  • REST 服务确实如其所应那样是无状态的。在大多数情况下,安全性会添加某种会话管理来进行身份验证。
  • 无状态使之可扩展。如果您需要更多服务器来处理工作负载,则无需在所有服务器之间共享会话。这使得扩展更容易,并且出错的可能性更小。
  • 可在不同服务之间使用。JWT 是自包含的,服务可以在不访问用户数据库的情况下进行授权。
  • JWT 提供了临时授权提升的精妙选项。在用户会话期间可以添加或删除声明。例如,您可以向用户添加一个声明,表示他已成功通过双重身份验证来执行付款。在成功执行付款后,可以删除该声明。这样,就不需要创建特殊的方法来跟踪用户状态。

缺点

  • JWT 没有内置的滑动过期功能,尽管您可以自己构建。
  • 密钥非常重要。如果密钥被盗或泄露,安全性将受到严重损害。

创建 JWT 签发者项目

主要任务是根据用户凭据提供 JWT 声明。该项目是一个标准的 **MVC** 应用程序,具有 **Individual User Accounts** 作为身份验证。

Individual User Accounts Authentication 用于保护网站安全,并方便访问用户及其角色和声明。我添加了 Microsoft.AspNetCore.Authentication.JwtBearer 包来进行实际的 JWT 创建。由于 **不** 使用 JWT 来保护此 Web 站点调用者,因此在启动时无需注册 JwtBearer 服务。仅在启动时配置 JWT 参数。

{
...
  "JwtIssuerSettings": {
    "Issuer": "JwtServer",
    "ValidFor": 30, // minutes
    "SecretKey": "@everone:KeepitSecret!"
  },
...

DI (Dependency Injection) 模式应用于配置。JwtIssuerSettings 类映射到 appsettings.json 中的 config 部分 JwtIssuerSettings,而 JwtIssuerFactory 类创建 IJwtIssuerOptions 接口的实例。

 public void ConfigureServices(IServiceCollection services)
    {
      ...
      // setup JWT parameters
      services.Configure<JwtIssuerSettings>
               (Configuration.GetSection(nameof(JwtIssuerSettings)));
      services.AddTransient<IJwtIssuerOptions, JwtIssuerFactory>();
      ...

它们已添加到服务集合中,现在可以作为控制器构造函数中的参数使用。

创建 JWT 声明

JwtIssuerController 控制器中的 Login 函数创建 JWT 声明。过程非常直接

  • 查找用户。
  • 检查密码。
  • 创建签发者、主题、电子邮件、唯一 ID 和 IssuedAt 声明。
  • 从存储中收集用户角色(声明)。
  • 根据配置参数和密钥创建 JWT。
  • 将令牌发送给调用者。
namespace Security.Controllers
{
  [AllowAnonymous]
  [Route("api/security")]
  public class JwtIssuerController : Controller
  {
    private readonly IJwtIssuerOptions JwtOptions;
    private readonly UserManager<ApplicationUser> UserManager;
    private readonly SignInManager<ApplicationUser> SignInManager;
    private readonly RoleManager<IdentityRole> RoleManager;

    public JwtIssuerController(IJwtIssuerOptions jwtOptions,
      UserManager<ApplicationUser> userManager,
      SignInManager<ApplicationUser> signInManager,
      RoleManager<IdentityRole> roleManager)
    {
      JwtOptions = jwtOptions;
      UserManager = userManager;
      SignInManager = signInManager;
      RoleManager = roleManager;
    }

    [HttpPost(nameof(Login))]
    public async Task<IActionResult> Login([FromBody] LoginResource resource)
    {
      if (resource == null)
        return BadRequest("Login resource must be asssigned");

      var user = await UserManager.FindByEmailAsync(resource.Email);

      if (user == null || (!(await SignInManager.PasswordSignInAsync
                          (user, resource.Password, false, false)).Succeeded))
        return BadRequest("Invalid credentials");

      String result = await CreateJwtTokenAsync(user);

      // Token is created, we can sign out
      await SignInManager.SignOutAsync();

      return Ok(result);
    }

    /// <summary>
    /// Fetch user roles and claims from storage
    /// </summary>
    /// <param name="user">application user</param>
    /// <returns>JWT token</returns>
    private async Task<String> CreateJwtTokenAsync(ApplicationUser user)
    {
      // Create JWT claims
      var claims = new List<Claim>(new[]
      {
        // Issuer
        new Claim(JwtRegisteredClaimNames.Iss, JwtOptions.Issuer),   

        // UserName
        new Claim(JwtRegisteredClaimNames.Sub, user.UserName),       

        // Email is unique
        new Claim(JwtRegisteredClaimNames.Email, user.Email),        

        // Unique Id for all Jwt tokes
        new Claim(JwtRegisteredClaimNames.Jti, await JwtOptions.JtiGenerator()), 

        // Issued at
        new Claim(JwtRegisteredClaimNames.Iat, 
        JwtOptions.IssuedAt.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64) 
      });

      // Add userclaims from storage
      claims.AddRange(await UserManager.GetClaimsAsync(user));

      // Add user role, they are converted to claims
      var roleNames = await UserManager.GetRolesAsync(user);
      foreach (var roleName in roleNames)
      {
        // Find IdentityRole by name
        var role = await RoleManager.FindByNameAsync(roleName);
        if (role != null)
        {
          // Convert Identity to claim and add 
          var roleClaim = new Claim(ClaimTypes.Role, role.Name, 
                                    ClaimValueTypes.String, JwtOptions.Issuer);
          claims.Add(roleClaim);

          // Add claims belonging to the role
          var roleClaims = await RoleManager.GetClaimsAsync(role);
          claims.AddRange(roleClaims);
        }
      }

      // Prepare Jwt Token
      var jwt = new JwtSecurityToken(
          issuer: JwtOptions.Issuer,
          audience: JwtOptions.Audience,
          claims: claims,
          notBefore: JwtOptions.NotBefore,
          expires: JwtOptions.Expires,
          signingCredentials: JwtOptions.SigningCredentials);

      // Serialize token
      var result = new JwtSecurityTokenHandler().WriteToken(jwt);

      return result;
    }

测试数据

在启动时,会创建一个内存数据库。它包含三个用户和三个角色,模拟人力资源部门。

角色

  • Employee - 任何公司成员
  • HR-Worker - 任何人力资源部成员
  • HR-Manager - 当然是人力资源部主管

用户

  • employee@xyz.com
  • hrworker@xyz.com
  • hrmanager@xyz.com

命名空间 Microsoft.AspNetCore.Identity 包含 RoleManager<IdentityRole>,无需显式配置即可使用。您在示例或文档中很少看到它。这是一个有点错失的机会,因为该类对于管理系统中的角色非常有用。

  // This method gets called by the runtime. 
  // Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, 
           IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
     ...
      // Fill empty inmemory database during development
      if (env.IsDevelopment())
        app.InitDb();
     ...
  public static class InitDbExtensions
  {
    public static IApplicationBuilder InitDb(this IApplicationBuilder app)
    {
      var roleManager = 
          app.ApplicationServices.GetService<RoleManager<IdentityRole>>();
      var userManager = 
          app.ApplicationServices.GetService<UserManager<ApplicationUser>>();

      if (userManager.Users.Count() == 0)
      {
        Task.Run(() => InitRoles(roleManager)).Wait();
        Task.Run(() => InitUsers(userManager)).Wait();
      }

      return app;
    }

    private static async Task InitRoles(RoleManager<IdentityRole> roleManager)
    {
      var role = new IdentityRole("Employee");
      await roleManager.CreateAsync(role);

      role = new IdentityRole("HR-Worker");
      await roleManager.CreateAsync(role);
      await roleManager.AddClaimAsync(role, new Claim("Department", "HR"));

      role = new IdentityRole("HR-Manager");
      await roleManager.CreateAsync(role);
      await roleManager.AddClaimAsync(role, new Claim("Department", "HR"));
    }

    private static async Task InitUsers(UserManager<ApplicationUser> userManager)
    {
      var user = new ApplicationUser() { UserName = "employee", 
                                         Email = "employee@xyz.com" };
      await userManager.CreateAsync(user, "password");
      await userManager.AddToRoleAsync(user, "Employee");

      user = new ApplicationUser() 
             { UserName = "hrworker", Email = "hrworker@xyz.com" };
      await userManager.CreateAsync(user, "password");
      await userManager.AddToRoleAsync(user, "Employee");
      await userManager.AddToRoleAsync(user, "HR-Worker");

      user = new ApplicationUser() 
             { UserName = "hrmanager", Email = "hrmanager@xyz.com" };
      await userManager.CreateAsync(user, "password");
      await userManager.AddToRoleAsync(user, "Employee");
      await userManager.AddToRoleAsync(user, "HR-Worker");
      await userManager.AddToRoleAsync(user, "HR-Manager");
    }
  }
}

测试 JWT 声明

我通过添加 Swashbuckle.AspNetCore 包来添加 Swagger 以进行测试。您可以 在此处 阅读更多关于如何配置 Swagger 的信息。简而言之,它归结为这一点

public void ConfigureServices(IServiceCollection services)
    {
     ...
      // Register the Swagger generator, defining one or more Swagger documents
      services.AddSwaggerGen(c =>
      {
        c.AddSecurityDefinition("Bearer", new ApiKeyScheme()
        {
          Description = "Authorization format : Bearer {token}",
          Name = "Authorization",
          In = "header",
          Type = "apiKey"
        });

        c.SwaggerDoc("v1", new Info { Title = "Security Api", Version = "v1" });
      });
     ...
 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.SwaggerEndpoint("/swagger/v1/swagger.json", "Security Api v1");
      });
    ...

现在可以在 https://:49842/swagger/ 上测试 Swagger。

我们可以在 https://jwt.net.cn/ 上测试响应。

一切看起来都很顺利,我们可以开始保护 REST 服务了。

Visual Studio 启动项目

有时,Visual Studio 启动项目会丢失并阻止应用程序运行。右键单击解决方案,然后选择“Set Startup Projects...”。

并修复启动设置

结论

这篇博客演示了如何设置 JWT (JSON Web Token) 签发者。无状态、自包含、可扩展和其他功能使 JWT 成为一项明智的设计。借助集成 JWT 与 .NET Core 的软件包,设置工作量很小。

下一篇文章:JWT 安全第二部分 - 保护 REST 服务

延伸阅读

版本

  • 1.0 2017 年 8 月 31 日:首次发布
  • 1.1 2017 年 9 月 5 日:源代码已升级为 .NET Core 2.0
JWT 安全第一部分 - 创建令牌 - CodeProject - 代码之家
© . All rights reserved.