1 前言与注意事项
1.1 前言
在.Net(Core)6框架的程序中所使用的Jwt认证中间件是:“Microsoft.AspNetCore.Authentication.JwtBearer”,从名字就可以直接看出该中间件虽然是第3方中间件,但是由微软提供,所以它能与.Net(Core)6框架做到无逢集成。
Jwt认证方式实际上是微软为了移动端开发所提供的特定的解决方案,更加直白的说法是:Jwt认证方式结合跨域(Cors)操作实现服务器端与手机端App应用之间的认证,当然Jwt认证方式也可以实现服务器端与浏览器认证,但是效果上不如.Net(Core)6框架内置的“Cookie” 认证中间件,还有一种使用方式是在同一个Web项中把Jwt认证方式和“Cookie”认证方式结合起来使用,这除了炫技以外,从工程学的角度来看,这是一个及其糟糕的解决方案,如非十分必要不要这样定义实现。
如果Web项最终通过浏览器渲染显示,则在该Web项中只选择使用“Cookie”认证方式;如果Web项是以Api形式进行定义,且最终需要结合跨域(Cors)操作实现服务器端与手机端App应用之间的认证,则在该Web项中只选择使用“Jwt”认证方式。
1.2 注意事项:同1指定令牌(Token)
通过的Jwt认证中间件生成令牌(Token)的操作有两大部部分组成:
- 定义在Program.cs中的依赖注入配置。
- 用于令牌(Token)生成的自定义方法。
Jwt认证中间件通过3个参数成员或属性成员及其相对应的值来保证依赖注入配置和自定义方法所生成的于令牌(Token)是同1指定于令牌(Token),它们分别是:
(自定义方法参数)secretKey
--(依赖注入配置)IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("qwertyuiop123456")),
(自定义方法)new Claim(JwtClaimTypes.Issuer,"Token签发机关"),--(依赖注入配置) ValidIssuer = "Token签发机关",
(自定义方法)new Claim(JwtClaimTypes.Audience, "Token订阅"), --(依赖注入配置) ValidAudience = "Token订阅",
这3个参数成员或属性成员及其所对应的值必须相等,否则会导致被持久化的指定令牌(Token),将不是同1个令牌(Token),重定向授权页面时,会在指定令牌(Token)的生命周期内,重新定向到登陆页面,以获取新的令牌(Token)才能打开授权页面。
Jwt令牌(Token)与HTML(Session,无状态,即不保存数据)的最大不同,就是在设计之初时,就是为了保存数据,即Jwt令牌(Token)不用像Session一样在持久化时需要特定进行设定,因为Jwt令牌(Token)在设计上就是必须持久化的,所以可能过删除Session达到注销的目的,但是Jwt令牌(Token)却不能被删除,所以要达到注销的目的,就要通过新生成1个“空”或“随机”的Jwt令牌(Token)覆盖“原”Jwt令牌(Token)来达到注销的目的。
最好也把“空”或“随机”的Jwt令牌(Token)的生命周期属性实例也设定为:0。
2 内置Cookie认证方式
2.1 Program.cs中的依赖注入配置
//把“"Cookies"("Cookies"=CookieAuthenticationDefaults.AuthenticationScheme)认证类型”依赖注入中间件,注入到.Net(Core)6框架内置容器中。
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "ShenFenZheng";//指定证件在Cookies文件中持久存储的名称。
options.ClaimsIssuer = "证件签发机关";//证件的签发机关。
options.Cookie.HttpOnly = true;//通过js脚本将无法读取到登录用户的Cookie信息,这样能有效的防止XSS攻击,具体一点的介绍请google进行搜索。
//传输协议(HTTP/HTTPS)默认值。视情况而定,如果登录接口是Https请求,则设置Secure = true,即是安全状态,可以把用户实例持久存储到客户端浏览器的Cookies为文件中,否则,不设置。
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.SlidingExpiration = true;//指示是否在指定cookie快到期的时候会重新颁发该指定cookie。
options.ExpireTimeSpan = TimeSpan.FromSeconds(60);//证件在“Cookies”文件中的失效日期,默认值为14天(TimeSpan.FromDays(14);),为了方便测试设定为1分钟。
options.LoginPath = "/Login/Index";//默认登录页面的网络地址字符串(URL)。
options.AccessDeniedPath = "/Home/Error";//登录失败操作后重定向页面的网络地址字符串(URL)。
});
2.2 DirectRenderController添加标记“[Authorize]”
[Authorize]
public class DirectRenderController : Controller
2.3 LoginModel
/// <summary>
/// 【登录模型--纪录】
/// </summary>
/// <remarks>
/// 摘要:
/// 为登录操作提供数据支撑。
/// </remarks>
public record LoginModel
{
/// <summary>
/// 【用户名】
/// <remarks>
/// 摘要:
/// 获取/设置登录模型类1个指定实例的用户名。
/// </remarks>
/// </summary>
[Display(Name = "用户名")]
public string Name { get; set; }
/// <summary>
/// 【密码】
/// <remarks>
/// 摘要:
/// 获取/设置登录模型类1个指定实例的用密码。
/// </remarks>
/// </summary>
[Display(Name = "密码")]
public string Password { get; set; }
/// <summary>
/// 【保存登录信息】
/// <remarks>
/// 摘要:
/// 获取/设置学生实体1个指定实例的学号。
/// </remarks>
/// </summary>
[Display(Name = "保存登录信息")]
public bool RememberMe { get; set; }
}
2.4 LoginController
public class LoginController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Index(LoginModel model, string? returnUrl = null)
{
if (model.Name == "admin" && model.Password == "admin")
{
//获取1个用于存储身份单元实例的列表实例。
List<Claim> _claimList = new List<Claim>();
//获取用户名身份单元实例。
if (!string.IsNullOrEmpty(model.Name))
_claimList.Add(new Claim(ClaimTypes.Name, model.Name, ClaimValueTypes.String));
//通过存储身份单元实例的列表实例,构建1个身份证实例,该实例的持久化方法是:把客户的登录信息持久化到客户端浏览器的Cookie文件中。
ClaimsIdentity _userIdentity = new ClaimsIdentity(_claimList, CookieAuthenticationDefaults.AuthenticationScheme);
//把1个身份证实例交付给指定证件当事人实例。
ClaimsPrincipal _userPrincipal = new ClaimsPrincipal(_userIdentity);
//通过身份认证参数实例,设置是否把客户的登录信息持久化到客户端浏览器的Cookie文件中,及其身份证实例的签发日期。
//注意:在该身份认证参数实例中没有设置过期时间间隔:当前用户执行登录操作且成功后,浏览器会所该用户的身份证信息,(如果“IsPersistent”属性的实例值为:true时)会被持久化存储到该浏览器的Cookies文件中。
//但是该被持久化存储的身份证信息是有时间限制的,如果在限制时间内重新打关闭的浏览器并直接打开授权页面,浏览器会通过验证的Cookies文件中所存储的用户的身份证信息,
//从而直接打开该授权页面(该业务实现可能是由服务器端通过session自动执行的)。如果已经错过限制时间,操作系统会在时间过期后自动的删除浏览器的Cookies文件中,
//所持久化存储的用户的身份证信息,这时重新打关闭的浏览器并直接打开授权页面,由于浏览器会通过验证则的Cookies文件中,没有存储的用户的身份证信息,会自动跳转到登录页面。
AuthenticationProperties _authenticationProperties = new AuthenticationProperties
{
//指示是否把客户的登录信息持久化到客户端浏览器的Cookie文件中。
IsPersistent = true,
//证件在“Cookies”文件中的失效日期,默认值为14天(TimeSpan.FromDays(14);),为了方便测试设定为2分钟。
//注意:该属性实例的权限高于“Program.cs”中“AddCookie.ExpireTimeSpan”属性实例的权限,即如果对两个属性实例都进行了设定,则证件的失效日期最终由该属性实例所决定。
IssuedUtc = DateTime.Now.AddSeconds(120),
};
//通过默认身份认证方式(也被称为:Cookie身份认证方式),执行登录操作。
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, _userPrincipal, _authenticationProperties);
if (!string.IsNullOrEmpty(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("Index", "Home");
}
else
{
return RedirectToAction("Error", "Home");
}
}
public async Task<IActionResult> LoginOut()
{
if (HttpContext.User.Identity.IsAuthenticated)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
return RedirectToAction("Index", "Login");
}
}
2.5 Login\Index.cshtml
@model Web.Models.LoginModel
<div class="row">
<div class="col-md-12">
<form asp-action="Index"
asp-route-returnurl="@Context.Request.Query["returnUrl"]">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password" class="control-label"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group form-check mt-3">
<label class="form-check-label">
<input class="form-check-input checkBox20 " asp-for="RememberMe" /> @Html.DisplayNameFor(model => model.RememberMe)
</label>
</div>
<div class="form-group mt-3">
<input type="submit" value="登 录" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
3 第3方JwtBearer认证方式
3.1 Program.cs设定
3.1.1 依赖注入配置
//把“"Bearer"("Bearer"=JwtBearerDefaults.AuthenticationScheme)认证类型”依赖注入中间件,注入到.Net(Core)6框架内置容器中。
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
context.Token = context.Request.Cookies["LoginToken"];
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
if (context.Request.Path.Value.ToString() == "/LoginJwt/LoginOut")
{
var token = ((context as TokenValidatedContext).SecurityToken as JwtSecurityToken).RawData;
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
// 如果指定令牌(Token)已经过期,则在头信息实例中添加相应的字典实例。
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
options.TokenValidationParameters = new TokenValidationParameters
{
// 指示在使用jwt中间件生成指定令牌(Token)时是否使用秘钥,当前设定为:true,即使用秘钥。
ValidateIssuerSigningKey = true,
//由发行者(开发都)提供的常量“秘钥”字符串,该字符串不能小于16个字符。
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("qwertyuiop123456")),
// 指示在使用jwt中间件生成指定令牌(Token)中是否包含有“签发机关”的数据信息,当前设定为:true,即包含。
ValidateIssuer = true,
//指定令牌(Token)“签发机关”所对应的数据信息为:“Token签发机关”。
ValidIssuer = "Token签发机关",
// 指示在使用jwt中间件生成指定令牌(Token)中是否包含有“订阅者”的数据信息,当前设定为:true,即包含。
ValidateAudience = true,
//指定令牌(Token)“订阅者”所对应的数据信息为:“Token订阅”
ValidAudience = "Token订阅",
//指示是否对指定令牌(Token)的过期时间进行限定,当前设定为:true,即限定。
RequireExpirationTime = true,
//指示是否对指定令牌(Token)的生命周期进行自动管理,当前设定为:true,即管理,
//当前指定令牌(Token)的生命周期结束时,程序必须重新生成1个新的指定令牌(Token)才能方法授权页面。
//使用当前时间与Token的Claims中的NotBefore和Expires对比后,进行管理。
ValidateLifetime = true,
//缓冲过期时间,指定令牌(Token)的总有效时间等于该时间加上jwt的过期时间,缓冲过期时间的默认值为“5分钟”,
//当前把缓冲过期时间设定为:0,指定令牌(Token)的总有效时间即为jwt的过期时间。
ClockSkew = TimeSpan.FromSeconds(0),
};
});
3.1.2 内置“状态代码页”管道中件间方法
//把内置“状态代码页”管道中件间集成到.Net(Core)6框架中。
//注意:该管道中件间必须在“认证”管道中件间之前集成,否则依然会出现“401”错误页面。
app.UseStatusCodePages(async context => {
await Task.Run(() => {
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
response.Redirect("/LoginJwt/Index?returnUrl=" + request.Path);
}
});
});
//把内置“认证”管道中件间集成到.Net(Core)6框架中。
app.UseAuthentication();
app.UseAuthorization();
3.2 JwtExtension
// <summary>
/// 【Jwt扩展】
/// </summary>
/// <remarks>
/// 摘要:
/// 通过该类中的方法获取于获取1个指定令牌(Token)实例的字符串值,为指定用户登陆操作提供数据支撑。
/// </remarks>
public static class JwtExtension
{
///<param name="name">令牌(Token)证件单元的名称,例如:身份证中的姓名“单元”。</param>
///<param name="secretKey">“秘钥”字符串。</param>
/// <summary>
/// 【生成令牌】
/// <remarks>
/// 摘要:
/// 该方法用于获取1个指定令牌(Token)实例的字符串值,为指定用户登陆操作提供数据支撑。
/// 注意:
/// 该方法中的“秘钥”字符串参数实例必须与TokenValidationParameters.IssuerSigningKey相对应的值相等,否则被持久化的指定令牌(Token),
/// 将不是同1个令牌(Token),重定向授权页面时,会在指定令牌(Token)的生命周期内,重新定向到登陆页面,以获取新的令牌(Token)才能打开授权页面。
/// </remarks>
/// </summary>
public static string CreateToken(this string name, string secretKey)
{
//把“秘钥”字符串,生成二进制“秘钥”实例。
SymmetricSecurityKey _securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey));
//获取通过哈希算法加密后的二进制“秘钥”实例。
SigningCredentials _signingCredential = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256Signature);
JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
DateTime _dateTime = DateTime.Now;//指定令牌(Token)被生成的时间。
//注意: _tokenDescriptor中属性成员实例的权限高于“Program.cs”中“ TokenValidationParameters”属性成员实例的权限,
//即如果对两个属性成员实例都进行了设定,则指定令牌(Token)最终由 _tokenDescriptor中属性成员实例所决定。
SecurityTokenDescriptor _tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name,name),
new Claim(JwtClaimTypes.Id, Guid.NewGuid().ToString()),
//注意该“Issuer”和“Audience”对的值必须与TokenValidationParameters.IssuerSigningKey和TokenValidationParameters.ValidAudience相对应的值相等,
//否则被持久化的指定令牌(Token),将不是同1个令牌(Token),重定向授权页面时,会在指定令牌(Token)的生命周期内,重新定向到登陆页面,以获取新的令牌(Token)才能打开授权页面。
new Claim(JwtClaimTypes.Issuer,"Token签发机关"),//指定令牌(Token)“签发机关”所对应的数据信息为:“"https://localhost:7023/”。
new Claim(JwtClaimTypes.Audience, "Token订阅"), //指定令牌(Token)“订阅者”所对应的数据信息为:“Api”,即控制器的行为方法。
new Claim(JwtClaimTypes.NotBefore, $"{new DateTimeOffset(_dateTime).ToUnixTimeSeconds()}"),//指定令牌(Token)的生效时间,当前与“生成的时间”相同,即立即生效。
new Claim(JwtClaimTypes.Expiration, $"{new DateTimeOffset(_dateTime.AddSeconds(120)).ToUnixTimeSeconds()}")//指定令牌(Token)的生命周期,按秒数计算。
}),
IssuedAt = _dateTime,//指定令牌(Token)被生成的时间。
Expires = _dateTime.AddSeconds(120),//指定令牌(Token)的生命周期。
NotBefore = _dateTime,//指定令牌(Token)的生效时间,当前与“生成的时间”相同,即立即生效。
SigningCredentials = _signingCredential
};
//生成1个新的令牌(Token,JSON编码格式的)实例。
SecurityToken _securityToken = _tokenHandler.CreateToken(_tokenDescriptor);
//把1个新的令牌(Token)实例转换为相应的字符串。
string _tokenString = _tokenHandler.WriteToken(_securityToken);
return _tokenString;
}
}
3.3 LoginJwtController
public class LoginJwtController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult Index(LoginModel model, string? returnUrl = null)
{
if (model.Name == "admin" && model.Password == "admin")
{
//由发行者(开发者)提供的常量“秘钥”字符串,该字符串不能小于16个字符。
//注意:该“秘钥”字符串必须与TokenValidationParameters.IssuerSigningKey相对应的值相等,否则被持久化的指定令牌(Token),将不是同1个令牌(Token),
//重定向授权页面时,会在指定令牌(Token)的生命周期内,重新定向到登陆页面,以获取新的令牌(Token)才能打开授权页面。
string _token = model.Name.CreateToken("qwertyuiop123456");
Response.Cookies.Append("LoginToken", _token);
if (!string.IsNullOrEmpty(returnUrl))
return Redirect(returnUrl);
return RedirectToAction("Index", "Home");
}
else
{
return RedirectToAction("Error", "Home");
}
}
public IActionResult LoginOut()
{
//注意:
//Jwt令牌(Token)与HTML(Session,无状态,即不保存数据)的最大不同,就是在设计之初时,就是为了保存数据,
//所以可能过删除Session达到注销的目的,但是Jwt令牌(Token)却不能被删除,所以要达到注销的目的,
//就要通过新生成1个“空”或“随机”的Jwt令牌(Token)覆盖“原”Jwt令牌(Token)来达到注销的目的。
//最好也把“空”或“随机”的Jwt令牌(Token)的生命周期属性实例也设定为:0。
string _token = "admin".CreateToken(Guid.NewGuid().ToString());
Response.Cookies.Append("LoginToken", _token);
return RedirectToAction("Index", "LoginJwt");
}
}
3.4 LoginJwt\Index.cshtml
@model Web.Models.LoginModel
<div class="row">
<div class="col-md-12">
<form asp-action="Index"
asp-route-returnurl="@Context.Request.Query["returnUrl"]">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password" class="control-label"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group mt-3">
<input type="submit" value="登 录" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
3.5 Shared\_Layout.cshtml
@*内置身份认证。*@
<li class="nav-item">
@if (Context.User.Identity.IsAuthenticated)
{
<a class="nav-link text-dark" asp-area="" asp-controller="Login" asp-action="LoginOut">注销Cookie认证</a>
}
else
{
<a class="nav-link text-dark" asp-area="" asp-controller="Login" asp-action="Index">Cookie认证登录</a>
}
</li>
@*Jwt身份认证。*@
<li class="nav-item">
@if (Context.User.Identity.IsAuthenticated)
{
<a class="nav-link text-dark" asp-area="" asp-controller="LoginJwt" asp-action="LoginOut">注销Jwt认证</a>
}
else
{
<a class="nav-link text-dark" asp-area="" asp-controller="LoginJwt" asp-action="Index">Jwt认证登录</a>
}
</li>
对以上功能更为具体实现和注释见:22-10-11-07_SqlSugarAcquaintance(初识SqlSugarCore之Jwt认证中间件)。