c#.net的学习(二)

ASP.CORE的学习(二)

Filter的使用

简单来说,过滤器的作用和意义在于提供一种机制,用于在请求处理管道的特定阶段执行代码,从而优雅地处理那些“横切关注点” (Cross-Cutting Concerns)。
常见的过滤器的执行顺序
请求 -> [授权] -> [资源] -> [操作] -> Action方法 -> [操作] -> [结果] -> [资源] -> 响应
异常过滤器像一个安全网,在上述任何一个环节(授权之后)出错时都会被触发。

ResoureFilter

  • 接口:IResourceFilterIAsyncResourceFilter
  • 执行时机:在授权之后,但在模型绑定之前运行。它会"环绕"后续大部分管道,所以在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匿名。

  1. 扩展一个CustomAllowAnonymousAttribute
  2. CustomAllowAnonymousAttribute专门用来做匿名的
  3. 升级扩展的Filter,让Filter来支持CustomAllowAnonymousAttribute匿名。
  4. 在扩展的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.ImageSharpSixLabors.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的作用范围:

  1. 标记在Action上,仅对当前的Action函数有效。
  2. 标记在控制器上,对于当前控制器下的所有的Action有效。
  3. 全局进行标记,对于整个项目的所有函数都生效。
builder.Services.AddControllers(option => option.Filters.Add<CustomResourceAsyncFilterAttribute>());
posted @ 2025-08-28 15:44  飘雨的河  阅读(19)  评论(0)    收藏  举报