You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 8 Identity:认证后强制用户设置时区的实现方法

在ASP.NET Core 8 Identity中强制用户完成时区设置的实现方案

要解决用户绕过页面检查直接访问应用的问题,核心是全局拦截已认证用户的请求,确保未完成时区设置的用户只能访问设置页面。以下是两种可靠的实现方式:


方法一:使用自定义中间件(全局拦截)

中间件可以插入到请求管道中,对所有已认证用户的请求进行检查,从根源上限制未设置时区用户的访问范围。

1. 编写时区检查中间件

public class RequireTimeZoneMiddleware
{
    private readonly RequestDelegate _next;
    private readonly PathString _setupPath;

    public RequireTimeZoneMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _setupPath = config.GetValue<string>("TimeZoneSetupPath") ?? "/Identity/Account/SetTimeZone";
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // 跳过未认证用户、设置页、静态资源请求
        if (!context.User.Identity.IsAuthenticated)
        {
            await _next(context);
            return;
        }

        var requestPath = context.Request.Path;
        if (requestPath.StartsWithSegments(_setupPath) || 
            requestPath.StartsWithSegments("/_framework") || 
            requestPath.StartsWithSegments("/css") || 
            requestPath.StartsWithSegments("/js"))
        {
            await _next(context);
            return;
        }

        // 检查用户是否已设置时区(优先从Claim读取,避免重复查库)
        var hasTimeZone = context.User.HasClaim(c => c.Type == "TimeZoneId");
        // 若未添加Claim,可直接查数据库:
        // var userManager = context.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
        // var user = await userManager.GetUserAsync(context.User);
        // var hasTimeZone = !string.IsNullOrEmpty(user?.TimeZoneId);

        if (!hasTimeZone)
        {
            context.Response.Redirect(_setupPath);
            return;
        }

        await _next(context);
    }
}

// 扩展方法简化注册
public static class RequireTimeZoneMiddlewareExtensions
{
    public static IApplicationBuilder UseRequireTimeZone(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequireTimeZoneMiddleware>();
    }
}

2. 注册中间件到请求管道

在Program.cs中,将中间件放在认证和授权中间件之后,确保只拦截已认证用户:

var app = builder.Build();

// ...其他中间件(如静态文件、异常处理)

app.UseAuthentication();
app.UseAuthorization();

// 注册时区检查中间件
app.UseRequireTimeZone();

app.MapRazorPages();
app.MapControllers();

app.Run();

方法二:使用自定义授权策略(灵活控制范围)

通过自定义授权要求,可全局或针对特定页面/控制器强制用户完成时区设置,适合需要精细化权限控制的场景。

1. 编写授权要求与处理器

// 定义授权要求
public class HasTimeZoneRequirement : IAuthorizationRequirement { }

// 实现授权逻辑
public class HasTimeZoneHandler : AuthorizationHandler<HasTimeZoneRequirement>
{
    private readonly UserManager<ApplicationUser> _userManager;

    public HasTimeZoneHandler(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HasTimeZoneRequirement requirement)
    {
        var user = await _userManager.GetUserAsync(context.User);
        if (user != null && !string.IsNullOrEmpty(user.TimeZoneId))
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }
    }
}

2. 注册授权策略

在Program.cs中配置授权策略,可选择全局默认应用或单独指定:

builder.Services.AddAuthorization(options =>
{
    // 注册自定义策略
    options.AddPolicy("RequireTimeZone", policy =>
        policy.Requirements.Add(new HasTimeZoneRequirement()));

    // 可选:将该策略设为全局默认,所有已认证用户必须满足
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddRequirements(new HasTimeZoneRequirement())
        .Build();
});

// 注册授权处理器
builder.Services.AddScoped<IAuthorizationHandler, HasTimeZoneHandler>();

3. 处理授权失败重定向

配置Cookie认证事件,将授权失败的用户重定向到时区设置页:

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(options =>
{
    options.ApplicationCookie.Events = new CookieAuthenticationEvents
    {
        OnRedirectToAccessDenied = context =>
        {
            if (context.User.Identity.IsAuthenticated)
            {
                context.Response.Redirect("/Identity/Account/SetTimeZone");
            }
            else
            {
                context.Response.Redirect(context.RedirectUri);
            }
            return Task.CompletedTask;
        }
    };
});

优化建议:将时区信息加入用户Claim

为避免每次请求都查询数据库,可将用户时区信息加入Claims,提升性能:

1. 自定义Claims生成工厂

public class CustomUserClaimsFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
    public CustomUserClaimsFactory(UserManager<ApplicationUser> userManager, IOptions<IdentityOptions> options) 
        : base(userManager, options) { }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
    {
        var identity = await base.GenerateClaimsAsync(user);
        if (!string.IsNullOrEmpty(user.TimeZoneId))
        {
            identity.AddClaim(new Claim("TimeZoneId", user.TimeZoneId));
        }
        return identity;
    }
}

2. 注册自定义工厂

builder.Services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomUserClaimsFactory>();

时区设置页核心逻辑

设置完成后需更新用户信息并刷新登录状态,确保后续请求不再被拦截:

public class SetTimeZoneModel : PageModel
{
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly SignInManager<ApplicationUser> _signInManager;

    public SetTimeZoneModel(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
    {
        _userManager = userManager;
        _signInManager = signInManager;
    }

    [BindProperty]
    public string TimeZoneId { get; set; }

    public IEnumerable<string> AvailableTimeZones => TimeZoneInfo.GetSystemTimeZones().Select(tz => tz.Id);

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await _userManager.GetUserAsync(User);
        if (user != null && !string.IsNullOrEmpty(user.TimeZoneId))
        {
            return RedirectToPage("/Index");
        }
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid) return Page();

        var user = await _userManager.GetUserAsync(User);
        if (user == null) return NotFound();

        user.TimeZoneId = TimeZoneId;
        var result = await _userManager.UpdateAsync(user);
        
        if (result.Succeeded)
        {
            await _signInManager.RefreshSignInAsync(user); // 刷新Claims
            return RedirectToPage("/Index");
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
        return Page();
    }
}

内容的提问来源于stack exchange,提问作者kj49

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 04:57:12