首先我們實現Cookie認證,然后再次引入JWT,最后在結合二者使用時聯系其他我們可能需要注意的事項
Cookie認證
在startup中我們添加cookie認證服務,如下:
- services.AddAuthentication(options =>
- {
- options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
- options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
- })
- .AddCookie(options =>
- {
- options.ExpireTimeSpan = TimeSpan.FromMinutes(1);
- options.Cookie.Name = "user-session";
- options.SlidingExpiration = true;
- });
接下來則是使用認證和授權中間件,注意將其置于路由和終結點終結點之間,否則啟動也會有明確異常提示
- app.UseRouting();
- app.UseAuthentication();
- app.UseAuthorization();
- app.UseEndpoints(endpoints =>
- {
- ......
- });
我們給出測試視圖頁,并要求認證即控制器添加特性
- [Authorize]
- public class HomeController : Controller
- {
- public IActionResult Index()
- {
- return View();
- }
- }
當進入首頁,未認證默認進入account/login,那么接下來創建該視圖
- public class AccountController : Controller
- {
- [AllowAnonymous]
- public IActionResult Login()
- {
- return View();
- }
- ......
- }
我們啟動程序先看看效果
如上圖,自動跳轉至登錄頁,此時我們點擊模擬登錄按鈕,發起請求去模擬登錄(發起ajax請求代碼就占不用篇幅給出了)
- /// <summary>
- /// 模擬登錄
- /// </summary>
- /// <returns></returns>
- [HttpPost]
- [AllowAnonymous]
- public async Task<IActionResult> TestLogin()
- {
- var claims = new Claim[]
- {
- new Claim(ClaimTypes.Name, "Jeffcky"),
- };
- var claimsIdentity = new ClaimsIdentity(claims, "Login");
- await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
- return Ok();
- }
上述無非就是構建身份以及該身份下所具有的身份屬性,類似個人身份證唯一標識個人,身份證上各個信息即表示如上聲明
同時呢,肯定要調用上下文去登錄,在整個會話未過期之前,根據認證方案獲取對應處理方式,最后將相關信息進行存儲等等,有興趣的童鞋可以去了解其實現細節哈
當我們請求過后,再次訪問首頁,將看到生成當前會話信息,同時我們將會話過期設置為1分鐘,在1分鐘內未進行會話,將自動重定向至登錄頁
注意如上標注并沒有值,那么這個值可以設置嗎?當然可以,在開始配置時我們并未給出,那么這個屬性又代表什么含義呢?
- options.Cookie.MaxAge = TimeSpan.FromMinutes(2);
那么結合ExpireTimeSpan和MaxAge使用,到底代表什么意思呢?我們暫且撇開滑動過期設置
ExpireTimeSpan表示用戶身份認證票據的生命周期,它是認證cookie的有效負載,存儲的cookie值是一段加密字符串,在每次請求時,web應用程序都會根據請求對其進行解密
MaxAge控制著cookie的生命周期,若cookie過期,瀏覽器將會自動清除,如果沒有設置該值,實質上它的生命周期就是ExpireTimeSpan,那么它到底有何意義呢?
上述我們設置票據的生命周期為1分鐘,同時我們控制cookie的生命周期為2分鐘,若在2分鐘內關閉瀏覽器或重啟web應用程序,此時cookie生命周期并未過期,所以仍將處于會話狀態即無需登錄,若未設置MaxAge,關閉瀏覽器或重啟后將自動清除其值即需登錄,當然一切前提是未手動清除瀏覽器cookie
問題又來了,在配置cookie選項中,還有一個也可以設置過期的屬性
- options.Cookie.Expiration = TimeSpan.FromMinutes(3);
當配置ExpireTimeSpan或同時配置MaxAge時,無需設置Expiration,因為會拋出異常
JWT認證
上述已經實現Cookie認證,那么在與第三方進行對接時,我們要使用JWT認證,我們又該如何處理呢?
首先我們添加JWT認證服務
- .AddJwtBearer(options =>
- {
- options.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")),
- ValidateIssuer = true,
- ValidIssuer = "http://localhost:5000",
- ValidateAudience = true,
- ValidAudience = "http://localhost:5001",
- ValidateLifetime = true,
- ClockSkew = TimeSpan.FromMinutes(5)
- };
- });
將JWT Token置于cookie中,此前文章已有講解,這里我們直接給出代碼,先生成Token
- private string GenerateToken(Claim[] claims)
- {
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
- var token = new JwtSecurityToken(
- issuer: "http://localhost:5000",
- audience: "http://localhost:5001",
- claims: claims,
- notBefore: DateTime.Now,
- expires: DateTime.Now.AddMinutes(5),
- signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
- );
- return new JwtSecurityTokenHandler().WriteToken(token);
- }
在登錄方法中,將其寫入響應cookie中,如下這般
- /// <summary>
- /// 模擬登錄
- /// </summary>
- /// <returns></returns>
- [HttpPost]
- [AllowAnonymous]
- public async Task<IActionResult> TestLogin()
- {
- var claims = new Claim[]
- {
- new Claim(ClaimTypes.Name, "Jeffcky"),
- };
- var claimsIdentity = new ClaimsIdentity(claims, "Login");
- Response.Cookies.Append("x-access-token", GenerateToken(claims),
- new CookieOptions()
- {
- Path = "/",
- HttpOnly = true
- });
- await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
- return Ok();
- }
去取Bearer Token值,若成功取到這賦值給如下context.Token,所以此時我們需要手動從cookie中取出token并賦值
- options.Events = new JwtBearerEvents
- {
- OnMessageReceived = context =>
- {
- var accessToken = context.Request.Cookies["x-access-token"];
- if (!string.IsNullOrEmpty(accessToken))
- {
- context.Token = accessToken;
- }
- return Task.CompletedTask;
- }
- };
一切已就緒,接下來我們寫個api接口測試驗證看看
- [Authorize("Bearer")]
- [Route("api/[controller]/[action]")]
- [ApiController]
- public class JwtController : ControllerBase
- {
- [HttpGet]
- public IActionResult Test()
- {
- return Ok("test jwt");
- }
- }
思考一下,我們通過Postman模擬測試,會返回401嗎?結果會是怎樣的呢?
問題不大,主要在于該特性參數為聲明指定策略,但我們需要指定認證方案即scheme,修改成如下:
如此在與第三方對接時,請求返回token,后續將token置于請求頭中即可驗證通過,同時上述取cookie中token并手動賦值,對于對接第三方則是多余,不過是為了諸多其他原因而已
- [Authorize(AuthenticationSchemes = "Bearer,Cookies")]
注意混合認證方案設置存在順序,后者將覆蓋前者即如上設置,此時將走cookie認證
滑動過期思考擴展
若我們實現基于Cookie滑動過期,同時使用signalr進行數據推送,勢必存在問題,因為會一直刷新會話,那么將導致會話永不過期問題,從安全層面角度考慮,我們該如何處理呢?
我們知道票據生命周期存儲在上下文AuthenticationProperties屬性中,所以在配置Cookie選項事件中我們可以進行自定義處理
- public class CookieAuthenticationEventsExetensions : CookieAuthenticationEvents
- {
- private const string TicketIssuedTicks = nameof(TicketIssuedTicks);
- public override async Task SigningIn(CookieSigningInContext context)
- {
- context.Properties.SetString(
- TicketIssuedTicks,
- DateTimeOffset.UtcNow.Ticks.ToString());
- await base.SigningIn(context);
- }
- public override async Task ValidatePrincipal(
- CookieValidatePrincipalContext context)
- {
- var ticketIssuedTicksValue = context
- .Properties.GetString(TicketIssuedTicks);
- if (ticketIssuedTicksValue is null ||
- !long.TryParse(ticketIssuedTicksValue, out var ticketIssuedTicks))
- {
- await RejectPrincipalAsync(context);
- return;
- }
- var ticketIssuedUtc =
- new DateTimeOffset(ticketIssuedTicks, TimeSpan.FromHours(0));
- if (DateTimeOffset.UtcNow - ticketIssuedUtc > TimeSpan.FromDays(3))
- {
- await RejectPrincipalAsync(context);
- return;
- }
- await base.ValidatePrincipal(context);
- }
- private static async Task RejectPrincipalAsync(
- CookieValidatePrincipalContext context)
- {
- context.RejectPrincipal();
- await context.HttpContext.SignOutAsync();
- }
- }
在添加Cookie服務時,有對應事件選項,使用如下
- options.EventsType = typeof(CookieAuthenticationEventsExetensions);
擴展事件實現表示在第一次會話到當前時間截止超過3天,則自動重定向至登錄頁,最后將上述擴展事件進行注冊即可
原文鏈接:https://mp.weixin.qq.com/s/zff_H_7eG0EaV7dP5xoMhg