前言: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 -
打开 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" } }
-
创建JWT配置实体类 JWTOptions
public class JWTOptions { public string SigningKey { get; set; } public int ExpireSeconds { get; set; } }
-
打开 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 }; });
-
打开 Program.cs 文件,在 app.UseAuthorization 之前,添加身份验证中间件
// 使用 Authentication 中间件,放在 UseAuthorization 之前 app.UseAuthentication();
-
创建继承 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> { }
-
创建继承 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); } }
-
(可选)如果数据表还没创建,执行数据库迁移命令
- 参考前文《ASP.NET Core 标识(Identity)框架系列(一):如何使用标识(Identity)创建用户和角色?》
-
创建登录请求的参数实体类 LoginRequest
public record LoginRequest(string UserName, string Password);
-
打开登录请求控制器,编写 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); } } }
-
打开其它控制器,在类上添加 [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}"); } } }
-
打开 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
附录:完整的 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 完成的,不需要开发人员自己编写代码
往期精彩
- C# 静态类,高手不想让你知道的 15 个真相
- 封装一个 C# 范围判断函数,从此告别重复编写范围判断代码的烦恼
- 用 C# Stopwatch 计时,让代码性能飞起来!
- 轻装上阵,Visual Studio LocalDB:.NET 程序员的本地数据库神器
- 封装一个C#万能基础数据类型转换器,一招解决所有基础类型转换烦恼
- 闲话 .NET(7):.NET Core 能淘汰 .NET FrameWork 吗?
- 常用的 4 种 ORM 框架(EF Core,SqlSugar,FreeSql,Dapper)对比总结
- C# AutoMapper 10个常用方法总结
- C# 7个方法比较两个对象是否相等
- C# 去掉字符串最后一个字符的 4 种方法
来源链接:https://www.cnblogs.com/JackyGz/p/18591554
© 版权声明
本站所有资源来自于网络,仅供学习与参考,请勿用于商业用途,否则产生的一切后果将由您(转载者)自己承担!
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
如有侵犯您的版权,请及时联系3500663466#qq.com(#换@),我们将第一时间删除本站数据。
THE END
暂无评论内容