这里写自定义目录标题
- 介绍
-
- 目录
- 工具
- JWT 定义
- JWT结构
- 在 .NET 应用程序中生成 JWT
- 总结
介绍
本文将给出 JSON Web Tokens 的定义,并展示它们如何在 .Net 应用程序中使用。本文将参考文章 使用 .NET 5.0 Web API 和 Microsoft SQL Server 构建待办事项列表应用程序 ,其中我展示了如何使用 .NET 5.0 Web API 构建待办事项列表的后端。本文将描述 JWT 的结构,并解释如何在 .NET 应用程序中生成 JWT。
目录
- 工具
- JWT 定义
- JWT结构
- 在 .NET 应用程序中生成 JWT
- 结论
工具
- Visual Studio 2019
JWT 定义
JSON Web Token (JWT) 是一种开放标准,可在空间受限的环境中安全传输声明。这允许 JWT 通过 URL、POST 方法或 HTTP 标头传输。在本文中,我们将通过 POST 方法发送 JWT。
JWT 是一个标准,因为 2011 年成立的 JSON 对象签名和加密组 (JOSE) “标准化了完整性保护(签名和 MAC)和加密的机制以及密钥和算法标识符的格式,以支持使用 JSON 的协议的安全服务”。JOSE 小组建立了以 JSON 格式作为数字签名令牌安全传输声明的规范。
声明是关于对象的断言语句。在本文中,我将展示名为“name”的声明断言登录用户是“Sakhile-admin”。在这种情况下,我们的对象是用户。共有三种类型的索赔:注册索赔、公共索赔和私人索赔。在待办事项应用程序中,我们使用了注册声明和我定义的两个私有声明。
JWT结构
JWT 具有三个不同的部分,即标头、有效负载和签名。标头通常包含两个声明,用于对令牌进行签名的算法和令牌的类型。但是,只有算法声明是强制性的。有不同类型的算法用于对令牌进行签名,例如RS256、RS256等。如果没有使用算法,则声明的断言为无,并且此 JWT 是不安全的。我将展示如何创建一个不安全的 JWT。不安全的 JWT 没有签名,一些 JWT 验证库可以将这些令牌解释为有效的令牌,这可能允许某人修改令牌负载。TokenValidationParameters 是 JwtBearerOptions 的一个选项,不认为未签名的令牌有效。我将展示 .NET 应用程序将未签名的令牌视为无效并返回 401 未经授权的状态代码。
负载包含关于对象的声明。这是添加用户数据的地方。待办事项应用程序中使用的注册声明之一是过期时间,它显示令牌被视为无效的确切时刻。JWT 设置为 3 小时后到期。短期令牌有助于缓解跨站点请求伪造 (CSRF) 攻击。但是由于to-do应用的前端使用localStorage来存储token而不是Cookies,所以CSRF是不可能的。
签名是使用 Base64Url 编码的标头和有效载荷、用户提供的秘密字符串和标头中描述的算法形成的。签名验证令牌中包含的数据在令牌从发行者发送到受众以及从受众发送到发行者时没有被更改。
在 .NET 应用程序中生成 JWT
要生成 JWT,必须在我们简单的待办事项列表应用程序中编辑三个文件。startup.cs 文件、appsettings.json 文件和 AthenticationController.cs 文件。
appsettings.cs 文件包含 JWT 凭据。出于安全原因,凭据存储在此处。秘密字符串用于对 JWT 进行签名。
{
"Logging": {
"LogLevel": {
"Default": "Information","Microsoft": "Warning","Microsoft.Hosting.Lifetime": "Information"}},"AllowedHosts": "*","ConnectionStrings": {
"SQLConnection": "Server=.;Database=ToDoDB;Trusted_Connection=True;Integrated Security=true"},"JWT": {
"ValidAudience": "http: //localhost:4200","ValidIssuer": "http://localhost:24288","Secret": "MySecretStringMuuustBeVeeeeeeeeeeryLooooooooOng"}
}
startup.cs 文件。在 ConfigureServices 方法中,添加了 AddAuthentication 服务来验证令牌。在 Configure 方法中,添加了 app.UseAuthentication() 以在此应用程序中使用身份验证。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;namespace ToDoAPI
{
public class Startup{
public Startup(IConfiguration configuration){
Configuration = configuration;}public IConfiguration Configuration {
get; }// This method gets called by the runtime. Use this method to add services to the container.public void ConfigureServices(IServiceCollection services){
services.AddControllers();services.AddSwaggerGen(c =>{
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "ToDoAPI", Version = "v1" });});services.AddDbContext<ApplicationDbContext>(options =>options.UseSqlServer(Configuration.GetConnectionString("SQLConnection")));services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();services.AddAuthentication(options =>{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(options =>{
options.SaveToken = true;options.RequireHttpsMetadata = false;options.TokenValidationParameters = new TokenValidationParameters(){
ValidateIssuer = true,ValidateAudience = true,ValidateLifetime = true,ValidAudience = Configuration["JWT:ValidAudience"],ValidIssuer = Configuration["JWT:ValidIssuer"],// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)ClockSkew = TimeSpan.Zero,IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))};});services.AddCors();}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.public void Configure(IApplicationBuilder app, IWebHostEnvironment env){
app.UseCors(options =>options.WithOrigins("http://localhost:4200").AllowAnyMethod().AllowAnyHeader());if (env.IsDevelopment()){
app.UseDeveloperExceptionPage();app.UseSwagger();app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ToDoAPI v1"));}app.UseAuthentication();app.UseRouting();app.UseAuthorization();app.UseEndpoints(endpoints =>{
endpoints.MapControllers();});}}
}
要生成安全的 JWT,必须在 athenticationController.cs 文件中的第 33 行和第 70 行之间编写 Login 方法,如下所示。第 44 行和第 50 行中的代码显示在 JWT 中定义和使用了名称和角色私有声明。第 60 行显示了 authSigningKey 秘密和 SecurityAlgorithms.HmacSha256 算法用于对 JWT 进行签名。这产生了一个签名的 JWT。
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using ToDoAPI.Authentication;
using ToDoAPI.Models;namespace ToDoAPI.Controllers
{
[Route("api/[controller]")][ApiController]public class AuthenticationController : ControllerBase{
private readonly UserManager<ApplicationUser> userManager;private readonly RoleManager<IdentityRole> roleManager;private readonly IConfiguration _configuration;public AuthenticationController(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IConfiguration configuration){
this.userManager = userManager;this.roleManager = roleManager;_configuration = configuration;}[HttpPost][Route("login")]public async Task<IActionResult> Login([FromBody] LoginModel model){
var user = await userManager.FindByNameAsync(model.Username);if (user != null && await userManager.CheckPasswordAsync(user, model.Password)){
var userRoles = await userManager.GetRolesAsync(user);var authClaims = new List<Claim>{
new Claim("name", user.UserName),new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),};foreach (var userRole in userRoles){
authClaims.Add(new Claim("role", userRole));}var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));var token = new JwtSecurityToken(issuer: _configuration["JWT:ValidIssuer"],audience: _configuration["JWT:ValidAudience"],expires: DateTime.Now.AddMinutes(120),claims: authClaims,signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256));return Ok(new{
token = new JwtSecurityTokenHandler().WriteToken(token),expiration = token.ValidTo});}return Unauthorized();}[HttpPost][Route("register")]public async Task<IActionResult> Register([FromBody] RegisterModel model){
var userExists = await userManager.FindByNameAsync(model.Username);if (userExists != null){
return StatusCode(StatusCodes.Status500InternalServerError, new Response {
Status = "Error", Message = "User already exists!" });};ApplicationUser user = new ApplicationUser(){
Email = model.Email,SecurityStamp = Guid.NewGuid().ToString(),UserName = model.Username};var 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." });}return Ok(new Response {
Status = "Success", Message = "User created successfully" });}[HttpPost][Route("register-admin")]public async Task<IActionResult> RegisterAdmin([FromBody] RegisterModel model){
var userExists = await userManager.FindByNameAsync(model.Username);if (userExists != null){
return StatusCode(StatusCodes.Status500InternalServerError, new Response {
Status = "Error", Message = "User already exists!" });};ApplicationUser user = new ApplicationUser(){
Email = model.Email,SecurityStamp = Guid.NewGuid().ToString(),UserName = model.Username};var 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." });}if (!await roleManager.RoleExistsAsync(UserRoles.Admin)){
await roleManager.CreateAsync(new IdentityRole(UserRoles.Admin));}if (!await roleManager.RoleExistsAsync(UserRoles.User)){
await roleManager.CreateAsync(new IdentityRole(UserRoles.User));}if (await roleManager.RoleExistsAsync(UserRoles.Admin)){
await userManager.AddToRoleAsync(user, UserRoles.Admin);}return Ok(new Response {
Status = "Success", Message = "User created successfully" });}}
}
生成的签名 JWT 如下所示。请注意,令牌具有三个编码部分。“alg”声明是 HS256,它显示了用于签署此 JWT 的算法。
要生成不安全的 JWT,必须更改 Login 方法,如下所示。
[HttpPost]
[Route("login")]
public async Task < IActionResult > Login([FromBody] LoginModel model) {
var user = await userManager.FindByNameAsync(model.Username);if (user != null && await userManager.CheckPasswordAsync(user, model.Password)) {
var userRoles = await userManager.GetRolesAsync(user);var authClaims = new List < Claim > {
new Claim("name", user.UserName),new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),};foreach(var userRole in userRoles) {
authClaims.Add(new Claim("role", userRole));}var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));var token = new JwtSecurityToken(issuer: _configuration["JWT:ValidIssuer"], audience: _configuration["JWT:ValidAudience"], expires: DateTime.Now.AddMinutes(120), claims: authClaims);return Ok(new {
token = new JwtSecurityTokenHandler().WriteToken(token),expiration = token.ValidTo});}return Unauthorized();
}
生成的未签名 JWT 如下所示。请注意,令牌只有两个编码部分,编码后的令牌末尾有一个点。此外,“alg”声明是没有的。
总结
在本文中,我描述了令牌及其结构。我展示了如何在 .NET 应用程序中生成安全和不安全的 JWT。我描述了一些具有不安全令牌的应用程序可能会导致跨站点请求伪造 (CSRF) 攻击。JWT 是一种标准,它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。签名的 JWT 可用于在应用程序中进行身份验证和授权。我已经展示了可以在 .NET 应用程序中轻松实现 JWT 身份验证,并且只有经过签名的 JWT 才能用于访问应用程序的端点。