【问题标题】:Conditional AccessDeniedPath in ASP.NET Core 3.1ASP.NET Core 3.1 中的条件 AccessDeniedPath
【发布时间】:2021-03-23 15:48:01
【问题描述】:

我的项目有两个控制器来支持不同角色的用户 - 成员顾问。登录时我设置了“角色”ClaimType 每个人。

会员和顾问有不同的登录页面,登录后 MemberControllerConsultantController 都会重定向到“桌面”操作。

CONSULTANTCONTROLLER.CS

    [HttpPost()]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> SignIn(SignIn sin)
    {
        try
        {
            // check authorisation
            if (ModelState.IsValid)
            {
                sin = await RepoSamadhi.ShopSignIn(sin);
                if (sin.ShopID == 0 || sin.IsValidationFail || string.IsNullOrEmpty(sin.ShopToken))
                {
                    is_err = true;
                    _logger.LogInformation("Consultant SignIn Invalid Credentials", sin.EmailAddress);                        
                    ModelState.AddModelError("Consultant", "Account not found. Check your credentials.");
                }
            }                
            else
            {
                sin.IsSignInFailed = true;
                return View("SignIn", sin);
            }

            // create claims
            var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Sid, sin.ShopToken),
            new Claim(ClaimTypes.NameIdentifier, sin.ShopID.ToString()),
            new Claim(ClaimTypes.Email, sin.EmailAddress.ToLower()),
            new Claim(ClaimTypes.Role, "Consultant")
        };

            // create identity
            var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // cookie or local            

            // create principal
            ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));

            // create auth properties
            var authProperties = new AuthenticationProperties
            {
                IsPersistent = sin.RememberMe;
            };

            // sign-in
            await HttpContext.SignInAsync(scheme: CookieAuthenticationDefaults.AuthenticationScheme, principal: principal, properties: authProperties);
        }
        catch (Exception ex)
        {
            gFunc.ProcessError(ex);
        }
        return RedirectToAction("Desktop", new { date = DateTime.Today.ToString("d MMM yyyy"), timer = false });
    }

STARTUP.CS

    public void ConfigureServices(IServiceCollection services)
    {
        try
        {
            services.AddRazorPages()
                .AddRazorRuntimeCompilation();

            services.AddControllersWithViews();

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                {
                    options.ExpireTimeSpan = new TimeSpan(30, 0, 0, 0);
                    options.LoginPath = new PathString("/Home/Index/");
                    options.AccessDeniedPath = new PathString("/Home/Index/");
                    options.LogoutPath = new PathString("/Home/Index/");
                    options.Validate();
                });

            services.Configure<Microsoft.AspNetCore.Identity.IdentityOptions>(options =>
            {
                options.Password.RequireDigit = true;
                options.Password.RequireLowercase = true;
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireUppercase = true;
                options.Password.RequiredLength = 8;
                options.Password.RequiredUniqueChars = 1;
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
                options.Lockout.MaxFailedAccessAttempts = 5;
                options.Lockout.AllowedForNewUsers = true;
                options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
                options.User.RequireUniqueEmail = false;
            });

            // add detection services container and device resolver service
            services.AddDetectionCore()
                .AddDevice();

            services.AddMvc();
            services.AddAntiforgery();
            services.Configure<MvcOptions>(options =>
            {
                options.Filters.Add(new RequireHttpsAttribute());
            });
        }
        catch (Exception ex)
        {
            gFunc.ProcessError(ex);
        }
    }

问题

当用户尝试访问授权资源但未登录(即没有有效的身份验证 cookie)时,如何配置身份验证服务以将用户重定向到正确的登录页面?目前我只有一个“AccessDeniedPath”,它将用户带到主页。

【问题讨论】:

    标签: c# visual-studio asp.net-core cookie-authentication


    【解决方案1】:

    我尝试了 King King 的方法,通过自定义 CookieAuthenticationHandler 来覆盖 HandleForbiddenAsync,但代码永远不会执行。

    这是因为尚未登录的用户是“未授权”。如果他们尝试访问 [Authorize] 资源,用户将被定向到 LoginPath,而不是 AccessDeniedPath。这对应于 HTTP 请求的 401。

    如果用户已经登录,则“禁止”,但他们使用的身份无权查看请求的资源,对应于 HTTP 中的 403。

    在 MS 文档中:“AccessDeniedPath 获取或设置如果用户不批准远程服务器请求的授权请求,则用户代理重定向到的可选路径。此属性不是由默认情况下,如果远程授权服务器返回access_denied响应,则抛出异常。”

    因此,在登录并随后请求没有所需角色的受保护资源(即用 [Authorize(Roles = "MyRole")] 修饰的操作)后,应将其重定向到配置的 AccessDeniedPath。在这种情况下,我应该能够使用King King的方法。

    解决方案

    最后,我只是为 CookieAuthenticationOptions 事件 (OnRedirectToLogin) 添加了一个委托。

    我已经更新了以下代码以合并来自 KingKing 的反馈/cmets。这包括使用 StartsWithSegments 而不仅仅是 Path.ToString().Contains。

    另外按照 KK 的建议,我捕获了默认回调,然后在返回中使用它。

    services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
    {
         o.ExpireTimeSpan = new TimeSpan(90, 0, 0, 0);
         o.AccessDeniedPath = new PathString("/Samadhi/SignIn/");
         o.LoginPath = new PathString("/Samadhi/SignIn/");
         o.LogoutPath = new PathString("/Samadhi/SignOut/");
         var defaultCallback = o.Events.OnRedirectToLogin;
         o.Events.OnRedirectToLogin = context =>
         {
              if (context.Request.Path.StartsWithSegments(new PathString("/member"), StringComparison.OrdinalIgnoreCase))
              {
                   context.RedirectUri = "/Member/SignIn/";
                   context.Response.Redirect(context.RedirectUri);
              }
              else if (context.Request.Path.StartsWithSegments(new PathString("/consultant"), StringComparison.OrdinalIgnoreCase))
              {
                   context.RedirectUri = "/Consultant/SignIn/";
                   context.Response.Redirect(context.RedirectUri);
              }
              return defaultCallback(context);
        };
        o.Validate();
    });
    

    【讨论】:

    • 重定向 uri 可以是相对路径(以主机地址为根)但必须以 / 开头,因此在您的情况下,您应该使用 /member/SignIn ... 还有您的 @987654325 逻辑@ 不是很安全,它可能会选择错误的路径来处理。因此,您需要完全匹配路径以处理您想要的正确路径。如果您不想手动找到匹配项,则可能需要为此使用Regex(在性能更高和更不方便之间进行权衡)。
    • 我删除了我的答案,因为实际上Options 是共享的,所以应该禁止修改它。以下是我对您的代码的原始评论(来自我的回答):...
    • 就像在您重新实现回调时一样,您缺乏支持 ajax 请求重定向的情况,正如此处github.com/dotnet/aspnetcore/blob/… 的默认实现所处理的那样 - 这就是我们应该获取默认值并调用它的原因在我们的回调中更改 context.Uri 之后。将来,默认值可能会有其他我们不知道的附加逻辑,但我们的代码仍然不需要相应地更新
    • @KingKing 奇怪,当我尝试实现静态 IsAjaxRequest 方法时,我收到一个编译错误,提示 HeaderNames 不包含“XRequestedWith”的定义。在我的项目中 Microsoft.Net.Http.Headers 显示程序集版本 3.1.0.0。
    • XRequestedWith.NET 5 中是新的,所以你当然不能在.net core 3.1 中使用它。您可以改用 X-Requested-With 字符串。但是,正如我所说,您不应该将那里的代码复制到您的实现中。只需先捕获默认回调,然后在您的实现中调用该回调。例如:var defaultCallback = o.Events.OnRedirectToLogin; o.Events.OnRedirectToLogin = context =&gt; { .... return defaultCallback(context); };
    【解决方案2】:

    在我看来,主要问题是如果用户不包含令牌,您如何知道当前登录用户?

    在我看来,我建议您首先可以使用主登录页面。然后如果用户输入了用户名,你可以使用 ajax 之类的 js 来检查服务器中的用户名或电子邮件。

    如果用户是 Members ,那么您可以在 ajax 成功方法中编写逻辑,将用户重定向到 Members 登录页面。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-03-15
      • 2021-01-29
      • 2018-07-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-22
      • 1970-01-01
      相关资源
      最近更新 更多