为什么最终用户必须注销两次?

Why does the end user have to log out twice?

我正在尝试让 IdentityServer4 在新的 .NET Core 2.1 应用程序中工作(它在 .NET Core 2.0 应用程序中完美运行)。我尝试了以下方法:

1) 下载这个项目,它是 IdentityServer4 应用程序:https://github.com/ghstahl/IdentityServer4-Asp.Net-2.1-Identity-Examples/tree/e0aeeff7e078aa082c8e16029dd2c220acc77d7b

2) 下载这个项目,它是使用 Identity Server4 应用程序的 MVC 应用程序:https://github.com/IdentityServer/IdentityServer4.Samples/tree/dev/Quickstarts/6_AspNetIdentity/src/MvcClient

3) 将两个项目添加到同一个解决方案中。 MVC项目使用IdentityServer项目进行身份验证;授权等

我必须进行以下更改:

1) 更改为包含在 IdentityServer 应用程序中的 Startup(AddIdentityServer 现在接受参数):

services.AddIdentityServer(options =>
{
    options.UserInteraction.LoginUrl = "/Identity/Account/Login";
    options.UserInteraction.LogoutUrl = "/Identity/Account/Logout";
})

2) 配置 IdentityServer 应用以侦听端口 5000 并在身份服务器上禁用 SSL。

开箱即用,除注销功能外一切正常。当我在 MVC 应用程序中单击注销时;在 MVC 应用程序中调用以下代码:

public async Task Logout() 
{ 
    await HttpContext.SignOutAsync("Cookies"); 
    await HttpContext.SignOutAsync("oidc"); 
} 

然后用户被重定向到 IdentityServer 应用程序中的 Logout.cshtml。但是,他们必须再次单击注销(在 IdentityServer 应用程序上)才能真正注销,即他们在 MVC 应用程序中单击注销(第二点),然后在 IdentityServer 中注销(第一点)。

为什么最终用户必须注销两次?

在脚手架 ASP.NET Core Identity 代码中 Areas/Identity/Account/Logout.cshtml.cs 下的 Account/Logout 页面中,有一个 OnGet 处理程序,如下所示:

public void OnGet() { }

因为这是使用 ASP.NET Core Razor 页面,所有这一切都是呈现相应的 Logout.cshtml 页面。在您的示例中,当您在 MVC 应用程序中点击 Logout 时,它会清除自己的 cookie,然后将您传递给 IS4 应用程序(特别是 OnGet)。因为这个 OnGet 处理程序是空的,所以它实际上没有做任何事情,而且肯定不会让您退出 IS4 应用程序。

如果您查看 Logout.cshtml.cs 内部的 OnPost 处理程序,您会发现它看起来像这样:

public async Task<IActionResult> OnPost(string returnUrl = null)
{
    await _signInManager.SignOutAsync();
    // ...
}

这个对 SignOutAsync 的调用完全符合它的建议:它让你退出 IS4 本身。但是,在您当前的工作流程中,未调用此 OnPost 处理程序。正如我已经提到的,当您在 MVC 应用程序中使用 Logout 时,会间接调用 OnGet 处理程序。

现在,如果您查看 Quickstart.UI 项目中 IS4 注销的 controller/action 实现,您会发现它基本上将 GET 请求传递给 POST 请求。这是代码,删除了注释:

[HttpGet]
public async Task<IActionResult> Logout(string logoutId)
{
    var vm = await BuildLogoutViewModelAsync(logoutId);

    if (vm.ShowLogoutPrompt == false)
        return await Logout(vm);

    return View(vm);
}

注销时,有一个设置可以控制是否应首先提示用户确认他们是否要注销。这主要是这段代码要处理的事情——如果不需要提示,它会将它直接传递给 POST 请求处理程序。这是 POST:

的代码片段
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
    var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);

    if (User?.Identity.IsAuthenticated == true)
    {
        await HttpContext.SignOutAsync();

        // ...
    }

    // ...

    return View("LoggedOut", vm);
}

此处重要的一行是对 HttpContext.SignOutAsync 的调用 - 这最终删除了 IS4 用来让您保持登录状态的 cookie。删除后,您将退出 IS4。最终,这就是您当前的实施所缺少的。

在最简单的层面上,您可以通过将 OnGet 更新为如下所示来解决您的问题:

public async Task<IActionResult> OnGet()
{
    if (User?.Identity.IsAuthenticated == true)
    {
        await _signInManager.SignOutAsync();          
        return RedirectToPage(); // A redirect ensures that the cookies has gone.
    }

    return Page();
}

这不支持我在上面详述的 ShowLogoutPrompt 选项,只是为了让这个答案更短一些。除此之外,鉴于您处于 ASP.NET Core Identity 世界,它只是使用 _signInManager 进行注销。

我鼓励您探索 Quickstart.UI 实现中的完整源代码,以支持 ShowLogoutPromptreturnUrl 等 - 如果没有,我不可能在这里做到这一点写一本书。

可以实现简单的注销功能,如下所示:

        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LogoutModel> _logger;
        private readonly IIdentityServerInteractionService _interaction;
        public LogoutModel(SignInManager<IdentityUser> signInManager, ILogger<LogoutModel> logger,
            IIdentityServerInteractionService interaction)
        {
            _signInManager = signInManager;
            _logger = logger;
            _interaction = interaction;
        }

        public async Task<IActionResult> OnGet(string logoutId)
        {
            return await OnPost(logoutId);
        }

        public async Task<IActionResult> OnPost(string logoutId)
        {
            await _signInManager.SignOutAsync();
            _logger.LogInformation("User logged out.");
            var r = await _interaction.GetLogoutContextAsync(logoutId);
            if (r.PostLogoutRedirectUri == null)
            {
                return Redirect("/");
            }
            return Redirect(r.PostLogoutRedirectUri);
        }

我遇到了同样的问题,我解决了它,并决定把我的答案放在这里让其他人看到。

解决方案:IdentityServer4内部快速启动项目逻辑已经存在,可以根据用户需要进行配置。

  1. 打开SolutionName/Quickstart/Account/AccountOptions.cs
  2. ShowLogoutPrompt 设置为 false
  3. AutomaticRedirectAfterSignOut 设置为 true

希望对你有所帮助,祝你好运。