前言:JWT实现登录的流程
- 客户端向服务器端发送用户名、密码等请求登录。
- 服务器端校验用户名、密码,如果校验成功,则从数据库中取出这个用户的ID、角色等用户相关信息。
- 服务器端采用只有服务器端才知道的密钥来对用户信息的 JSON 字符串进行签名,形成签名数据。
- 服务器端把用户信息的 JSON 字符串和签名拼接到一起形成JWT,然后发送给客户端。
- 客户端保存服务器端返回的 JWT,并且在客户端每次向服务器端发送请求的时候都带上这个 JWT。
- 每次服务器端收到浏览器请求中携带的 JWT 后,服务器端用密钥对JWT的签名进行校验,如果校验成功,服务器端则从 JWT 中的 JSON 字符串中读取出用户的信息。
Step By Step 步骤
- 创建一个 Asp.Net Core WebApi 项目
- 引用以下 Nuget 包:
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
3.打开 appsettings.json 文件,配置数据库连接字符串和JWT的密钥、过期时间
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "Default": "Server=(localdb)\\mssqllocaldb;Database=IdentityTestDB;Trusted_Connection=True;MultipleActiveResultSets=true" }, "JWT": { "SigningKey": "fasdfad&9045dafz222#fadpio@0232", "ExpireSeconds": "86400" } }
4.创建JWT配置实体类 JWTOptions
public class JWTOptions { public string SigningKey { get; set; } public int ExpireSeconds { get; set; } }
5.打开 Program.cs 文件,在 builder.Build 之前,编写代码对 JWT 进行配置
// 注入 JWT 配置 services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); // 注入 JwtBearer 配置 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>(); byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); x.TokenValidationParameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = secKey }; });
6.打开 Program.cs 文件,在 app.UseAuthorization 之前,添加身份验证中间件
// 使用 Authentication 中间件,放在 UseAuthorization 之前 app.UseAuthentication();
7.创建继承 IdentityRole 的 User 和 Role 实体类
using Microsoft.AspNetCore.Identity; public class User: IdentityUser<long> { public DateTime CreationTime { get; set; } public string? NickName { get; set; } } public class Role: IdentityRole<long> { }
8.创建继承 IdentityDbContext 的上下文类
using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; public class IdDbContext: IdentityDbContext<User, Role, long> { public IdDbContext(DbContextOptions<IdDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } }
9.(可选)如果数据表还没创建,执行数据库迁移命令
10.创建登录请求的参数实体类 LoginRequest
public record LoginRequest(string UserName, string Password);
11.打开登录请求控制器,编写 Login API,在其中创建 JWT
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace ASPNETCore_JWT1.Controllers { [ApiController] [Route("[controller]/[action]")] public class Test1Controller : ControllerBase { private readonly UserManager<User> userManager; //注入 UserManager public Test1Controller(UserManager<User> userManager) { this.userManager = userManager; } // 生成 JWT private static string BuildToken(IEnumerable<Claim> claims, JWTOptions options) { DateTime expires = DateTime.Now.AddSeconds(options.ExpireSeconds); byte[] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature); var tokenDescriptor = new JwtSecurityToken( expires: expires, signingCredentials: credentials, claims: claims); var result = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); return result; } // 在方法中注入 IOptions<JWTOptions> // 只需要返回 JWT Token 即可,其它的身份验证中间件会处理 [HttpPost] public async Task<IActionResult> Login( LoginRequest req, [FromServices] IOptions<JWTOptions> jwtOptions) { string userName = req.UserName; string password = req.Password; var user = await userManager.FindByNameAsync(userName); if (user == null) { return NotFound($"用户名不存在{userName}"); } if (await userManager.IsLockedOutAsync(user)) { return BadRequest("LockedOut"); } var success = await userManager.CheckPasswordAsync(user, password); if (!success) { return BadRequest("Failed"); } var claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); claims.Add(new Claim(ClaimTypes.Name, user.UserName)); var roles = await userManager.GetRolesAsync(user); foreach (string role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } var jwtToken = BuildToken(claims, jwtOptions.Value); return Ok(jwtToken); } } }
12.打开其它控制器,在类上添加 [Authorize] 这个特性
using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using Microsoft.AspNetCore.Authorization; namespace ASPNETCore_JWT1.Controllers { // [Authorize] 特性标识此控制器的方法需要身份授权才能访问 // 授权中间件会处理其它的 [ApiController] [Route("[controller]/[action]")] [Authorize] public class Test2Controller : Controller { [HttpGet] public IActionResult Hello() { // ControllerBase中定义的ClaimsPrincipal类型的User属性代表当前登录用户的身份信息 // 可以通过ClaimsPrincipal的Claims属性获得当前登录用户的所有Claim信息 // this.User.Claims string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value; string userName = this.User.FindFirst(ClaimTypes.Name)!.Value; IEnumerable<Claim> roleClaims = this.User.FindAll(ClaimTypes.Role); string roleNames = string.Join(",", roleClaims.Select(c => c.Value)); return Ok($"id={id},userName={userName},roleNames ={roleNames}"); } } }
13.打开 Program.cs 文件,配置 Swagger,支持发送 Authorization 报文头
// 配置 Swagger 支持 Authorization builder.Services.AddSwaggerGen(c => { var scheme = new OpenApiSecurityScheme() { Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'", Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Authorization" }, Scheme = "oauth2", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey }; c.AddSecurityDefinition("Authorization", scheme); var requirement = new OpenApiSecurityRequirement(); requirement[scheme] = new List<string>(); c.AddSecurityRequirement(requirement); });
启动项目,进行测试
- 首先访问/Test1/Login,获取 JWT Token,复制下这个值
- 然后访问/Test2/Hello,不带 JWT Token,将收到 401 信息
- 在 Swagger 上的 Authorization 输入 JWT Token,重新访问/Test2/Hello,将返回正确的结果
- 如果是在 Postman 等第三方,要在 Header 上加上参数 Authorization=bearer {JWT Token}
附录:完整的 Program 代码(重点注意代码中的注释)
using Microsoft.AspNetCore.Identity; using Microsoft.OpenApi.Models; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Text; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); // 配置 Swagger 支持 Authorization builder.Services.AddSwaggerGen(c => { var scheme = new OpenApiSecurityScheme() { Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'", Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Authorization" }, Scheme = "oauth2", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey }; c.AddSecurityDefinition("Authorization", scheme); var requirement = new OpenApiSecurityRequirement(); requirement[scheme] = new List<string>(); c.AddSecurityRequirement(requirement); }); var services = builder.Services; // 注册数据库服务 services.AddDbContext<IdDbContext>(opt => { string connStr = builder.Configuration.GetConnectionString("Default")!; opt.UseSqlServer(connStr); }); // 注册数据保护服务 services.AddDataProtection(); // 注册 IdentityCore 服务 services.AddIdentityCore<User>(options => { options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequiredLength = 6; options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider; options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider; }); // 注入UserManager、RoleManager等服务 var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services); idBuilder.AddEntityFrameworkStores<IdDbContext>() .AddDefaultTokenProviders() .AddRoleManager<RoleManager<Role>>() .AddUserManager<UserManager<User>>(); // 注入 JWT 配置 services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); // 注入 JwtBearer 配置 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { var jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>(); byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); x.TokenValidationParameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = secKey }; }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // 使用 Authentication 中间件,放在 UseAuthorization 之前 app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
总结
- 如果其中某个操作方法不想被验证,可以在这个操作方法上添加 [AllowAnonymous] 特性
- 对于客户端获得的 JWT,在前端项目中,可以把令牌保存到 Cookie、LocalStorage 等位置,从而在后续请求中重复使用,而对于移动App、PC客户端,可以把令牌保存到配置文件中或者本地文件数据库中。当执行【退出登录】操作的时候,我们只要在客户端本地把 JWT 删除即可。
- 在发送请求的时候,只要按照 HTTP 的要求,把 JWT 按照 “Bearer {JWT Token}” 格式放到名字为 Authorization 的请求报文头中即可
- 从 Authorization 中取出令牌,并且进行校验、解析,然后把解析结果填充到 User 属性中,这一切都是 ASP.NET Core 完成的,不需要开发人员自己编写代码