园子 open api (api.cnblogs.com) 的授权服务器(authorization server,oauth.cnblogs.com)基于 IdentyServer 实现,而登录则通过单独的登录中心(account.cnblogs.com)完成,这样的场景对 IdentyServer 来说似乎是非典型应用场景,在网上几乎找不到相关的参考资料。
IdentyServer 的典型应用场景是授权(authorization)与登录验证(authentication)在同样一个应用中,登录验证通过后,调用 IdentityServer 提供的 HttpContext.SignInAsync
方法保存一些 IdentityServer 所需的 Claims
,就大功告成了。
var isUser = new IdentityServerUser(userId.ToString());
await HttpContext.SignInAsync(isUser.CreatePrincipal());
而我们的非典型应用场景需要在请求 //oauth.cnblogs.com/connect/authorize
时重定向到统一登录中心( account.cnblogs.com
)进行登录验证,登录成功后再通过 //oauth.cnblogs.com/connect/authorize/callback
跳转回来,问题来了,在哪里保存 IdentityServer 所需的 Claims
?
开始我们采用的一个变通方法是在授权服务器的 SignIn Action 方法中添加下面的实现代码
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public async Task SignIn(string returnUrl)
{
var user = new IdentityServerUser(User.GetUserIdString());
await HttpContext.SignInAsync(user);
return Redirect(returnUrl);
}
上面的代码实现的效果:
- 用户请求
//oauth.cnblogs.com/connect/authorize
时未登录 - 跳转到
//oauth.cnblogs.com/users/signin
进行登录,returnUrl 是/connect/authorize/callback
[Authorize]
触发跳转到登录中心account.cnblogs.com
进行登录,returnUrl 是/users/signin
- 登录成功后跳转回
/users/signin
- 在 SignIn Action 中设置 IdentityServer 所需的
Claims
- 跳转回
/connect/authorize/callback
后来发现这个变通方法存在一个问题,由于 IdentyServer 的登录与网站默认登录用的是同一个 Authentication Scheme,如果用户在通过授权服务器授权之前已经在网站上处于登录状态,点击授权时会跳过登录过程,直接完成授权。虽然可以通过启用 IdentyServer 在登录成功后的授权同意页面避免这个问题带来的影响,但是经过考虑,决定采用更保险的方法 —— 将来自 IdentyServer 的登录与网站默认登录进行隔离。
于是,解决 IdentityServer 授权与登录分离的问题就变成了 —— 如何通过 Authentication Scheme 实现登录验证隔离?下面分享一下在写这篇博文时采用的解决方法。
首先,确定一个专门用于 IdentityServer 登录验证的 Scheme —— CnblogsIdsrv
以及统一的 cookie name —— .Cnblogs.AspNetCore.Idsrv
public static class IdentityServerAuthentication
{
public const string DefaultScheme = "CnblogsIdsrv";
}
接着,在 Startup 中注册这个 Scheme
authenticationBuilder.AddCookie(
IdentityServerAuthentication.DefaultScheme,
options =>
{
options.Cookie.Name = ".Cnblogs.AspNetCore.Idsrv";
options.Cookie.Domain = ".cnblogs.com";
options.ExpireTimeSpan = TimeSpan.FromMinutes(2);
});
然后,登录中心在登录成功后根据 url 查询参数 sheme
判断是是否是来自 IdentyServer 授权服务器的登录,如果是,则将 IdentyServer 所需的 Claims 写入对应于专用 Authentication Scheme 的登录 Cookie 中(参考自 IdentyServer 的源码)
if (IdentityServerAuthentication.IsDefaultScheme(scheme))
{
var isu = new IdentityServerUser(userId.ToString());
isu.IdentityProvider = IdentityServerConstants.LocalIdentityProvider;
isu.AuthenticationMethods.Add(OidcConstants.AuthenticationMethods.Password);
isu.AuthenticationTime = DateTime.UtcNow;
await HttpContext.SignInAsync(
IdentityServerAuthentication.DefaultScheme,
isu.CreatePrincipal());
return;
}
注:上面的代码需要安装 nuget 包 IdentityServer4
Startup 中注册 Scheme 为 CnblogsIdsrv
的 CookieAuthentication
services.AddAuthentication("CnblogsIdsrv")
.AddCookie("CnblogsIdsrv", options =>
{
options.Cookie.Name = ".Cnblogs.AspNetCore.Idsrv";
options.Cookie.Domain = ".cnblogs.com";
options.ExpireTimeSpan = TimeSpan.FromMinutes(2);
options.LoginPath = "/users/signin";
options.LogoutPath = "/users/signout";
});
Startup 中将 IdentityServer 所使用的 CookieAuthenticationScheme 设置为 CnblogsIdsrv
services.AddIdentityServer(options =>
{
options.Authentication.CookieAuthenticationScheme = "CnblogsIdsrv";
});
修改 SignIn Action 登录跳转代码
public IActionResult SignIn(string returnUrl)
{
if (!returnUrl.StartsWith("http"))
{
returnUrl = "https://" + Request.Host.Host + returnUrl;
}
return Redirect($"{_signInUrl}?scheme={Config.AuthenticationScheme}&returnUrl={WebUtility.UrlEncode(returnUrl)}");
}
搞定,分享到此,解决这个问题的过程中走的最大的弯路是开始没有及时看 IdentityServer HttpContext.SignInAsync 部分的实现源码。
Original: https://www.cnblogs.com/dudu/p/14403310.html
Author: dudu
Title: 解决 IdentityServer 授权与登录分离的问题
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/547793/
转载文章受原作者版权保护。转载请注明原作者出处!