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的代码:
- 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();
- }
- }
复制代码 视图的代码填加退出的视图代码- @* 只在用户登录后显示登出按钮 *@
- @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;
- }
- [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, 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
- [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 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. 使用- [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>());
复制代码 来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |