如何在 Azure AD 身份验证后重定向到 ASP Net Core MVC 中的不同控制器操作

How to redirect after Azure AD authentication to different controller action in ASP Net Core MVC

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

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

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

这里有一些代码片段,可能有助于阐明...

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);
    }

}

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

作为解决方法,您可以修改默认网站路由以将用户重定向到特定 controller/action:

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

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

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

内部启动

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);
}

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

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

基本上,我将授权属性与策略 [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 在这种情况下只是一个空标记 class:

    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 和 returns 0 当找不到或 azureObjectId 为空时。

在Startup.cs中,在ConfigureServices方法中,添加Authorization policy和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>

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

备注:如果使用策略不适合您的解决方案,请查看