AspNet Core: Jwt 身份认证
AspNet Core: Jwt 身份认证
资源服务器
创建项目
新建一个“AspNetCore WebApi” 项目,名为:DotNet.WebApi.Jwt.ApiResources
依赖包
添加依赖包:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />
添加API
新建控制器 Controllers/StudentController.cs:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DotNet.WebApi.Jwt.ApiResources.Controllers
{
//[Authorize(Policy = "OnlyRead")]
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
[Authorize(Policy = "ReadWrite")]
[HttpGet("GetStudents")]
public ActionResult<dynamic> GetStudents()
{
return new List<dynamic>()
{
new {Id=1,Name="张三",Age=21 },
new {Id=2,Name="李四",Age=22 },
new {Id=3,Name="王五",Age=23 },
};
}
[Authorize(Policy = "OnlyRead")]
[HttpGet("GetStudent")]
public ActionResult<dynamic> GetStudent()
{
return new List<dynamic>()
{
new { Id = 10, Name = "钱六", Age = 19 }
};
}
}
}
Program
将 Program.cs 修改为:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace DotNet.WebApi.Jwt.ApiResources
{
public class Program
{
public static void Main(string[] args)
{
Console.Title = "API资源服务器";
var builder = WebApplication.CreateBuilder(args);
//设置跨域
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
//允许任何来源访问。
builder.AllowAnyOrigin().AllowAnyHeader();
//将isexpired头添加到策略中。
builder.WithExposedHeaders(new string[] { "isexpired" });
});
});
//配置策略授权
builder.Services.AddAuthorization(options => {
options.AddPolicy("OnlyRead", policy => policy.RequireRole("Read").Build());
});
//配置JWT。
builder.Services.AddAuthentication(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(j =>
{
j.RequireHttpsMetadata = false;
j.SaveToken = true;
j.TokenValidationParameters = new TokenValidationParameters
{
//是否调用对签名securityToken的SecurityKey进行验证
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("798654167464654646")),//签名秘钥
ValidateIssuer = true,//是否验证颁发者
ValidIssuer = "dotnet-jwt",//颁发者
ValidateAudience = true, //是否验证接收者
ValidAudience = "StudentAPI",//接收者
ValidateLifetime = true,//是否验证失效时间
};
//捕获Token过期事件
j.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
//出现此类异常。
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
//在响应头中添加isexpired:true键值对。
context.Response.Headers.Add("isexpired", "true");
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
if (app.Environment.IsProduction())
{
//生产环境端口号
app.Urls.Add("https://*:6002");
}
app.UseHttpsRedirection();
app.UseCors(); //启用跨域
app.UseAuthentication(); //身份验证
app.UseAuthorization(); //授权
app.MapControllers();
app.Run();
}
}
}
代码解析:
(1)添加JWT 身份认证中间件
//配置JWT。
builder.Services.AddAuthentication(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(j =>
{
j.RequireHttpsMetadata = false;
j.SaveToken = true;
j.TokenValidationParameters = new TokenValidationParameters
{
//是否调用对签名securityToken的SecurityKey进行验证
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("798654167464654646")),//签名秘钥
ValidateIssuer = true,//是否验证颁发者
ValidIssuer = "dotnet-jwt",//颁发者
ValidateAudience = true, //是否验证接收者
ValidAudience = "StudentAPI",//接收者
ValidateLifetime = true,//是否验证失效时间
};
//捕获Token过期事件
j.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
//出现此类异常。
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
//在响应头中添加isexpired:true键值对。
context.Response.Headers.Add("isexpired", "true");
}
return Task.CompletedTask;
}
};
});
(2)捕获 Token 事件,处理refreshToken:
//捕获Token过期事件
j.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
//出现此类异常。
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
//在响应头中添加isexpired:true键值对。
context.Response.Headers.Add("isexpired", "true");
}
return Task.CompletedTask;
}
};
(3)添加基于策略的授权:
//配置策略授权
builder.Services.AddAuthorization(options => {
options.AddPolicy("OnlyRead", policy => policy.RequireRole("Read").Build());
});
然后在控制器中使用基于策略的授权:
代码:Controllers/StudentController.cs:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DotNet.WebApi.Jwt.ApiResources.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
[Authorize(Policy = "ReadWrite")]
[HttpGet("GetStudents")]
public ActionResult<dynamic> GetStudents()
{
......
}
[Authorize(Policy = "OnlyRead")]
[HttpGet("GetStudent")]
public ActionResult<dynamic> GetStudent()
{
......
}
}
}
认证服务器
创建项目
新建一个“AspNetCore 空” 项目,名为:DotNet.WebApi.Jwt.Authentication
依赖包
添加依赖包:
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />
数据库
创建一个数据库,用于保存登录用户
JWTUser
namespace DotNet.WebApi.Jwt.Authentication.Data
{
public class JWTUser
{
//用户Id。
[Key]
public int UserId { get; set; }
//用户名。
public string? UserName { get; set; }
//用户密码。
public string? UserPwd { get; set; }
//用户邮箱。
public string? UserEmail { get; set; }
}
}
JWTDbContext
using Microsoft.EntityFrameworkCore;
namespace DotNet.WebApi.Jwt.Authentication.Data
{
/// <summary>
/// 数据库上下文。
/// </summary>
public class JWTDbContext : DbContext
{
public JWTDbContext(DbContextOptions<JWTDbContext> options) : base(options)
{
}
public DbSet<JWTUser>? JWTUsers { get; set; }
}
}
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"JwtDbConnection": "Server=localhost;Database=JWTDb;Uid=sa;Pwd=123456;Encrypt=True;TrustServerCertificate=True;"
}
}
用户注册
Controllers/AccountController.cs
using DotNet.WebApi.Jwt.Authentication.Data;
using Microsoft.AspNetCore.Mvc;
namespace DotNet.WebApi.Jwt.Authentication.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly JWTDbContext _context;
public AccountController(JWTDbContext context)
{
_context = context;
}
/// <summary>
/// 添加用户。
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[HttpPost("Register")]
public async Task<ActionResult<int>> RegisterUser(JWTUser user)
{
//如果user参数为空,则返回404错误。
if (user == null) return NotFound();
//添加用户
_context.JWTUsers?.Add(user);
//执行操作。
var count = await _context.SaveChangesAsync();
return count;
}
}
}
Token 控制器
Controllers/TokenController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using DotNet.WebApi.Jwt.Authentication.Data;
namespace DotNet.WebApi.Jwt.Authentication.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TokenController : ControllerBase
{
private readonly JWTDbContext _context;
private const string signingKey = "798654167464654646";
private const string issuer = "dotnet-jwt";
private const string audience = "StudentAPI";
public TokenController(JWTDbContext context)
{
_context = context;
}
/// <summary>
/// 生成Token
/// </summary>
/// <returns></returns>
[HttpGet("Get")]
public async Task<ActionResult> BuildAccessToken(string userName, string userPwd)
{
//判断用户信息是否为空
if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(userPwd))
{
return NotFound();
}
//根据用户名和密码找到用户实体
var user = await _context.JWTUsers!.AsNoTracking()
.FirstOrDefaultAsync( u =>
u.UserName!.Equals(userName) && u.UserPwd!.Equals(userPwd)
);
if (user == null)
{
return BadRequest("用户名或密码错误。");
}
//声明
var claims = new[]
{
new Claim(ClaimTypes.Sid,user.UserId.ToString()),
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role,"Read")
};
//设置密钥
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
//设置凭据
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//生成token
var jwtToken = new JwtSecurityToken(issuer,
audience,
claims,
expires: DateTime.UtcNow.AddMinutes(30),
signingCredentials: credentials);
var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
return Ok(token);
}
/// <summary>
/// 根据Token获取身份声明。
/// </summary>
/// <param name="token">token</param>
/// <returns></returns>
private ClaimsPrincipal GetPrincipalFromAccessToken(string token)
{
var jwtSecurityToken = new JwtSecurityTokenHandler();
var claimsPrincipal = jwtSecurityToken.ValidateToken(token, new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
ValidateLifetime = false
}, out SecurityToken validatedToken);
return claimsPrincipal;
}
/// <summary>
/// 根据旧Token换取新Token
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
[HttpGet("Refresh")]
public ActionResult BuildRefreshToken(string accessToken)
{
if (string.IsNullOrWhiteSpace(accessToken)) return NotFound();
var userClaims = GetPrincipalFromAccessToken(accessToken);
if (userClaims == null) return NotFound();
//获取旧Token中的声明
var claims = new[]
{
//用户ID
new Claim(ClaimTypes.Sid,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Sid))!.Value),
//用户名
new Claim(ClaimTypes.Name,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Name))!.Value),
//角色
new Claim(ClaimTypes.Role,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Role))!.Value)
};
//设置密钥
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
//设置凭据
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//生成token
var jwtToken = new JwtSecurityToken(issuer, audience, claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: credentials);
var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
return Ok(token);
}
}
}
代码分析:
(1).生成token:BuildAccessToken();
(2).刷新token:BuildRefreshToken(accessToken),调用GetPrincipalFromAccessToken(accessToken)
方法传入过期的 token 解析出 userClaims,用于生成新的token。
Program
Program.cs
using DotNet.WebApi.Jwt.Authentication.Data;
using Microsoft.EntityFrameworkCore;
namespace DotNet.WebApi.Jwt.Authentication
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
//注册数据库上下文服务
//UseSqlServer表示使用SQLServer数据库。
builder.Services.AddDbContext<JWTDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("JwtDbConnection")
));
//设置跨域
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
//允许任何来源访问。
builder.AllowAnyOrigin().AllowAnyHeader();
});
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
//生成数据库和表结构
var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<JWTDbContext>();
//如果数据库不存在,则生成表结构。
context.Database.EnsureCreated();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.Urls.Add("https://*:6001"); // 修改端口
app.UseHttpsRedirection();
app.UseCors(); //启用跨域
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
客户端
创建项目
创建一个 “AspNet Core 空项目”,名为:DotNet.WebApi.Jwt.WebClient。这个项目没用到 AspNet Core 的任何功能,仅仅只是作为一个静态文件站点,即:一个纯前端项目。
添加 JS 库
创建"wwwroot"文件夹,然后选择该文件夹,右键【添加/客户端库】,添加 jquery.min.js、bootstrap.min.css 文件。
用户注册
新建Html页面:wwwroot/Users/Register.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>用户注册</title>
<link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<script src="../jquery/jquery.min.js"></script>
</head>
<body>
<div style="padding:20px;width:600px;margin:30px;">
<h3>用户注册</h3>
<hr />
<div class="form-floating">
<div class="mb-3">
<label class="form-label">用户名:</label>
<input type="text" id="userName" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">密 码:</label>
<input type="password" id="userPwd" class="form-control">
</div>
<div class="mb-3">
<input type="submit" id="btn" value="注册" class="btn btn-primary" />
</div>
<div>
<span id="msg" style="color:red"></span>
</div>
</div>
</div>
<script>
$("#btn").click(function () {
$.ajax({
//请求类型
type: "post",
//请求路径
url: "https://localhost:6001/api/Account/Register",
//预期服务器返回的数据类型
dataType: "text",
data: JSON.stringify({ UserName: $("#userName").val(), UserPwd: $("#userPwd").val() }),
contentType: "application/json",
//请求成功时的回调函数
success: function (result) {
if (result == "1") {
$("#msg").text("用户注册成功。");
}
}
});
});
</script>
</body>
</html>
用户登录
新建Html页面:wwwroot/Users/Login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>用户登录</title>
<link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<script src="../jquery/jquery.min.js"></script>
</head>
<body>
<div style="padding:20px;width:600px;margin:30px;">
<h3>用户登录</h3>
<hr />
<div class="form-floating">
<div class="mb-3">
<label class="form-label">用户名:</label>
<input type="text" id="userName" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">密 码:</label>
<input type="password" id="userPwd" class="form-control">
</div>
<div class="mb-3">
<input type="submit" id="btn" value="登录" class="btn btn-primary" />
</div>
<div class="mb-3">
<span id="msg" style="color:red"></span>
</div>
</div>
</div>
<script>
$("#btn").click(function () {
$.ajax({
//请求类型
type: "get",
//请求路径
url: "https://localhost:6001/api/Token/Get",
//预期服务器返回的数据类型
dataType: "text",
data: { UserName: $("#userName").val(), UserPwd: $("#userPwd").val() },
contentType: "application/json",
//请求成功时的回调函数
success: function (token) {
localStorage.setItem("token", token);
console.log(token);
location.href = 'GetData.html';
}
});
});
</script>
</body>
</html>
代码解析:
(1).获取token:调用 Get请求:https://localhost:6001/api/Token/Get, 获取token。
(2).将token保存到本地存储: localStorage.setItem("token", token);
获取API数据
新建Html页面:wwwroot/Users/GetData.html,内部调用 认证服务器【DotNet.WebApi.Jwt.Authentication】获取 token,然后使用token调用资源服务器【DotNet.WebApi.Jwt.ApiResources】的API
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>获取数据</title>
<link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<script src="../jquery/jquery.min.js"></script>
</head>
<body style="margin:20px;">
<table id="showTable" class="table table-bordered">
<thead>
<tr>
<td>ID</td>
<td>姓名</td>
<td>年龄</td>
</tr>
</thead>
<tbody></tbody>
</table>
<script>
var tbody = $("#showTable tbody")
//请求资源
$.ajax({
type: 'get',
contentType: 'application/json',
url: 'https://localhost:6002/api/Student/GetStudent',
beforeSend: function (xhr) {
//获取Token
var accessToken = localStorage.getItem("token");
//使用Token请求资源
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
},
//获取的数据
success: function (data) {
$.each(data, function (n, value) {
var trs = "";
trs += "<tr>" +
"<td>" + value.id + "</td>" +
"<td>" + value.name + "</td>" +
"<td>" + value.age + "</td>" +
"</tr>";
tbody += trs;
});
$("#showTable").append(tbody);
},
error: function (xhr) {
if (xhr.status === 401 && xhr.getResponseHeader('isexpired') === 'true') {
//Token已过期了。
getRefreshAccessToken();
}
}
})
//获取刷新后的新Token。
function getRefreshAccessToken() {
$.ajax({
type: 'get',
contentType: 'application/json',
url: 'https://localhost:6001/api/Token/Refresh',
data: { accessToken: localStorage.getItem("token") },
success: function (token) {
//将获取的新Token存储起来
localStorage.setItem("token", token);
}
})
}
</script>
</body>
</html>
代码解析:
(1) 从本地存储中获取token:var accessToken = localStorage.getItem("token");
(2) 刷新token: 当返回401并且响应头中有‘xhr.getResponseHeader('isexpired') === 'true'’,调用getRefreshAccessToken()
从认证服务的
https://localhost:6001/api/Token/Refresh
,参数为当前过期的 token,获取新的token。
Program
修改 Program.cs 为:
namespace DotNet.WebApi.Jwt.WebClient
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles(); //启用静态文件
app.Run();
}
}
}
有关刷新token
上述获取刷新token的方式是通过返回401和返回头中带特定的字段来判断token是否过期,这种做法的缺点就是必须失败一次。更好的做法是,
- 返回token的请求中包含其过期时间,
- 前端封装出一个Http请求函数:每次请求前都将当前时间与过期时间比对,若token过期,使用过期的token请求新的token,并重设请求头:authorization:bearer token.