在 ASP.NET Core 自訂登入邏輯
2 | 6,503 |
上回提到我想做過手機掃 QR Code 條碼登入 ASP.NET Core 網站,這涉及自訂登入身分檢查邏輯,在 ASP.NET 時代可以呼叫 FormsAuthentication.SetAuthCookie 寫入 Cookie (範例) 模擬登入成功狀態,但換成 ASP.NET Core 要怎麼做?這是個好問題,也是本篇文章的研究重點。
我假想了一種不用密碼,回答挑戰問題的登入方式(Challenge Response Authentication),為求簡便,範例是用兩個數字相加當成挑戰,回應正確答案即登入成功。實務應用時,可將挑戰問題換成雜湊或公私鑰相關的演算,即能達到資安要求的強度。
我使用 ASP.NET Core MVC 專案實作,加一個 VIPServiceController,加註 [Authorize] 要求先登入才能使用:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace DemoWeb.Controllers
{
[Authorize]
public class VIPServiceController : Controller
{
public IActionResult Index()
{
return View();
}
}
}
登入身分驗證邏輯寫在 AuthController.cs。挑戰問題的解答我存入 IDistributedCache,開發測試用記憶體模擬,正式應用時可改用 Redis、SQL 或 NCache,程式不必修改就能適用負載平衡架構。挑戰答案 Cache Key 採隨機 GUID,與答案一起送回後端,再從 IDistributedCache 取出答案比對。至於寫入身分認證 Cookie 的方法,我是參考微軟這篇: Use cookie authentication without ASP.NET Core Identity),用 HttpContext.SignInAsync() 搞定:
using System.Security.Claims;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using DemoWeb.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
namespace DemoWeb.Controllers
{
public class AuthController : Controller
{
private readonly IDistributedCache _cache;
public AuthController(IDistributedCache cache)
{
_cache = cache;
}
public IActionResult Index()
{
return Content("OK");
}
void CreateChallenge()
{
var token = Guid.NewGuid().ToString();
var rnd = new Random();
var a = rnd.Next(9) + 1;
var b = rnd.Next(9) + 1;
var answer = a + b;
var challenge = $"{a}+{b}=";
_cache.SetString(token, answer.ToString(), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
ViewBag.Challenge = challenge;
ViewBag.Token = token;
}
[HttpGet]
public IActionResult Login(string ReturnUrl)
{
CreateChallenge();
ViewBag.ReturnUrl = ReturnUrl;
return View();
}
[HttpPost]
public async Task<IActionResult> Login(string token, string name, string response, string ReturnUrl)
{
var message = string.Empty;
try
{
var answer = await _cache.GetStringAsync(token);
if (answer == null)
throw new ApplicationException("Invalid Token");
await _cache.RemoveAsync(token);
if (answer == response)
{
await SignIn(name);
return Redirect(ReturnUrl ?? "~/");
}
else
{
message = "Login Failed";
}
}
catch (Exception ex)
{
message = ex.Message;
}
ViewBag.Message = message;
ViewBag.Name = name;
CreateChallenge();
ViewBag.ReturnUrl = ReturnUrl;
return View();
}
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return Redirect("~/");
}
async Task SignIn(string name)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, name),
new Claim("FullName", name),
new Claim(ClaimTypes.Role, "Administrator"),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
//AllowRefresh = <bool>,
// Refreshing the authentication session should be allowed.
//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
// The time at which the authentication ticket expires. A
// value set here overrides the ExpireTimeSpan option of
// CookieAuthenticationOptions set with AddCookie.
//IsPersistent = true,
// Whether the authentication session is persisted across
// multiple requests. When used with cookies, controls
// whether the cookie's lifetime is absolute (matching the
// lifetime of the authentication ticket) or session-based.
//IssuedUtc = <DateTimeOffset>,
// The time at which the authentication ticket was issued.
//RedirectUri = <string>
// The full path or absolute URI to be used as an http
// redirect response value.
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
}
}
Program.cs 部分只加三個地方:(見中文註解)
using DemoWeb.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
var builder = WebApplication.CreateBuilder(args);
// 使用記憶體模擬 IDistributedCache,正式環境可用 Redis/SQL/NCache
builder.Services.AddDistributedMemoryCache();
// 設定 Cookie 式登入驗證,指定登入登出 Action
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/Auth/Login";
options.LogoutPath = "/Auth/Logout";
//options.AccessDeniedPath = "/Auth/AccessDenied";
});
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
// 啟用身分認證
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
實測結果如下:
範例專案已放上 Github,想試玩的同學可自取參考。
Example of customized login logic with cookie authentication in ASP.NET Core.
Comments
# by quintos
私以为,巨硬在 auth schema 名称这里设计的const名称 CookieAuthenticationDefaults.AuthenticationScheme过于冗长,不如直接用 ”cookie“ 来得清晰,或者自己定义一套常量, AuthSchema.Cookie, AuthSchema.Jwt, AuthSchema.other
# by Masa
謝謝,受益良多