前文介绍了identity的用法,同时介绍了什么是identitySourece、apiSource、client 这几个概念,和具体案例,那么下面继续介绍案例了。
这里用官网的案例,因为学习一门技术最好的就是看官网了,所以不会去夹杂个人的自我编辑的案例,当然后面实战中怎么处理,遇到的问题是会展示开来的。
官网给的第二个例子是这个: https://identityserver4.readthedocs.io/en/latest/quickstarts/2_interactive_aspnetcore.html
首先来看下与identityServer 对接的客户端是怎么样的。
看着项目是一个标准mvc。
JwtSecurityTokenHandler.DefaultMapInboundClaims = false; services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "mvc"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.SaveTokens = true; });
上面的意思是使用方案认证方案是cookies,然后查问方案使用oidc。
AddCookie("Cookies") 就是注入cookies 方案,这个要和前面设置的options.DefaultScheme = "Cookies" 对应的,前面是配置,这个是具体实现。
我写过认证这块源码的,可以去看下,这里就不多介绍了。
然后下面AddOpenIdConnect 注册了查问访问oidc。
public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions) { builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>()); return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler>(authenticationScheme, displayName, configureOptions); }
这里再介绍一下DefaultScheme 和 DefaultChallengeScheme 分别是什么哈。
/// <summary> /// Used as the fallback default scheme for all the other defaults. /// </summary> public string DefaultScheme { get; set; }
默认就是使用这种方案。
/// <summary> /// Used as the default scheme by <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>. /// </summary> public string DefaultChallengeScheme { get; set; }
这个就是IAuthenticationService.ChallengeAsync 会使用到这个。
/// <summary> /// Challenge the specified authentication scheme. /// </summary> /// <param name="context">The <see cref="HttpContext"/>.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param> /// <returns>A task.</returns> Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);
这个方案确认了是否能通过,有兴趣的可以看下源码。
我们知道使用了AddAuthentication 是添加这个服务,我们需要在中间件中注册进去。
app.UseRouting(); app.UseAuthentication(); app.UseAuthorization();
那么这里mvc 客户端就算完成了。
那么identityServer 怎么该做些什么呢?
new Client { ClientId = "mvc", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, // where to redirect to after login RedirectUris = { "https://localhost:5002/signin-oidc" }, // where to redirect to after logout PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } }
这里解释一下。
RedirectUris 是登录完成之后会跳转的地址。
PostLogoutRedirectUris 是登录失败后会跳转的位置。
有人就会问了,为什么登录完成之后的地址为什么不是跳转过来的地址呢。
这里的流程是这样的,如果没有登录,那么就会跳转到identity Server的登录页面,然后再跳转回客户端的接收token 或者code 的路径,然后这个路径再跳转到一开始未登录的页面,有些直接到首页的。
然后可以看到这两个路径signin-oidc 和 signout-callback-oidc 发现我们mvc 中根本就没有写这两个路由,这个是由AddOpenIdConnect 提供的。
我们看下OpenIdConnectOptions 配置。
拦截到这两个路由,会进入OpenIdConnectHandler 做相应的处理。
这样子client 就注册了。
public static List<TestUser> Users { get { var address = new { street_address = "One Hacker Way", locality = "Heidelberg", postal_code = 69118, country = "Germany" }; return new List<TestUser> { new TestUser { SubjectId = "818727", Username = "alice", Password = "alice", Claims = { new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) } }, new TestUser { SubjectId = "88421113", Username = "bob", Password = "bob", Claims = { new Claim(JwtClaimTypes.Name, "Bob Smith"), new Claim(JwtClaimTypes.GivenName, "Bob"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://bob.com"), new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json) } } }; } }
那么需要将用户注册进去。
/// <summary> /// Entry point into the login workflow /// </summary> [HttpGet] public async Task<IActionResult> Login(string returnUrl) { // build a model so we know what to show on the login page var vm = await BuildLoginViewModelAsync(returnUrl); if (vm.IsExternalLoginOnly) { // we only have one option for logging in and it's an external provider return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl }); } return View(vm); }
这样不好看,直接debug调试下。
当我访问5002客户端的时候,那么:
这里跳转到5001 identity server 服务中去。
同样设置了返回的地址,红框中标明了。
然后又转到了account login
然后我们看到account login 接收到了什么。
这里可以看到如果login action 结束会进入到/connect/authorize/callback。
/connect/authorize -> account/login -> /connect/authorize/callback, 中间account/login就是用来验证是否通过的。
那么看一下登录的处理逻辑。
这是参数。
// check if we are in the context of an authorization request var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); // the user clicked the "cancel" button if (button != "login") { if (context != null) { // if the user cancels, send a result back into IdentityServer as if they // denied the consent (even if this client does not require consent). // this will send back an access denied OIDC error response to the client. await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied); // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null if (context.IsNativeClient()) { // The client is native, so this change in how to // return the response is for better UX for the end user. return this.LoadingPage("Redirect", model.ReturnUrl); } return Redirect(model.ReturnUrl); } else { // since we don't have a valid context, then we just go back to the home page return Redirect("~/"); } }
然后就会回到原先的进来的页面了。
然后看下正常登录逻辑。
if (ModelState.IsValid) { // validate username/password against in-memory store if (_users.ValidateCredentials(model.Username, model.Password)) { var user = _users.FindByUsername(model.Username); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId)); // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; // issue authentication cookie with subject ID and username var isuser = new IdentityServerUser(user.SubjectId) { DisplayName = user.Username }; await HttpContext.SignInAsync(isuser, props); if (context != null) { if (context.IsNativeClient()) { // The client is native, so this change in how to // return the response is for better UX for the end user. return this.LoadingPage("Redirect", model.ReturnUrl); } // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null return Redirect(model.ReturnUrl); } // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } else if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } else { // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } } }
大体逻辑就是验证账户密码是否正确,如果正确设置cookie。
await HttpContext.SignInAsync(isuser, props); 这个就是设置cookie了,很多人还不了解里面做了啥,看下源码。
经过这个方法后的结果为:
然后看一下_inner.SignInasync 做了什么。
这里放下源码,然后这个innser 就是 AuthenticationService。
public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties) { if (principal == null) { throw new ArgumentNullException(nameof(principal)); } if (Options.RequireAuthenticatedSignIn) { if (principal.Identity == null) { throw new InvalidOperationException("SignInAsync when principal.Identity == null is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true."); } if (!principal.Identity.IsAuthenticated) { throw new InvalidOperationException("SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true."); } } if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions)."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignInHandlerException(scheme); } var signInHandler = handler as IAuthenticationSignInHandler; if (signInHandler == null) { throw await CreateMismatchedSignInHandlerException(scheme, handler); } await signInHandler.SignInAsync(principal, properties); }
最后处理结果如上。后面就不继续看了,有兴趣可以看下CookieAuthenticationHandler的HandleSignInAsync。
然后处理完成后就可以进行交替给/connect/authorize/callback处理。
然后就可以看到结果了。
这里值得注意的是一定要使用https,不然会报错的。
这样登录就完成了,那么登出怎么处理呢?
public IActionResult Logout() { return SignOut("Cookies", "oidc"); }
这样就可以了,那么登出做了什么事情呢?
这个肯定是清除了cookie,并通知了identity server 进行清除cookie。
public virtual SignOutResult SignOut(params string[] authenticationSchemes) => new SignOutResult(authenticationSchemes); public SignOutResult(IList<string> authenticationSchemes) : this(authenticationSchemes, properties: null) { }
SignOutResult : ActionResult 是一个actionResult,那么actionResult 会做什么呢?
An <see cref="ActionResult"/> that on execution invokes <see cref="M:HttpContext.SignOutAsync"/>.
那么SignOutResult 其会执行下面这一段。
public override async Task ExecuteResultAsync(ActionContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (AuthenticationSchemes == null) { throw new InvalidOperationException( Resources.FormatPropertyOfTypeCannotBeNull( /* property: */ nameof(AuthenticationSchemes), /* type: */ nameof(SignOutResult))); } var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>(); var logger = loggerFactory.CreateLogger<SignOutResult>(); logger.SignOutResultExecuting(AuthenticationSchemes); if (AuthenticationSchemes.Count == 0) { await context.HttpContext.SignOutAsync(Properties); } else { for (var i = 0; i < AuthenticationSchemes.Count; i++) { await context.HttpContext.SignOutAsync(AuthenticationSchemes[i], Properties); } } }
重点看context.HttpContext.SignOutAsync 做了什么。AuthenticationSchemes 我们传递了SignOut("Cookies", "oidc")。
public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) => context.RequestServices.GetRequiredService<IAuthenticationService>().SignOutAsync(context, scheme, properties);
那么就会掉我们注入的IAuthenticationService的SignOutAsync方法。
那么IAuthenticationService 注入的是什么呢?
那么会执行:
public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties) { if (scheme == null) { var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync(); scheme = defaultScheme?.Name; if (scheme == null) { throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action<AuthenticationOptions> configureOptions)."); } } var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { throw await CreateMissingSignOutHandlerException(scheme); } var signOutHandler = handler as IAuthenticationSignOutHandler; if (signOutHandler == null) { throw await CreateMismatchedSignOutHandlerException(scheme, handler); } await signOutHandler.SignOutAsync(properties); }
那么其实就是分为两步,一步是清除自身的cookie,自身退出登录,然后通知identityserver 退出登录(清除cookie)
cookie 自身的就不看了,看identity相关处理逻辑。
public async virtual Task SignOutAsync(AuthenticationProperties properties) { var target = ResolveTarget(Options.ForwardSignOut); if (target != null) { await Context.SignOutAsync(target, properties); return; } properties = properties ?? new AuthenticationProperties(); Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName); if (_configuration == null && Options.ConfigurationManager != null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted); } var message = new OpenIdConnectMessage() { EnableTelemetryParameters = !Options.DisableTelemetry, IssuerAddress = _configuration?.EndSessionEndpoint ?? string.Empty, // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath) }; // Get the post redirect URI. if (string.IsNullOrEmpty(properties.RedirectUri)) { properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri); if (string.IsNullOrWhiteSpace(properties.RedirectUri)) { properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString; } } Logger.PostSignOutRedirect(properties.RedirectUri); // Attach the identity token to the logout request when possible. message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken); var redirectContext = new RedirectContext(Context, Scheme, Options, properties) { ProtocolMessage = message }; await Events.RedirectToIdentityProviderForSignOut(redirectContext); if (redirectContext.Handled) { Logger.RedirectToIdentityProviderForSignOutHandledResponse(); return; } message = redirectContext.ProtocolMessage; if (!string.IsNullOrEmpty(message.State)) { properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State; } message.State = Options.StateDataFormat.Protect(properties); if (string.IsNullOrEmpty(message.IssuerAddress)) { throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid."); } if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet) { var redirectUri = message.CreateLogoutRequestUrl(); if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute)) { Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri); } Response.Redirect(redirectUri); } else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost) { var content = message.BuildFormPost(); var buffer = Encoding.UTF8.GetBytes(content); Response.ContentLength = buffer.Length; Response.ContentType = "text/html;charset=UTF-8"; // Emit Cache-Control=no-cache to prevent client caching. Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store"; Response.Headers[HeaderNames.Pragma] = "no-cache"; Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate; await Response.Body.WriteAsync(buffer, 0, buffer.Length); } else { throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}"); } Logger.AuthenticationSchemeSignedOut(Scheme.Name); }
会发送请求,然后调用identity 登出通知。
那么抓包看一下,一共4步。
这个源码倒是挺简单的,就不把源码贴出来了。
然后这里很多人就有问题了。
这里我们明明传了回调地址了,为什么我们还有填一次呢?
其实一般情况下真的可以不填,但是需求可以填一下,比如有多个回调地址的时候。
然后我们可以选择登出的方式有get 和post,post的情况下是这样的。
客户端可以选择方式。
这个案例就先到这,后面介绍单页面客户端。
转 https://www.cnblogs.com/aoximin/p/16590293.html