【问题标题】:How to redirect after Azure AD authentication to different controller action in ASP Net Core MVCAzure AD 身份验证后如何重定向到 ASP Net Core MVC 中的不同控制器操作
【发布时间】:2017-08-10 15:56:28
【问题描述】:

我已经设置了我的 ASP Net Core 2.0 项目以使用 Azure AD 进行身份验证(使用 VS2017 中使用 OIDC 的标准 Azure AD 身份验证模板)。一切正常,应用程序返回到基本 url (/) 并在身份验证成功后运行 HomeController.Index 操作。

但是我现在想在身份验证后重定向到不同的控制器操作(AccountController.CheckSignIn),以便我可以检查用户是否已经存在于我的本地数据库表中,如果不存在(即它是新用户)创建一个本地用户记录然后重定向到 HomeController.Index 操作。

我可以将此检查放入 HomeController.Index 操作本身,但我想避免每次用户单击主页按钮时运行此检查。

这里有一些代码 sn-ps 可能有助于清晰...

appsettings.json 中的 AAD 设置

"AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<my-domain>.onmicrosoft.com",
    "TenantId": "<my-tennant-id>",
    "ClientId": "<my-client-id>",
    "CallbackPath": "/signin-oidc" // I don't know where this goes but it doesn't exist anywhere in my app and authentication fails if i change it
}

我向我的 AccountController.CheckSignIn 添加了一个新操作来处理此要求,但我找不到在身份验证后调用它的方法。

public class AccountController : Controller
{
    // I want to call this action after authentication is successful
    // GET: /Account/CheckSignIn
    [HttpGet]
    public IActionResult CheckSignIn()
    {
        var provider = OpenIdConnectDefaults.AuthenticationScheme;
        var key = User.FindFirstValue(ClaimTypes.NameIdentifier);
        var info = new ExternalLoginInfo(User, provider, key, User.Identity.Name);
        if (info == null)
        {
            return BadRequest("Something went wrong");
        }

        var user = new ApplicationUser { UserName = User.Identity.Name };
        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (!result.Succeeded)
            {
                return BadRequest("Something else went wrong");
            }
        }

        return RedirectToAction(nameof(HomeController.Index), "Home");
    }

    // This action only gets called when user clicks on Sign In link but not when user first navigates to site
    // GET: /Account/SignIn
    [HttpGet]
    public IActionResult SignIn()
    {
        return Challenge(
            new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
    }

}

【问题讨论】:

    标签: authentication asp.net-core asp.net-core-mvc azure-active-directory asp.net-core-identity


    【解决方案1】:

    我找到了一种通过使用重定向使其工作的方法,如下所示...

    内部启动

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Account}/{action=SignIn}/{id?}");
    });
    

    AccountController 内部

    // GET: /Account/CheckSignIn
    [HttpGet]
    [Authorize]
    public IActionResult CheckSignIn()
    {
        //add code here to check if AzureAD identity exists in user table in local database
        //if not then insert new user record into local user table
    
        return RedirectToAction(nameof(HomeController.Index), "Home");
    }
    
    //
    // GET: /Account/SignIn
    [HttpGet]
    public IActionResult SignIn()
    {
        return Challenge(
            new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
    }
    

    在 AzureAdServiceCollectionExtensions (.net core 2.0) 中

    private static Task RedirectToIdentityProvider(RedirectContext context)
    {
        if (context.Request.Path != new PathString("/"))
        {
            context.Properties.RedirectUri = new PathString("/Account/CheckSignIn");
        }
        return Task.FromResult(0);
    }
    

    【讨论】:

    • 看来您提倡使用正确的工具完成正确的工作,我喜欢。
    【解决方案2】:

    默认行为是:用户将被重定向到原始页面。例如,用户未认证访问Index页面,认证后重定向到Index页面;用户未通过身份验证访问联系人页面,通过身份验证后,将被重定向到联系人页面。

    作为一种解决方法,您可以修改默认网站路由以将用户重定向到特定控制器/操作:

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Account}/{action=CheckSignIn}/{id?}"
        );
    });
    

    在您的自定义逻辑之后,您可以将用户重定向到您真正的默认页面(主页/索引)。

    【讨论】:

    • 我喜欢这种方法的简单性,但如果用户加载特定页面(即主页/联系人)会怎样。如果他们直接加载主页/联系页面,我如何确保首先调用 Account/CheckSignIn 操作?
    • 您是否使用 [Authorize] 属性来保护您的控制器或操作?如果你使用那个,使用应该被重定向到azure广告登录页面,经过身份验证后,他将被重定向到Account/CheckSignIn。
    • 是的,我正在使用 [Authorize] 属性。问题是,如果我有你所展示的默认路由设置,那么这种方法只有在我加载默认站点(即 mysite.com)时才有效。但是,如果我输入 url mysite.com/Home/About,那么默认路由将被绕过,并且永远不会调用 Account/CheckSignIn。有没有办法解决这个问题?
    【解决方案3】:

    我想检查用户是否存在于我的本地数据库中,不仅是在选择 登录 时,而且在点击我网站的任何其他需要身份验证的链接时强>。

    经过多次反复试验,我找到了解决方案。不确定这是否是最佳解决方案,但它确实有效。

    基本上,我将 Authorize 属性与[Authorize(Policy = "HasUserId")] 中的策略一起使用,如Claims-based authorization in ASP.NET Core 中所述。 现在,当不符合政策时,您可以重新路由到注册操作。

    AccountController 的一个非常简化的版本如下所示(我使用 LogOn 操作而不是 SignIn 来防止与 AzureADB2C AccountController 发生冲突):

        public class AccountController : Controller
        {
            public IActionResult AccessDenied([FromQuery] string returnUrl)
            {
                if (User.Identity.IsAuthenticated)
                    return RedirectToAction(nameof(Register), new { returnUrl });
    
                return new ActionResult<string>($"Access denied: {returnUrl}").Result;
            }
    
            public IActionResult LogOn()
            {
                // TODO: set redirectUrl to the view you want to show when a registerd user is logged on.
                var redirectUrl = Url.Action("Test");
                return Challenge(
                    new AuthenticationProperties { RedirectUri = redirectUrl },
                    AzureADB2CDefaults.AuthenticationScheme);
            }
    
            // User must be authorized to register, but does not have to meet the policy:
            [Authorize]
            public string Register([FromQuery] string returnUrl)
            {
                // TODO Register user in local database and after successful registration redirect to returnUrl.
                return $"This is the Account:Register action method. returnUrl={returnUrl}";
            }
    
            // Example of how to use the Authorize attribute with a policy.
            // This action will only be executed whe the user is logged on AND registered.
            [Authorize(Policy = "HasUserId")]
            public string Test()
            {
                return "This is the Account:Test action method...";
            }
        }
    

    在 Startup.cs 的 ConfigureServices 方法中,设置 AccessDeniedPath:

    services.Configure<CookieAuthenticationOptions>(AzureADB2CDefaults.CookieScheme,
        options => options.AccessDeniedPath = new PathString("/Account/AccessDenied/"));
    

    实现 HasUserId 策略的一种快速而简单的方法是将本地数据库中的 UserId 添加为 CookieAuthenticationOptions 的 OnSigningIn 事件中的声明,然后使用 RequireClaim 检查 UserId 声明。但是因为我需要我的数据上下文(具有作用域的生命周期),所以我使用了 AuthorizationRequirement 和 AuthorizationHandler(参见Authorization Requirements):

    在这种情况下,AuthorizationRequirement 只是一个空标记类:

        using Microsoft.AspNetCore.Authorization;
        namespace YourAppName.Authorization
        {
            public class HasUserIdAuthorizationRequirement : IAuthorizationRequirement
            {
            }
        }
    

    AuthorizationHandler 的实现:

        public class HasUserIdAuthorizationHandler : AuthorizationHandler<HasUserIdAuthorizationRequirement>
        {
            // Warning: To make sure the Azure objectidentifier is present,
            // make sure to select in your Sign-up or sign-in policy (user flow)
            // in the Return claims section: User's Object ID.
            private const string ClaimTypeAzureObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
    
            private readonly IUserService _userService;
    
            public HasUserIdAuthorizationHandler(IUserService userService)
            {
                _userService = userService;
            }
    
            protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HasUserIdAuthorizationRequirement requirement)
            {
                // Load User Id from database:
                var azureObjectId = context.User?.FindFirst(ClaimTypeAzureObjectId)?.Value;
                var userId = await _userService.GetUserIdForAzureUser(azureObjectId);
                if (userId == 0)
                    return;
    
                context.Succeed(requirement);
            }
        }
    

    _userService.GetUserIdForAzureUser 在数据库中搜索现有的 UserId,连接到 azureObjectId 并在未找到或 azureObjectId 为 null 时返回 0。

    在 Startup.cs 的 ConfigureServices 方法中,添加授权策略和 AuthorizationHandler:

            services.AddAuthorization(options => options.AddPolicy("HasUserId",
                policy => policy.Requirements.Add(new HasUserIdAuthorizationRequirement())));
    
            // AddScoped used for the HasUserIdAuthorizationHandler, because it uses the
            // data context with a scoped lifetime.
            services.AddScoped<IAuthorizationHandler, HasUserIdAuthorizationHandler>();
    
            // My custom service to access user data from the database:
            services.AddScoped<IUserService, UserService>();
    

    最后,在 _LoginPartial.cshtml 中更改登录操作:

    <a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>
    

    收件人:

    <a class="nav-link text-dark" asp-controller="Account" asp-action="LogOn">Sign in</a>
    

    现在,当用户未登录并单击登录时,或任何指向带有 [Authorize(Policy="HasUserId")] 装饰的操作或控制器的链接,他将首先被重新路由到 AD B2C 登录页面。然后,登录后,当用户已经注册时,他将被重新路由到所选链接。如果未注册,他将被重新路由到 Account/Register 操作。

    备注:如果使用策略不适合您的解决方案,请查看https://stackoverflow.com/a/41348219

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-03
      • 1970-01-01
      • 1970-01-01
      • 2021-03-16
      • 2021-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多