c#.net的學習(二)
ASP.CORE的學習(二)
Filter的使用
簡單來說,過濾器的作用和意義在于提供一種機制,用于在請求處理管道的特定階段執行代碼,從而優雅地處理那些“橫切關注點” (Cross-Cutting Concerns)。
常見的過濾器的執行順序
請求 -> [授權] -> [資源] -> [操作] -> Action方法 -> [操作] -> [結果] -> [資源] -> 響應
異常過濾器像一個安全網,在上述任何一個環節(授權之后)出錯時都會被觸發。
ResoureFilter
- 接口:
IResourceFilter和IAsyncResourceFilter - 執行時機:在授權之后,但在模型綁定之前運行。它會"環繞"后續大部分管道,所以在Action執行后還會再次執行。
- 核心作用:非常適合用于緩存或任何需要在管道早期"短路"以避免執行Action的場景。
- 常見用途:
- 實現輸出緩存:在請求到達時,檢查緩存中是否有結果。如果命中,則直接返回緩存內容,后續的模型綁定、Action的執行等全部跳過,性能極高。
- 請求預處理:在模型綁定前對請求做一些全局性的處理。
- 向響應中添加全局Header:在管道末尾,向所有響應中添加一個公共響應頭(如
X-Version)
- 一句話總結:"緩存和資源的管家",能在第一時間決定是否需要干活,并負責善后。
同步的例子:
由于是演示,沒有使用內置緩存
實現同步的Filter的代碼CustomResourceFilterAttribute.cs
public class CustomResourceFilterAttribute : Attribute, IResourceFilter
{
private static Dictionary<string, Object> CacheDictionary = new Dictionary<string, Object>();
public void OnResourceExecuting(ResourceExecutingContext context)
{
string key = context.HttpContext.Request.Path.ToString().ToLower();
Console.WriteLine($"CustomResourceFilter: OnResourceExecuting - Checking cache for key: {key}");
if (CacheDictionary.TryGetValue(key, out var cachedValue))
{
Console.WriteLine("CustomResourceFilter: Cache hit - Returning cached response.");
context.Result = (Microsoft.AspNetCore.Mvc.IActionResult)cachedValue;
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
string key = context.HttpContext.Request.Path.ToString().ToLower();
if (!CacheDictionary.ContainsKey(key))
{
Console.WriteLine($"CustomResourceFilter: OnResourceExecuted - Caching response for key: {key}");
CacheDictionary[key] = context.Result;
}
}
}
在控制器的代碼
[Route("api/[controller]")]
[ApiController]
public class FilterController : ControllerBase
{
[HttpGet("Index"),CustomResourceFilter]
public IActionResult Index()
{
// 直接返回一個簡單的字符串響應
// 在 Action 中打印日志,用于判斷 Action 是否被執行
Console.WriteLine(">>> Action Executed: Generating new server time...");
// 返回一個每次都會變化的內容
var result = new
{
message = "This is a fresh response from the server1.",
serverTime = DateTime.Now.ToString("o") // "o" 格式包含毫秒,便于觀察
};
return Ok(result);
}
}
這樣在用戶訪問的時候就會返回固定的時間戳。
異步版本
public class CustomResourceAsyncFilterAttribute:Attribute,IAsyncResourceFilter
{
private static ConcurrentDictionary<string, Object> CacheDictionary = new ConcurrentDictionary<string, Object>();
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
string key = context.HttpContext.Request.Path.ToString().ToLower();
Console.WriteLine($"AsyncCustomResourceFilter: OnResourceExecuting - Checking cache for key: {key}");
// 1. 在 Action 執行前,檢查緩存
if (CacheDictionary.TryGetValue(key, out var cachedValue))
{
Console.WriteLine("AsyncCustomResourceFilter: Cache hit - Returning cached response.");
// 緩存命中,直接設置結果并短路,不調用 next()
context.Result = (IActionResult)cachedValue;
return; // 直接返回,后續代碼不執行
}
// 2. 緩存未命中,調用 next() 來執行后續的管道(包括 Action)
// next() 返回一個 ResourceExecutedContext,包含了執行完成后的結果
ResourceExecutedContext executedContext = await next();
// 3. 在 Action 執行后,將結果存入緩存
// 確保請求成功且結果不為空
if (executedContext.Result != null)
{
Console.WriteLine($"AsyncCustomResourceFilter: OnResourceExecuted - Caching response for key: {key}");
CacheDictionary[key] = executedContext.Result;
}
}
}
控制器的代碼
[HttpGet("ResourceAsyncFilter"), CustomResourceAsyncFilter]
public async Task<IActionResult> ResourceAsyncFilter()
{
// 直接返回一個簡單的字符串響應
// 在 Action 中打印日志,用于判斷 Action 是否被執行
Console.WriteLine(">>> Async Action Executed: Generating new server time...");
// 模擬一個異步操作,例如數據庫查詢或調用外部 API
await Task.Delay(100); // 暫停 100 毫秒
var result = new
{
message = "This is a fresh ASYNC response from the server.",
serverTime = DateTime.Now.ToString("o")
};
return Ok(result);
}
操作過濾器
- 接口:
IActionFilter/IAsyncFilter - 執行的時機:在
模型綁定完成之后,緊接著Action方法執行的前后運行。 - 核心的作用:這是最常用的過濾器,因為它能訪問到已經綁定和驗證好的模型數據(
ActionExecutingContext.ActionArguments),并且能操縱Action的執行結果。 - 常見的用途:
- 模型狀態驗證:檢查
ModelState.IsValid,如果模型數據無效,則提前返回400 Bad Request. - 記錄Action的執行日志,記錄Action的執行時間傳入傳出的參數等。
- 修改Action參數:在Action執行前,對參數進行一些修改或增強。
- 修改Action結果:在Action執行后,對返回的
IActionResult進行修改。
- 模型狀態驗證:檢查
- 一句話總結:
Action的方法貼身助理,負責處理與Action執行直接相關的雜務。
同步版的代碼:
public void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Items["start"]=Stopwatch.StartNew();
Console.WriteLine("CustomActionFilter: OnActionExecuting - Action is about to execute.");
// 可以在這里添加自定義邏輯,比如記錄日志、修改請求數據等
}
public void OnActionExecuted(ActionExecutedContext context)
{
Console.WriteLine("CustomActionFilter: OnActionExecuted - Action has executed.");
// 可以在這里添加自定義邏輯,比如記錄日志、修改響應數據等
var stopwatch = context.HttpContext.Items["start"] as Stopwatch;
if (stopwatch != null)
{
stopwatch.Stop();
Console.WriteLine($"Action executed in {stopwatch.ElapsedMilliseconds} ms");
}
else
{
Console.WriteLine("CustomActionFilter: Stopwatch not found in HttpContext.");
}
}
控制器的代碼:
[HttpGet("ActionFilter"),CustomActionFilter]
public IActionResult ActionFilter()
{
// 直接返回一個簡單的字符串響應
// 在 Action 中打印日志,用于判斷 Action 是否被執行
Console.WriteLine(">>> Action Executed: Generating new server time...");
// 返回一個每次都會變化的內容
var result = new
{
message = "This is a response from the server with Action Filter.",
serverTime = DateTime.Now.ToString("o") // "o" 格式包含毫秒,便于觀察
};
return Ok(result);
}
會顯示執行Action前后的時間。
異步版的代碼:
public class CustomAsyncActionFilterAttribute : Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var sw = Stopwatch.StartNew();
var executed = await next();
sw.Stop();
Console.WriteLine($"Async action {context.ActionDescriptor.DisplayName} took {sw.ElapsedMilliseconds}ms" );
}
}
控制器的代碼:
[HttpGet("AsyncActionFilter"), CustomAsyncActionFilter]
public async Task<IActionResult> ActionAsyncFilter()
{
// 直接返回一個簡單的字符串響應
// 在 Action 中打印日志,用于判斷 Action 是否被執行
Console.WriteLine(">>> Async Action Executed: Generating new server time...");
// 模擬一個異步操作,例如數據庫查詢或調用外部 API
await Task.Delay(100); // 暫停 100 毫秒
var result = new
{
message = "This is a fresh ASYNC response from the server with Action Filter.",
serverTime = DateTime.Now.ToString("o")
};
return Ok(result);
}
匿名Filter函數
系統支持的AllowAnoymous只能在后續的授權的Filter中去使用,對于我們自己擴展過的Filter,allowAnonymous不能直接使用。
支持Anonymous的匿名---在擴展Filter的時候,也要實現AllowAnonymou匿名。
- 擴展一個CustomAllowAnonymousAttribute
- CustomAllowAnonymousAttribute專門用來做匿名的
- 升級擴展的Filter,讓Filter來支持CustomAllowAnonymousAttribute匿名。
- 在擴展的Filter的內部進行判斷,是否標記了這個特性,否則自定義的Filter不生效。
ExceptionFilter
- 接口:
IExceptionFilter/IAsyncExceptionFilter - 執行時機:當控制器創建、模型綁定、過濾器或Action方法執行過程中拋出未被處理的異常時被觸發。
- 核心作用:提供一個集中的地方來捕獲和處理應用程序中的異常,避免將原始的錯誤堆棧信息暴露給客戶端。
- 常見用途:
- 全局異常處理:捕獲所有未處理的異常。
- 記錄異常日志:將異常信息詳細地記錄到日志系統中。
- 返回統一的錯誤響應:向客戶端返回一個標準化的、友好的錯誤信息(例如,
{"error":An internal server error occurred.}),而不是一個HTML錯誤頁面或堆棧跟蹤。
- 一句話總結:"安全網和事故報告員",專門處理意外情況。
同步版代碼:
filter的代碼:
public class CustomExceptionFilterAttribute:Attribute,IExceptionFilter
{
public void OnException(ExceptionContext context)
{
Console.WriteLine($"CustomExceptionFilter: An exception occurred in action {context.ActionDescriptor.DisplayName}.");
Console.WriteLine($"Exception Message: {context.Exception.Message}");
context.ExceptionHandled = true; // 標記異常已處理,防止進一步傳播
}
}
控制器的代碼
[HttpGet("ExceptionFilter"), CustomExceptionFilter]
public IActionResult ExceptionFilter()
{
Console.WriteLine(">>> Action Executed: About to throw an exception...");
// 故意拋出一個異常,觸發異常過濾器
throw new InvalidOperationException("This is a test exception thrown from the action.");
}
異步版代碼
Filter代碼:
public class CustomAsyncExceptionAttribute:Attribute, IAsyncExceptionFilter
{
public async Task OnExceptionAsync(ExceptionContext context)
{
await Task.Run(() =>
{
Console.WriteLine($"CustomAsyncExceptionFilter: An exception occurred in action {context.ActionDescriptor.DisplayName}.");
Console.WriteLine($"Exception Message: {context.Exception.Message}");
context.ExceptionHandled = true; // 標記異常已處理,防止進一步傳播
});
}
}
控制器的代碼
[HttpGet("AsyncExceptionFilter"), CustomAsyncExceptionFilter]
public async Task<IActionResult> AsyncExceptionFilter()
{
Console.WriteLine(">>> Async Action Executed: About to throw an exception...");
// 故意拋出一個異常,觸發異常過濾器
await Task.Run(() =>
{
throw new InvalidOperationException("This is a test exception thrown from the async action.");
});
return Ok(); // 這行代碼實際上不會被執行到
}
ResultFilter
- 接口:
IResultFilter/IAsyncResultFilter - 執行時機: 僅當 Action 方法成功執行并返回一個 ActionResult 之后,在結果被寫入響應體的前后運行。如果 Action 拋出異常或被其他過濾器短路,它不會執行。
- 核心作用: 對 Action 的執行結果進行最后的加工和處理。
- 常見用途:
- 統一響應格式包裝: 將所有成功的 API 響應數據包裝在一個標準的結構中,例如
{ "success": true, "data": ... }。 - 修改響應頭: 根據 Action 的結果動態地添加或修改響應頭。
- 格式化響應數據: 例如,對所有返回的 DateTime 類型進行統一的格式化。
- 一句話總結: “響應的化妝師”,負責在結果展示給客戶前進行最后的包裝和美化。
同步版代碼:
filter的代碼:
- 統一響應格式包裝: 將所有成功的 API 響應數據包裝在一個標準的結構中,例如
public class CustomResultFilterAttribute:Attribute, IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
Console.WriteLine("CustomResultFilter: OnResultExecuting - Result is about to be executed.");
// 可以在這里添加自定義邏輯,比如記錄日志、修改響應數據等
}
public void OnResultExecuted(ResultExecutedContext context)
{
Console.WriteLine("CustomResultFilter: OnResultExecuted - Result has been executed.");
// 可以在這里添加自定義邏輯,比如記錄日志、修改響應數據等
}
}
控制器的代碼
[HttpGet("ResultFilter"), CustomResultFilter]
public IActionResult ResultFilter()
{
Console.WriteLine(
">>> Action Executed: Generating new server time..."
);
var result = new
{
message = "This is a response from the server with Result Filter.",
serverTime = DateTime.Now.ToString("o") // "o" 格式包含毫秒,便于觀察
};
return Ok(result);
}
異步版的代碼
Filter的代碼
public class CustomAsyncResultFilterAttribute: Attribute, IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
Console.WriteLine("CustomAsyncResultFilter: OnResultExecutionAsync - Before result execution.");
// 可以在這里添加自定義邏輯,比如記錄日志、修改響應數據等
// 調用下一個過濾器或結果執行
await next();
Console.WriteLine("CustomAsyncResultFilter: OnResultExecutionAsync - After result execution.");
// 可以在這里添加自定義邏輯,比如記錄日志、修改響應數據等
}
}
控制器的代碼
[HttpGet("AsyncResultFilter"),CustomAsyncResultFilter]
public async Task<IActionResult> AsyncResultFilter()
{
Console.WriteLine(">>> Async Action Executed: Generating new server time...");
await Task.Delay(100); // 暫停 100 毫秒
var result = new
{
message = "This is a fresh ASYNC response from the server with Result Filter.",
serverTime = DateTime.Now.ToString("o")
};
return Ok(result);
}
IAlwaysRunResultFilter
IAlwaysRunResultFilter (和它的異步版本 IAsyncAlwaysRunResultFilter) 是一個非常有用的 Result Filter 接口,它的核心作用正如其名:一個保證總是會運行的 Result Filter。
“無論管道是否被前置的 Filter(如 Authorization, Resource, Action Filter)短路,只要一個 IActionResult 準備被執行,我這個 Filter 就必須運行。”
它忽略了短路信號,確保對最終要發往客戶端的任何 Result 對象都能進行處理。
Authorization Filter
- 接口:
IAuthorizationFilter/IAsyncAuthorizationFilter - 執行時機: 最早運行。在請求管道中位于所有其他過濾器之前。
- 核心作用: 決定當前用戶是否有權限訪問請求的資源。它的職責非常單一和明確:要么通過,要么拒絕。
- 常見用途:
- 身份驗證檢查: 確認用戶是否已登錄。
- 角色或策略檢查: 檢查用戶是否屬于某個角色(如 "Admin")或滿足某個策略(如 "Over18")。
- API 密鑰驗證: 檢查請求頭中是否包含有效的 API 密鑰。
- IP 白名單/黑名單: 檢查請求的來源 IP 是否被允許。
- 一句話總結: “看門人”,決定你是否能進門。
創建需要驗證的Model類
public class UserModel
{
public int Id { get; set; }
[Required(ErrorMessage ="用戶名為必填項")]
[Display(Name ="用戶名")]
public string Username { get; set; }
[Required(ErrorMessage ="密碼為必填項")]
[DataType(DataType.Password)]
[Display(Name ="密碼")]
public string Password { get; set; }
// 為了授權填加的字段
//
public string Role { get; set; }
public string Department { get; set; }
public DateTime BirthDate { get; set; }
public bool IsEmailVerified { get; set; }
public string Email { get; set; }
}
填加數字驗證碼的服務
接口代碼 ICaptchaService.cs
public interface ICaptchaService
{
(string text, byte[] imageBytes) GenerateCaptcha();
}
實現數字驗證的代碼:
要安裝SixLabors.ImageSharp和SixLabors.ImageSharp.Drawing的nuget包
public class CaptchaService:ICaptchaService
{
private const int ImageWidth = 150;
private const int ImageHeight = 50;
public (string text, byte[] imageBytes) GenerateCaptcha()
{
string captchaText = GenerateRandomText(4);
using (var image = new Image<Rgba32>(ImageWidth, ImageHeight))
{
// Draw the captcha text on the image
var font = SystemFonts.CreateFont("Arial", 32, FontStyle.Bold);
image.Mutate(ctx => ctx.Fill(Color.White));
var random = new Random();
for (int i = 0; i < 10; i++)
{
var startPoint = new PointF(random.Next(0, ImageWidth), random.Next(0, ImageHeight));
var endPoint = new PointF(random.Next(0, ImageWidth), random.Next(0, ImageHeight));
var color = Color.FromRgb((byte)random.Next(150, 256), (byte)random.Next(150, 256), (byte)random.Next(150, 256));
image.Mutate(ctx => ctx.DrawLine(color, 1, startPoint, endPoint));
}
for (int i = 0; i < captchaText.Length; i++)
{
char character = captchaText[i];
var location = new PointF(10 + i * 35, 5);
var color = Color.FromRgb((byte)random.Next(0, 150), (byte)random.Next(0, 150), (byte)random.Next(0, 150));
image.Mutate(ctx => ctx.DrawText(character.ToString(), font, color, location));
}
// 5. 將圖片保存到內存流
using (var ms = new MemoryStream())
{
image.SaveAsPng(ms);
return (captchaText, ms.ToArray());
}
}
}
private string GenerateRandomText(int length)
{
const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
var random = new Random();
var sb = new StringBuilder();
for (int i = 0; i < length; i++)
{
sb.Append(chars[random.Next(chars.Length)]);
}
return sb.ToString();
}
}
視圖的代碼
@* filepath: Views/Login/Login.cshtml *@
@model UserModel
@{
ViewData["Title"] = "登錄";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
<div class="col-md-4">
<form asp-action="Login" method="post">
@* 用于顯示摘要錯誤,如“用戶名或密碼無效” *@
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Username" class="control-label"></label>
<input asp-for="Username" class="form-control" autocomplete="username" />
<span asp-validation-for="Username" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Password" class="control-label"></label>
<input asp-for="Password" class="form-control" autocomplete="current-password" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
@* --- 新增字段開始 --- *@
<div class="form-group mb-3">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" autocomplete="email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Role" class="control-label"></label>
<input asp-for="Role" class="form-control" />
<span asp-validation-for="Role" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Department" class="control-label"></label>
<input asp-for="Department" class="form-control" />
<span asp-validation-for="Department" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="BirthDate" class="control-label"></label>
@* Tag Helper 會自動為 DateTime 生成 type="date" *@
<input asp-for="BirthDate" class="form-control" />
<span asp-validation-for="BirthDate" class="text-danger"></span>
</div>
<div class="form-group form-check mb-3">
@* 對于布爾值,使用 form-check 樣式 *@
<input class="form-check-input" asp-for="IsEmailVerified" />
<label class="form-check-label" asp-for="IsEmailVerified"></label>
<span asp-validation-for="IsEmailVerified" class="text-danger d-block"></span>
</div>
@* --- 新增字段結束 --- *@
<div class="form-group mb-3">
<label for="captchaCode" class="control-label">驗證碼</label>
<div class="input-group">
@*
關鍵點 1:
使用手動的 name="captchaCode" 和 id="captchaCode"。
name="captchaCode" 必須與 Controller Action 的參數名匹配。
*@
<input name="captchaCode" id="captchaCode" class="form-control" style="width: 120px;" autocomplete="off" />
<div class="input-group-append ms-2">
@*
關鍵點 2:
使用 <img> 標簽,其 src 指向返回圖片流的 Action。
*@
<img id="captcha-image" src="@Url.Action("GetCaptchaImage", "Login")"
alt="Captcha" style="cursor: pointer; border: 1px solid #ccc;" title="點擊刷新驗證碼" />
</div>
</div>
@*
關鍵點 3:
這個驗證標簽可以保留,它會顯示 ModelState 中鍵為 "CaptchaCode" 的錯誤。
*@
@Html.ValidationMessage("CaptchaCode", "", new { @class = "text-danger" })
</div>
<div class="form-group">
<input type="submit" value="登錄" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
<script>
document.addEventListener('DOMContentLoaded', function () {
const captchaImage = document.getElementById('captcha-image');
const captchaInput = document.getElementById('captchaCode');
if (captchaImage) {
captchaImage.addEventListener('click', function () {
// 通過給 URL 添加一個隨機的時間戳參數來強制瀏覽器重新加載圖片,避免緩存
const newSrc = '@Url.Action("GetCaptchaImage", "Login")?t=' + new Date().getTime();
this.src = newSrc;
// 刷新驗證碼后,清空用戶已輸入的文本
if(captchaInput) {
captchaInput.value = '';
}
});
}
});
</script>
}
填加退出的視圖代碼
@* 只在用戶登錄后顯示登出按鈕 *@
@if (User.Identity.IsAuthenticated)
{
<form asp-controller="Login" asp-action="Logout" method="post" class="form-inline">
<button type="submit" class="nav-link btn btn-link text-dark">登出</button>
</form>
}
控制器的代碼
LOGIN的控制器
public class LoginController : Controller
{
private readonly ILogger<LoginController> _logger;
private readonly ICaptchaService _captchaService;
private const string CaptchaSessionKey = "CaptchaCode";
public LoginController(ILogger<LoginController> logger, ICaptchaService captchaService)
{
_logger = logger;
_captchaService = captchaService;
}
[HttpGet]
public IActionResult Login()
{
return View();
}
[HttpGet]
public IActionResult GetCaptchaImage()
{
var (text, imageBytes) = _captchaService.GenerateCaptcha();
HttpContext.Session.SetString(CaptchaSessionKey, text);
_logger.LogInformation("Generated Captcha Image with text: {Captcha}", text);
return File(imageBytes, "image/png");
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(UserModel model, string captchaCode)
{
if (string.IsNullOrEmpty(captchaCode))
{
ModelState.AddModelError("CaptchaCode", "驗證碼為必填項。");
}
if (!ModelState.IsValid)
{
return View(model);
}
var sessionCaptcha = HttpContext.Session.GetString(CaptchaSessionKey);
if (sessionCaptcha == null || !sessionCaptcha.Equals(captchaCode, StringComparison.OrdinalIgnoreCase))
{
ModelState.AddModelError("CaptchaCode", "驗證碼不正確。");
return View(model);
}
if (model.Username.Equals("admin", StringComparison.OrdinalIgnoreCase) && model.Password == "123456")
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, model.Id.ToString()),
new Claim(ClaimTypes.Name, model.Username),
new Claim(ClaimTypes.Role, model.Role),
new Claim("Department", model.Department),
new Claim("BirthDate", model.BirthDate.ToString("yyyy-MM-dd")),
new Claim("IsEmailVerified", model.IsEmailVerified.ToString()),
new Claim(ClaimTypes.Email, model.Email)
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
_logger.LogInformation("User {UserName} logged in successfully.", model.Username);
return RedirectToAction("MyProfile", "User");
}
ModelState.AddModelError(string.Empty, "用戶名或密碼無效。");
return View(model);
}
[HttpPost] // 推薦使用 Post 防止 CSRF 攻擊
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
// 關鍵代碼:讓身份驗證 Cookie 失效
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
_logger.LogInformation("User logged out.");
// 登出后,通常重定向到主頁或登錄頁
return RedirectToAction("Index", "Home");
}
在Program.cs中
注冊服務:
// 注冊驗證碼服務為單例模式
builder.Services.AddSingleton<ICaptchaService, CaptchaService>();
// 添加分布式內存緩存,這是 Session 的基礎
builder.Services.AddDistributedMemoryCache();
// 添加 Session 服務并配置
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(3);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
// 添加 Cookie 認證服務
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login/Login";
options.AccessDeniedPath = "/Home/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromMinutes(3);
options.SlidingExpiration = true;
});
app.UseAuthorization(); //啟動認證。
1. [Authorize]常見用法
在登陸成功后就會跳轉新的控制器
[Authorize]
[Route("[controller]")]
public class UserController : Controller
{
[HttpGet("me")]
public IActionResult GetMyProfile()
{
if (User.Identity == null || !User.Identity.IsAuthenticated)
{
return Challenge(); // 如果未登錄,挑戰認證
}
// User 是 ControllerBase 提供的屬性,代表當前登錄的用戶
return Ok(new
{
Id = User.FindFirstValue(ClaimTypes.NameIdentifier),
Username = User.Identity.Name,
Department = User.FindFirstValue("Department"),
Role = User.FindFirstValue(ClaimTypes.Role),
BirthDate = User.FindFirstValue("BirthDate"),
IsEmailVerified = User.FindFirstValue("IsEmailVerified"),
Email = User.FindFirstValue(ClaimTypes.Email)
});
}
[HttpGet("all")]
[Authorize(Roles = "Admin,Manager")]
public IActionResult GetAllUsers()
{
// ... 返回所有用戶的邏輯 ...
return Ok("Returned all users (for Admins/Managers).");
}
[AllowAnonymous] // 允許匿名訪問,覆蓋了控制器級別的 [Authorize]
[HttpGet("public-announcement")]
public IActionResult GetPublicAnnouncement()
{
return Ok("This is a public announcement.");
}
}
這個代碼就顯示只有登陸是指定的角色才能訪問ALL方法和有認證才能訪問ME方法.
2. 常用 Policy 定義寫法與場景
在Program.cs(或Startup.cs)中配置策略
// 添加 Cookie 認證服務
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Login/Login";
options.AccessDeniedPath = "/Home/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromMinutes(3);
options.SlidingExpiration = true;
});
builder.Services.AddAuthorization(options =>
{
// 場景1: 基于角色 (等同于 [Authorize(Roles="Admin")])
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
// 場景2: 基于 Claim 的存在與否
// 要求用戶必須有 "department" 這個 Claim,值是什么無所謂
options.AddPolicy("HasDepartment", policy => policy.RequireClaim("Department"));
// 場景3: 基于 Claim 的具體值
// 要求用戶的 "email_verified" Claim 的值必須是 "True"
options.AddPolicy("EmailVerified", policy => policy.RequireClaim("IsEmailVerified", "True"));
// 場景4: 組合條件
// 要求用戶必須是 "Manager" 角色,并且屬于 "Sales" 或 "HR" 部門
options.AddPolicy("SalesOrHrManager", policy => policy
.RequireRole("Manager")
.RequireClaim("department", "Sales", "HR"));
// 場景5: 自定義條件 (將在下一節實現)
// 要求用戶必須年滿18歲
options.AddPolicy("Over18", policy =>
policy.AddRequirements(new MinimumAgeRequirement(18)));
});
控制器的代碼
[Authorize]
[Route("[controller]")]
public class ReportsController : Controller
{
// 必須是 Admin
[HttpGet("financial")]
[Authorize(Policy = "AdminOnly")]
public IActionResult GetFinancialReport() => Ok("Financial Report");
// 必須是銷售部或人力資源部的經理
[HttpGet("performance")]
[Authorize(Policy = "SalesOrHrManager")]
public IActionResult GetPerformanceReport() => Ok("Team Performance Report");
// 必須郵箱已驗證
[HttpGet("confidential-docs")]
[Authorize(Policy = "EmailVerified")]
public IActionResult GetConfidentialDocs() => Ok("Confidential Documents");
}
3. 自定義 Requirement + Handler
添加需求,要求大于18歲才能訪問
a. 定義 Requirement
它只是一個數據容器,標記需要滿足什么條件。
public class MinimumAgeRequirement:IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
b. 定義 Handler
這是真正的邏輯“回調”點,在這里檢查用戶是否滿足 Requirement
public class MinimumAgeHandler:AuthorizationHandler<Requirement.MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, Requirement.MinimumAgeRequirement requirement)
{
// 從當前用戶的 Claims 中查找 "birthdate"
var birthDateClaim = context.User.FindFirst(c => c.Type == "BirthDate");
if (birthDateClaim == null)
{
// 沒有找到 birthdate claim,無法判斷,直接返回
return Task.CompletedTask;
}
if (DateTime.TryParse(birthDateClaim.Value, out var birthDate))
{
var age = DateTime.Today.Year - birthDate.Year;
// 如果生日還沒到,年齡減一
if (birthDate.Date > DateTime.Today.AddYears(-age))
{
age--;
}
// 如果年齡滿足要求
if (age >= requirement.MinimumAge)
{
// 調用 Succeed,表示此 Requirement 已通過
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
c. 注冊 Handler
//添加新的policy
// 場景5: 自定義條件 (將在下一節實現)
// 要求用戶必須年滿18歲
options.AddPolicy("Over18", policy =>
policy.AddRequirements(new MinimumAgeRequirement(18)));
在Program.cs中注冊,以便系統能找到它。
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
添加控制器的代碼
// 必須年滿18歲
[HttpGet("adult-content")]
[Authorize(Policy = "Over18")]
public IActionResult GetAdultContent() => Ok("Adult Content");
4. 資源級授權 (Resource-based)
這個場景非常適合“用戶只能修改自己的信息,但管理員可以修改任何人的信息”。
a. 定義 Requirement和Operations
Operation:
public static class MyUserProfileOperations
{
public static readonly OperationAuthorizationRequirement Read =
new() { Name = nameof(Read) };
public static readonly OperationAuthorizationRequirement Update =
new() { Name = nameof(Update) };
}
Requirement:
public class MyUserProfileRequirement : IAuthorizationRequirement
{
public string Name { get; set; }
public MyUserProfileRequirement(string name)
{
Name = name;
}
}
b. 定義 Handler
Handler 會接收到要操作的資源實例 (UserModel)。
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MyUserProfileRequirement requirement, UserModel resource)
{
var loggedInUserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (loggedInUserId == null)
{
return Task.CompletedTask;
}
// 如果是管理員,允許任何操作
if (context.User.IsInRole("Admin"))
{
context.Succeed(requirement);
return Task.CompletedTask;
}
// 如果是用戶本人,允許讀取和更新自己的信息
if (resource.Id.ToString() == loggedInUserId)
{
if (requirement.Name == MyUserProfileOperations.Read.Name ||
requirement.Name == MyUserProfileOperations.Update.Name)
{
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
c. 注冊 Handler
builder.Services.AddSingleton<IAuthorizationHandler, MyUserProfileAuthorizationHandler>();
d. 在控制器中調用
你需要注入IAuthorizationService來手動觸發授權檢查。
public class UsersController : ControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IUserRepository _userRepository; // 假設有一個倉儲來獲取用戶
public UsersController(IAuthorizationService authorizationService, IUserRepository userRepository)
{
_authorizationService = authorizationService;
_userRepository = userRepository;
}
// ... 其他 Action ...
// GET: api/Users/5
[HttpGet("{id}")]
public async Task<IActionResult> GetUserById(int id)
{
var userToView = _userRepository.FindById(id);
if (userToView == null) return NotFound();
// 檢查當前登錄用戶是否有權限讀取目標用戶信息
var authorizationResult = await _authorizationService
.AuthorizeAsync(User, userToView, UserProfileOperations.Read);
if (!authorizationResult.Succeeded)
{
// 可以返回 Forbid() (403) 或 Challenge() (401)
return Forbid();
}
return Ok(userToView); // 授權成功,返回用戶信息
}
}
5. 動態策略提供器 (高級可選)
假設你想創建形如[Authorize(Policy = "IsInDepartment:Sales")]的動態策略,而不想預先定義所有部門。
a. 實現 IAuthorizationPolicyProvider
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
public class DepartmentPolicyProvider : IAuthorizationPolicyProvider
{
private const string POLICY_PREFIX = "IsInDepartment:";
private readonly DefaultAuthorizationPolicyProvider _fallbackProvider;
public DepartmentPolicyProvider(IOptions<AuthorizationOptions> options)
{
_fallbackProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => _fallbackProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync() => _fallbackProvider.GetFallbackPolicyAsync();
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase))
{
var department = policyName.Substring(POLICY_PREFIX.Length);
var policy = new AuthorizationPolicyBuilder();
policy.RequireClaim("department", department);
return Task.FromResult<AuthorizationPolicy?>(policy.Build());
}
return _fallbackProvider.GetPolicyAsync(policyName);
}
}
b. 注冊提供器 (替換默認的)
builder.Services.AddSingleton<IAuthorizationPolicyProvider, DepartmentPolicyProvider>();
c. 使用
[Authorize(Policy = "IsInDepartment:IT")]
[ApiController]
[Route("api/it-assets")]
public class ItAssetsController : ControllerBase
{
[HttpGet]
public IActionResult GetItAssets() => Ok("List of servers and laptops.");
}
6.自定義未授權/拒絕返回 (401/403)
如果你不希望默認跳轉到登錄頁,而是返回 JSON,可以配置認證處理器事件。
builder.Services.AddAuthentication("Cookies")
.AddCookie("Cookies", options =>
{
// ...
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
var json = System.Text.Json.JsonSerializer.Serialize(new { message = "用戶未登錄或認證已過期。" });
return context.Response.WriteAsync(json);
};
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = StatusCodes.Status403Forbidden;
context.Response.ContentType = "application/json";
var json = System.Text.Json.JsonSerializer.Serialize(new { message = "權限不足,無法訪問此資源。" });
return context.Response.WriteAsync(json);
};
});
過濾器的作用和意義:優雅地解決問題
過濾器允許您將這些橫切關注點從業務邏輯中抽離出來,放到獨立的、可重用的類中。ASP.NET Core 的請求處理管道會在合適的時機自動調用這些過濾器。
這帶來的核心意義是:
關注點分離 (Separation of Concerns):
控制器 (Controller) 只負責業務流程的編排。
服務 (Service) 只負責核心的業務邏輯實現。
過濾器 (Filter) 只負責處理通用的橫切關注點。
各司其職,代碼結構變得極其清晰。
代碼重用和可維護性:
同一個過濾器(例如日志過濾器)可以輕松地應用到整個應用程序、某個控制器或單個 Action 上。
修改一個功能(如異常處理邏輯),只需要修改對應的過濾器類即可,所有地方都會生效。
聲明式編程 (Declarative Programming):
您可以通過在控制器或 Action 上添加一個特性 (Attribute)(如 [Authorize] 或 [TypeFilter(typeof(MyFilter))])來“聲明”它需要某種行為,而不需要關心這個行為是如何實現的。這讓代碼的意圖更加明顯。
關于Filter的作用范圍:
- 標記在Action上,僅對當前的Action函數有效。
- 標記在控制器上,對于當前控制器下的所有的Action有效。
- 全局進行標記,對于整個項目的所有函數都生效。
builder.Services.AddControllers(option => option.Filters.Add<CustomResourceAsyncFilterAttribute>());

浙公網安備 33010602011771號