OWIN:如何检测用户是否已通过外部身份提供商完成认证
我来帮你搞定这个问题!结合你用OWIN + Keycloak + 老ASP.NET WebForms的场景,下面是几种实用的检测方式,都是我实际项目里用过的靠谱方案:
方法一:通过认证类型(AuthenticationType)判断
当用户通过外部IdP(比如你配置的那两个Keycloak关联的提供商)认证后,OWIN会为该用户创建一个ClaimsIdentity,其中的AuthenticationType就是你在Startup.cs里配置外部认证时指定的名称。我们可以通过这个来区分是否是外部认证:你可以在WebForms页面的后台代码里这么写:
protected void Page_Load(object sender, EventArgs e) { // 获取OWIN上下文和认证管理器 var owinContext = HttpContext.Current.GetOwinContext(); var authManager = owinContext.Authentication; bool isExternalAuthenticated = false; // 先判断用户是否已认证 if (authManager.User.Identity.IsAuthenticated) { // 检查是否存在来自外部IdP的认证标识 // 这里替换成你实际配置的两个IdP的AuthenticationType,比如"KeycloakIdP1"、"KeycloakIdP2" isExternalAuthenticated = authManager.User.Identities.Any(id => id.AuthenticationType == "KeycloakIdP1" || id.AuthenticationType == "KeycloakIdP2" ); } // 控制按钮显示:外部已认证就显示登出,否则显示两个登录按钮 btnIdP1.Visible = !isExternalAuthenticated; btnIdP2.Visible = !isExternalAuthenticated; btnSignOut.Visible = isExternalAuthenticated; }划重点:Startup.cs里配置外部认证时,一定要指定好
AuthenticationType,比如:app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { AuthenticationType = "KeycloakIdP1", // 这个名称要和上面判断的完全一致 Authority = "https://your-keycloak-domain/auth/realms/your-realm-1", ClientId = "your-client-id-1", // 其他必要配置(回调地址、响应类型等)... }); app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { AuthenticationType = "KeycloakIdP2", Authority = "https://your-keycloak-domain/auth/realms/your-realm-2", ClientId = "your-client-id-2", // 其他必要配置... });方法二:通过Claims的Issuer(签发者)判断
每个外部IdP签发的Claims都会带有自己的Issuer标识(比如Keycloak的Issuer就是你的Realm地址),我们也可以通过这个来判断,不用依赖AuthenticationType:代码示例如下:
protected void Page_Load(object sender, EventArgs e) { var owinContext = HttpContext.Current.GetOwinContext(); var authManager = owinContext.Authentication; bool isExternalAuthenticated = false; if (authManager.User.Identity.IsAuthenticated) { // 替换成你的两个IdP的Issuer值,比如Keycloak Realm的完整URL var allowedIssuers = new List<string> { "https://your-keycloak-domain/auth/realms/your-realm-1", "https://your-keycloak-domain/auth/realms/your-realm-2" }; isExternalAuthenticated = authManager.User.Claims.Any(c => allowedIssuers.Contains(c.Issuer)); } // 同样控制按钮显示逻辑 btnIdP1.Visible = !isExternalAuthenticated; btnIdP2.Visible = !isExternalAuthenticated; btnSignOut.Visible = isExternalAuthenticated; }这个方法更稳妥,因为Issuer是IdP本身的标识,不会因为你本地配置的AuthenticationType变化而失效,适合Keycloak这种多Realm的场景。
方法三:自定义Claim标记(最可控的方式)
如果你担心上面两种方式会因为IdP配置变动而出问题,还可以在用户通过外部认证并完成授权后,手动加一个自定义Claim,之后只需要检查这个Claim是否存在就行:首先在外部登录的回调处理方法里(就是你调用
AuthenticationManager.SignIn()的地方)添加自定义Claim:// 假设这是你处理外部登录回调的代码 var externalLoginInfo = await authManager.GetExternalLoginInfoAsync(); if (externalLoginInfo != null) { // 这里是你的用户授权验证逻辑,比如检查用户是否在允许的列表里 bool isAuthorized = YourAuthorizationCheckMethod(externalLoginInfo.Login.LoginProvider, externalLoginInfo.Login.ProviderKey); if (isAuthorized) { // 创建身份标识,添加自定义Claim标记是外部认证 var identity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie); identity.AddClaim(new Claim("IsExternalAuth", "True")); // 还可以添加用户的其他Claims,比如用户名、角色等 identity.AddClaim(new Claim(ClaimTypes.Name, externalLoginInfo.DefaultUserName)); // 登录用户 authManager.SignIn(new AuthenticationProperties { IsPersistent = false }, identity); } }然后在登录页面的后台代码里检查这个Claim:
protected void Page_Load(object sender, EventArgs e) { var owinContext = HttpContext.Current.GetOwinContext(); var authManager = owinContext.Authentication; bool isExternalAuthenticated = authManager.User.Identity.IsAuthenticated && authManager.User.Claims.Any(c => c.Type == "IsExternalAuth" && c.Value == "True"); // 控制按钮显示 btnIdP1.Visible = !isExternalAuthenticated; btnIdP2.Visible = !isExternalAuthenticated; btnSignOut.Visible = isExternalAuthenticated; }
最后补充个登出的小细节
记得在登出按钮的点击事件里,要把相关的认证类型都签出,不然可能会有残留的认证信息:
protected void btnSignOut_Click(object sender, EventArgs e) { var authManager = HttpContext.Current.GetOwinContext().Authentication; // 登出所有相关的认证类型:本地Cookie和两个外部IdP的 authManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie, "KeycloakIdP1", "KeycloakIdP2"); Response.Redirect("~/Login.aspx"); }
内容来源于stack exchange

