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






4.96/5 (11投票s)
了解如何创建 JWT 并将其与使用 .NET Core 构建的所有 WebApi、REST 和 MVC 一起使用
引言
JWT (JSON Web Token) 作为一种安全网站和 REST 服务的标准,正变得越来越受欢迎。我将讨论如何在所有使用 .NET Core 构建的 REST 服务和 MVC Web 应用程序中实现 JWT 安全。我将 JWT 安全分为 3 篇博客
- 创建 JWT
- 使用 JWT 保护 REST 服务
- 使用 JWT 保护 Web 应用程序
这是三篇博客中的第一篇,我将从一个简单的 JWT 解释开始。
JWT 入门
JWT (JSON Web Tokens) 是一种开放的安全协议,用于在两个方之间安全地交换声明。服务器生成或颁发一个令牌,并用一个密钥对其进行签名。客户端也知道这个密钥,并且可以验证令牌的真实性。令牌包含用于身份验证和授权的声明。身份验证只是验证某人是否确实是他/她声称的那个人。授权是指用户被授予访问资源或执行特定任务的权限。例如,用户 A 可以查看付款,用户 B 可以执行付款。JWT 是自包含的。由于 JWT 是一种协议而不是框架,因此它适用于 .NET、Java、Python 等多种语言。JWT 通常通过将 JWT 添加到请求的标头中进行传输,但也可以用作 URL 中的参数。这种传输使 JWT 具有无状态性。
JWT 结构
JWT 有三个部分
- 标题
- Payload(载荷)
- 签名
各部分之间用点分隔。
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 安全概述
解决方案概述显示了三个独立的服务器: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