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 = context.Result;
}
}
}在控制器的代码
")]
public class FilterController : ControllerBase
{
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 = executedContext.Result;
}
}
}控制器的代码
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.");
}
}控制器的代码:
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" );
}
}控制器的代码:
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; // 标记异常已处理,防止进一步传播
}
}控制器的代码
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; // 标记异常已处理,防止进一步传播
});
}
}控制器的代码
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的代码:
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.");
// 可以在这里添加自定义逻辑,比如记录日志、修改响应数据等
}
}控制器的代码
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.");
// 可以在这里添加自定义逻辑,比如记录日志、修改响应数据等
}
}控制器的代码
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; }
public string Username { get; set; }
public string Password { get; set; }
// 为了授权填加的字段
//
publicstring Role { get; set; }
publicstringDepartment { get; set; }
publicDateTime BirthDate { get; set; }
publicboolIsEmailVerified { get; set; }
publicstring 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;
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);
}
return sb.ToString();
}
}视图的代码
@* filepath: Views/Login/Login.cshtml *@
@model UserModel
@{
ViewData["Title"] = "登录";
}
<h1>@ViewData["Title"]</h1>
<form asp-action="Login" method="post">
@* 用于显示摘要错误,如“用户名或密码无效” *@
<label asp-for="Username" ></label>
<input asp-for="Username"autocomplete="username" />
<label asp-for="Password" ></label>
<input asp-for="Password"autocomplete="current-password" />
@* --- 新增字段开始 --- *@
<label asp-for="Email" ></label>
<input asp-for="Email"autocomplete="email" />
<label asp-for="Role" ></label>
<input asp-for="Role"/>
<label asp-for="Department" ></label>
<input asp-for="Department"/>
<label asp-for="BirthDate" ></label>
@* Tag Helper 会自动为 DateTime 生成 type="date" *@
<input asp-for="BirthDate"/>
@* 对于布尔值,使用 form-check 样式 *@
<inputasp-for="IsEmailVerified" />
<labelasp-for="IsEmailVerified"></label>
@* --- 新增字段结束 --- *@
<label for="captchaCode" >验证码</label>
@*
关键点 1:
使用手动的 name="captchaCode" 和 id="captchaCode"。
name="captchaCode" 必须与 Controller Action 的参数名匹配。
*@
<input name="captchaCode" id="captchaCode" autocomplete="off" />
@*
关键点 2:
使用 <img> 标签,其 src 指向返回图片流的 Action。
*@
<img id="captcha-image" src="https://www.cnblogs.com/@Url.Action("GetCaptchaImage", "Login")"
alt="Captcha"title="点击刷新验证码" />
@*
关键点 3:
这个验证标签可以保留,它会显示 ModelState 中键为 "CaptchaCode" 的错误。
*@
@Html.ValidationMessage("CaptchaCode", "", new { @class = "text-danger" })
<input type="submit" value="登录"/>
</form>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}填加退出的视图代码
@* 只在用户登录后显示登出按钮 *@
@if (User.Identity.IsAuthenticated)
{
<form asp-controller="Login" asp-action="Logout" method="post" >
<button type="submit" >登出</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;
}
public IActionResult Login()
{
return View();
}
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");
}
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);
}
// 推荐使用 Post 防止 CSRF 攻击
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. 常见用法
在登陆成功后就会跳转新的控制器
")]
public class UserController : Controller
{
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)
});
}
public IActionResult GetAllUsers()
{
// ... 返回所有用户的逻辑 ...
return Ok("Returned all users (for Admins/Managers).");
}
// 允许匿名访问,覆盖了控制器级别的
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: 基于角色 (等同于 )
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)));
});控制器的代码
")]
public class ReportsController : Controller
{
// 必须是 Admin
public IActionResult GetFinancialReport() => Ok("Financial Report");
// 必须是销售部或人力资源部的经理
public IActionResult GetPerformanceReport() => Ok("Team Performance Report");
// 必须邮箱已验证
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岁
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, MinimumAgeHandler>();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
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. 动态策略提供器 (高级可选)
假设你想创建形如的动态策略,而不想预先定义所有部门。
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 options)
{
_fallbackProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task GetDefaultPolicyAsync() => _fallbackProvider.GetDefaultPolicyAsync();
public Task GetFallbackPolicyAsync() => _fallbackProvider.GetFallbackPolicyAsync();
public Task 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(policy.Build());
}
return _fallbackProvider.GetPolicyAsync(policyName);
}
}b. 注册提供器 (替换默认的)
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();c. 使用
public class ItAssetsController : ControllerBase
{
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)(如 或 )来“声明”它需要某种行为,而不需要关心这个行为是如何实现的。这让代码的意图更加明显。
关于Filter的作用范围:
[*]标记在Action上,仅对当前的Action函数有效。
[*]标记在控制器上,对于当前控制器下的所有的Action有效。
[*]全局进行标记,对于整个项目的所有函数都生效。
builder.Services.AddControllers(option => option.Filters.Add<CustomResourceAsyncFilterAttribute>());
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除
页:
[1]