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;
}
};
認證服務器
創建項目
新建一個“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),使用過期的token,從中調用GetPrincipalFromAccessToken(accessToken)
解析出 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"文件夾,然後選擇該文件夾,右鍵【添加/客戶端庫】,添加bootract.min.css、jquery.min.js 文件。
用戶註冊
新建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();
}
}
}