前言

前文介绍了identity的用法,同时介绍了什么是identitySourece、apiSource、client 这几个概念,和具体案例,那么下面继续介绍案例了。

正文

这里用官网的案例,因为学习一门技术最好的就是看官网了,所以不会去夹杂个人的自我编辑的案例,当然后面实战中怎么处理,遇到的问题是会展示开来的。

官网给的第二个例子是这个: https://identityserver4.readthedocs.io/en/latest/quickstarts/2_interactive_aspnetcore.html

首先来看下与identityServer 对接的客户端是怎么样的。

看着项目是一个标准mvc。

  1. JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
  2. services.AddAuthentication(options =>
  3. {
  4. options.DefaultScheme = "Cookies";
  5. options.DefaultChallengeScheme = "oidc";
  6. })
  7. .AddCookie("Cookies")
  8. .AddOpenIdConnect("oidc", options =>
  9. {
  10. options.Authority = "https://localhost:5001";
  11. options.ClientId = "mvc";
  12. options.ClientSecret = "secret";
  13. options.ResponseType = "code";
  14. options.SaveTokens = true;
  15. });

上面的意思是使用方案认证方案是cookies,然后查问方案使用oidc。

AddCookie("Cookies") 就是注入cookies 方案,这个要和前面设置的options.DefaultScheme = "Cookies" 对应的,前面是配置,这个是具体实现。

我写过认证这块源码的,可以去看下,这里就不多介绍了。

然后下面AddOpenIdConnect 注册了查问访问oidc。

  1. public static AuthenticationBuilder AddOpenIdConnect(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<OpenIdConnectOptions> configureOptions)
  2. {
  3. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIdConnectOptions>, OpenIdConnectPostConfigureOptions>());
  4. return builder.AddRemoteScheme<OpenIdConnectOptions, OpenIdConnectHandler>(authenticationScheme, displayName, configureOptions);
  5. }

这里再介绍一下DefaultScheme 和 DefaultChallengeScheme 分别是什么哈。

  1. /// <summary>
  2. /// Used as the fallback default scheme for all the other defaults.
  3. /// </summary>
  4. public string DefaultScheme { get; set; }

默认就是使用这种方案。

  1. /// <summary>
  2. /// Used as the default scheme by <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>.
  3. /// </summary>
  4. public string DefaultChallengeScheme { get; set; }

这个就是IAuthenticationService.ChallengeAsync 会使用到这个。

  1. /// <summary>
  2. /// Challenge the specified authentication scheme.
  3. /// </summary>
  4. /// <param name="context">The <see cref="HttpContext"/>.</param>
  5. /// <param name="scheme">The name of the authentication scheme.</param>
  6. /// <param name="properties">The <see cref="AuthenticationProperties"/>.</param>
  7. /// <returns>A task.</returns>
  8. Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties);

这个方案确认了是否能通过,有兴趣的可以看下源码。

我们知道使用了AddAuthentication 是添加这个服务,我们需要在中间件中注册进去。

  1. app.UseRouting();
  2. app.UseAuthentication();
  3. app.UseAuthorization();

那么这里mvc 客户端就算完成了。

那么identityServer 怎么该做些什么呢?

  1. 肯定是要注册客户端的嘛
  1. new Client
  2. {
  3. ClientId = "mvc",
  4. ClientSecrets = { new Secret("secret".Sha256()) },
  5. AllowedGrantTypes = GrantTypes.Code,
  6. // where to redirect to after login
  7. RedirectUris = { "https://localhost:5002/signin-oidc" },
  8. // where to redirect to after logout
  9. PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
  10. AllowedScopes = new List<string>
  11. {
  12. IdentityServerConstants.StandardScopes.OpenId,
  13. IdentityServerConstants.StandardScopes.Profile
  14. }
  15. }

这里解释一下。

RedirectUris 是登录完成之后会跳转的地址。

PostLogoutRedirectUris 是登录失败后会跳转的位置。

有人就会问了,为什么登录完成之后的地址为什么不是跳转过来的地址呢。

这里的流程是这样的,如果没有登录,那么就会跳转到identity Server的登录页面,然后再跳转回客户端的接收token 或者code 的路径,然后这个路径再跳转到一开始未登录的页面,有些直接到首页的。

然后可以看到这两个路径signin-oidc 和 signout-callback-oidc 发现我们mvc 中根本就没有写这两个路由,这个是由AddOpenIdConnect 提供的。

我们看下OpenIdConnectOptions 配置。

拦截到这两个路由,会进入OpenIdConnectHandler 做相应的处理。

这样子client 就注册了。

  1. 登录,一般模式是需要账户密码,那么要账户密码就需要用户,这个用户怎么注册进去呢?
  1. public static List<TestUser> Users
  2. {
  3. get
  4. {
  5. var address = new
  6. {
  7. street_address = "One Hacker Way",
  8. locality = "Heidelberg",
  9. postal_code = 69118,
  10. country = "Germany"
  11. };
  12. return new List<TestUser>
  13. {
  14. new TestUser
  15. {
  16. SubjectId = "818727",
  17. Username = "alice",
  18. Password = "alice",
  19. Claims =
  20. {
  21. new Claim(JwtClaimTypes.Name, "Alice Smith"),
  22. new Claim(JwtClaimTypes.GivenName, "Alice"),
  23. new Claim(JwtClaimTypes.FamilyName, "Smith"),
  24. new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
  25. new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
  26. new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
  27. new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
  28. }
  29. },
  30. new TestUser
  31. {
  32. SubjectId = "88421113",
  33. Username = "bob",
  34. Password = "bob",
  35. Claims =
  36. {
  37. new Claim(JwtClaimTypes.Name, "Bob Smith"),
  38. new Claim(JwtClaimTypes.GivenName, "Bob"),
  39. new Claim(JwtClaimTypes.FamilyName, "Smith"),
  40. new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
  41. new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
  42. new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
  43. new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
  44. }
  45. }
  46. };
  47. }
  48. }

那么需要将用户注册进去。

  1. 这个时候还得处理identity Server的逻辑
  1. /// <summary>
  2. /// Entry point into the login workflow
  3. /// </summary>
  4. [HttpGet]
  5. public async Task<IActionResult> Login(string returnUrl)
  6. {
  7. // build a model so we know what to show on the login page
  8. var vm = await BuildLoginViewModelAsync(returnUrl);
  9. if (vm.IsExternalLoginOnly)
  10. {
  11. // we only have one option for logging in and it's an external provider
  12. return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
  13. }
  14. return View(vm);
  15. }

这样不好看,直接debug调试下。

当我访问5002客户端的时候,那么:

这里跳转到5001 identity server 服务中去。

同样设置了返回的地址,红框中标明了。

然后又转到了account login

然后我们看到account login 接收到了什么。

这里可以看到如果login action 结束会进入到/connect/authorize/callback。

/connect/authorize -> account/login -> /connect/authorize/callback, 中间account/login就是用来验证是否通过的。

那么看一下登录的处理逻辑。

这是参数。

  1. // check if we are in the context of an authorization request
  2. var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
  3. // the user clicked the "cancel" button
  4. if (button != "login")
  5. {
  6. if (context != null)
  7. {
  8. // if the user cancels, send a result back into IdentityServer as if they
  9. // denied the consent (even if this client does not require consent).
  10. // this will send back an access denied OIDC error response to the client.
  11. await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
  12. // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
  13. if (context.IsNativeClient())
  14. {
  15. // The client is native, so this change in how to
  16. // return the response is for better UX for the end user.
  17. return this.LoadingPage("Redirect", model.ReturnUrl);
  18. }
  19. return Redirect(model.ReturnUrl);
  20. }
  21. else
  22. {
  23. // since we don't have a valid context, then we just go back to the home page
  24. return Redirect("~/");
  25. }
  26. }

然后就会回到原先的进来的页面了。

然后看下正常登录逻辑。

  1. if (ModelState.IsValid)
  2. {
  3. // validate username/password against in-memory store
  4. if (_users.ValidateCredentials(model.Username, model.Password))
  5. {
  6. var user = _users.FindByUsername(model.Username);
  7. await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));
  8. // only set explicit expiration here if user chooses "remember me".
  9. // otherwise we rely upon expiration configured in cookie middleware.
  10. AuthenticationProperties props = null;
  11. if (AccountOptions.AllowRememberLogin && model.RememberLogin)
  12. {
  13. props = new AuthenticationProperties
  14. {
  15. IsPersistent = true,
  16. ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
  17. };
  18. };
  19. // issue authentication cookie with subject ID and username
  20. var isuser = new IdentityServerUser(user.SubjectId)
  21. {
  22. DisplayName = user.Username
  23. };
  24. await HttpContext.SignInAsync(isuser, props);
  25. if (context != null)
  26. {
  27. if (context.IsNativeClient())
  28. {
  29. // The client is native, so this change in how to
  30. // return the response is for better UX for the end user.
  31. return this.LoadingPage("Redirect", model.ReturnUrl);
  32. }
  33. // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
  34. return Redirect(model.ReturnUrl);
  35. }
  36. // request for a local page
  37. if (Url.IsLocalUrl(model.ReturnUrl))
  38. {
  39. return Redirect(model.ReturnUrl);
  40. }
  41. else if (string.IsNullOrEmpty(model.ReturnUrl))
  42. {
  43. return Redirect("~/");
  44. }
  45. else
  46. {
  47. // user might have clicked on a malicious link - should be logged
  48. throw new Exception("invalid return URL");
  49. }
  50. }
  51. }

大体逻辑就是验证账户密码是否正确,如果正确设置cookie。

await HttpContext.SignInAsync(isuser, props); 这个就是设置cookie了,很多人还不了解里面做了啥,看下源码。

经过这个方法后的结果为:

然后看一下_inner.SignInasync 做了什么。

这里放下源码,然后这个innser 就是 AuthenticationService。

  1. public virtual async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
  2. {
  3. if (principal == null)
  4. {
  5. throw new ArgumentNullException(nameof(principal));
  6. }
  7. if (Options.RequireAuthenticatedSignIn)
  8. {
  9. if (principal.Identity == null)
  10. {
  11. throw new InvalidOperationException("SignInAsync when principal.Identity == null is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true.");
  12. }
  13. if (!principal.Identity.IsAuthenticated)
  14. {
  15. throw new InvalidOperationException("SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true.");
  16. }
  17. }
  18. if (scheme == null)
  19. {
  20. var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync();
  21. scheme = defaultScheme?.Name;
  22. if (scheme == null)
  23. {
  24. 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).");
  25. }
  26. }
  27. var handler = await Handlers.GetHandlerAsync(context, scheme);
  28. if (handler == null)
  29. {
  30. throw await CreateMissingSignInHandlerException(scheme);
  31. }
  32. var signInHandler = handler as IAuthenticationSignInHandler;
  33. if (signInHandler == null)
  34. {
  35. throw await CreateMismatchedSignInHandlerException(scheme, handler);
  36. }
  37. await signInHandler.SignInAsync(principal, properties);
  38. }

最后处理结果如上。后面就不继续看了,有兴趣可以看下CookieAuthenticationHandler的HandleSignInAsync。

然后处理完成后就可以进行交替给/connect/authorize/callback处理。

然后就可以看到结果了。

这里值得注意的是一定要使用https,不然会报错的。

这样登录就完成了,那么登出怎么处理呢?

  1. public IActionResult Logout()
  2. {
  3. return SignOut("Cookies", "oidc");
  4. }

这样就可以了,那么登出做了什么事情呢?

这个肯定是清除了cookie,并通知了identity server 进行清除cookie。

  1. public virtual SignOutResult SignOut(params string[] authenticationSchemes)
  2. => new SignOutResult(authenticationSchemes);
  3. public SignOutResult(IList<string> authenticationSchemes)
  4. : this(authenticationSchemes, properties: null)
  5. {
  6. }

SignOutResult : ActionResult 是一个actionResult,那么actionResult 会做什么呢?

  1. An <see cref="ActionResult"/> that on execution invokes <see cref="M:HttpContext.SignOutAsync"/>.

那么SignOutResult 其会执行下面这一段。

  1. public override async Task ExecuteResultAsync(ActionContext context)
  2. {
  3. if (context == null)
  4. {
  5. throw new ArgumentNullException(nameof(context));
  6. }
  7. if (AuthenticationSchemes == null)
  8. {
  9. throw new InvalidOperationException(
  10. Resources.FormatPropertyOfTypeCannotBeNull(
  11. /* property: */ nameof(AuthenticationSchemes),
  12. /* type: */ nameof(SignOutResult)));
  13. }
  14. var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>();
  15. var logger = loggerFactory.CreateLogger<SignOutResult>();
  16. logger.SignOutResultExecuting(AuthenticationSchemes);
  17. if (AuthenticationSchemes.Count == 0)
  18. {
  19. await context.HttpContext.SignOutAsync(Properties);
  20. }
  21. else
  22. {
  23. for (var i = 0; i < AuthenticationSchemes.Count; i++)
  24. {
  25. await context.HttpContext.SignOutAsync(AuthenticationSchemes[i], Properties);
  26. }
  27. }
  28. }

重点看context.HttpContext.SignOutAsync 做了什么。AuthenticationSchemes 我们传递了SignOut("Cookies", "oidc")。

  1. public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
  2. context.RequestServices.GetRequiredService<IAuthenticationService>().SignOutAsync(context, scheme, properties);

那么就会掉我们注入的IAuthenticationService的SignOutAsync方法。

那么IAuthenticationService 注入的是什么呢?

那么会执行:

  1. public virtual async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
  2. {
  3. if (scheme == null)
  4. {
  5. var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync();
  6. scheme = defaultScheme?.Name;
  7. if (scheme == null)
  8. {
  9. 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).");
  10. }
  11. }
  12. var handler = await Handlers.GetHandlerAsync(context, scheme);
  13. if (handler == null)
  14. {
  15. throw await CreateMissingSignOutHandlerException(scheme);
  16. }
  17. var signOutHandler = handler as IAuthenticationSignOutHandler;
  18. if (signOutHandler == null)
  19. {
  20. throw await CreateMismatchedSignOutHandlerException(scheme, handler);
  21. }
  22. await signOutHandler.SignOutAsync(properties);
  23. }

那么其实就是分为两步,一步是清除自身的cookie,自身退出登录,然后通知identityserver 退出登录(清除cookie)

cookie 自身的就不看了,看identity相关处理逻辑。

  1. public async virtual Task SignOutAsync(AuthenticationProperties properties)
  2. {
  3. var target = ResolveTarget(Options.ForwardSignOut);
  4. if (target != null)
  5. {
  6. await Context.SignOutAsync(target, properties);
  7. return;
  8. }
  9. properties = properties ?? new AuthenticationProperties();
  10. Logger.EnteringOpenIdAuthenticationHandlerHandleSignOutAsync(GetType().FullName);
  11. if (_configuration == null && Options.ConfigurationManager != null)
  12. {
  13. _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
  14. }
  15. var message = new OpenIdConnectMessage()
  16. {
  17. EnableTelemetryParameters = !Options.DisableTelemetry,
  18. IssuerAddress = _configuration?.EndSessionEndpoint ?? string.Empty,
  19. // Redirect back to SigneOutCallbackPath first before user agent is redirected to actual post logout redirect uri
  20. PostLogoutRedirectUri = BuildRedirectUriIfRelative(Options.SignedOutCallbackPath)
  21. };
  22. // Get the post redirect URI.
  23. if (string.IsNullOrEmpty(properties.RedirectUri))
  24. {
  25. properties.RedirectUri = BuildRedirectUriIfRelative(Options.SignedOutRedirectUri);
  26. if (string.IsNullOrWhiteSpace(properties.RedirectUri))
  27. {
  28. properties.RedirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
  29. }
  30. }
  31. Logger.PostSignOutRedirect(properties.RedirectUri);
  32. // Attach the identity token to the logout request when possible.
  33. message.IdTokenHint = await Context.GetTokenAsync(Options.SignOutScheme, OpenIdConnectParameterNames.IdToken);
  34. var redirectContext = new RedirectContext(Context, Scheme, Options, properties)
  35. {
  36. ProtocolMessage = message
  37. };
  38. await Events.RedirectToIdentityProviderForSignOut(redirectContext);
  39. if (redirectContext.Handled)
  40. {
  41. Logger.RedirectToIdentityProviderForSignOutHandledResponse();
  42. return;
  43. }
  44. message = redirectContext.ProtocolMessage;
  45. if (!string.IsNullOrEmpty(message.State))
  46. {
  47. properties.Items[OpenIdConnectDefaults.UserstatePropertiesKey] = message.State;
  48. }
  49. message.State = Options.StateDataFormat.Protect(properties);
  50. if (string.IsNullOrEmpty(message.IssuerAddress))
  51. {
  52. throw new InvalidOperationException("Cannot redirect to the end session endpoint, the configuration may be missing or invalid.");
  53. }
  54. if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.RedirectGet)
  55. {
  56. var redirectUri = message.CreateLogoutRequestUrl();
  57. if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
  58. {
  59. Logger.InvalidLogoutQueryStringRedirectUrl(redirectUri);
  60. }
  61. Response.Redirect(redirectUri);
  62. }
  63. else if (Options.AuthenticationMethod == OpenIdConnectRedirectBehavior.FormPost)
  64. {
  65. var content = message.BuildFormPost();
  66. var buffer = Encoding.UTF8.GetBytes(content);
  67. Response.ContentLength = buffer.Length;
  68. Response.ContentType = "text/html;charset=UTF-8";
  69. // Emit Cache-Control=no-cache to prevent client caching.
  70. Response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
  71. Response.Headers[HeaderNames.Pragma] = "no-cache";
  72. Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
  73. await Response.Body.WriteAsync(buffer, 0, buffer.Length);
  74. }
  75. else
  76. {
  77. throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
  78. }
  79. Logger.AuthenticationSchemeSignedOut(Scheme.Name);
  80. }

会发送请求,然后调用identity 登出通知。

那么抓包看一下,一共4步。

  1. 调用自身的logout

  1. 调用identityserver 封装的logout。

  1. 调用identityserver 自己封装的logout

  1. 调用identityserver 封装的logout 回调

  1. 客户可以回调回去。

这个源码倒是挺简单的,就不把源码贴出来了。

然后这里很多人就有问题了。

这里我们明明传了回调地址了,为什么我们还有填一次呢?

其实一般情况下真的可以不填,但是需求可以填一下,比如有多个回调地址的时候。

然后我们可以选择登出的方式有get 和post,post的情况下是这样的。

客户端可以选择方式。

这个案例就先到这,后面介绍单页面客户端。

identity4 系列————案例篇[三]的更多相关文章

  1. identity4 系列————启航篇[二]

    前言 开始identity的介绍了. 正文 前文介绍了一些概念,如果概念不清的话,可以去前文查看. https://www.cnblogs.com/aoximin/p/13475444.html 对一 ...

  2. 《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)

    1.简介 在做web自动化时,有些情况selenium的api无法完成,需要通过第三方手段比如js来完成实现,比如去改变某些元素对象的属性或者进行一些特殊的操作,本文将来讲解怎样来调用JavaScri ...

  3. Maven提高篇系列之(三)——使用自己的Repository(Nexus)

    这是一个Maven提高篇的系列,包含有以下文章: Maven提高篇系列之(一)——多模块 vs 继承 Maven提高篇系列之(二)——配置Plugin到某个Phase(以Selenium集成测试为例) ...

  4. SQL Server调优系列基础篇(常用运算符总结——三种物理连接方式剖析)

    前言 上一篇我们介绍了如何查看查询计划,本篇将介绍在我们查看的查询计划时的分析技巧,以及几种我们常用的运算符优化技巧,同样侧重基础知识的掌握. 通过本篇可以了解我们平常所写的T-SQL语句,在SQL ...

  5. SQL Server调优系列玩转篇三(利用索引提示(Hint)引导语句最大优化运行)

    前言 本篇继续玩转模块的内容,关于索引在SQL Server的位置无须多言,本篇将分析如何利用Hint引导语句充分利用索引进行运行,同样,还是希望扎实掌握前面一系列的内容,才进入本模块的内容分析. 闲 ...

  6. 《手把手教你》系列技巧篇(三十)-java+ selenium自动化测试- Actions的相关操作下篇(详解教程)

    1.简介 本文主要介绍两个在测试过程中可能会用到的功能:Actions类中的拖拽操作和Actions类中的划取字段操作.例如:需要在一堆log字符中随机划取一段文字,然后右键选择摘取功能. 2.拖拽操 ...

  7. 《手把手教你》系列技巧篇(三十一)-java+ selenium自动化测试- Actions的相关操作-番外篇(详解教程)

    1.简介 上一篇中,宏哥说的宏哥在最后提到网站的反爬虫机制,那么宏哥在自己本地做一个网页,没有那个反爬虫的机制,谷歌浏览器是不是就可以验证成功了,宏哥就想验证一下自己想法,于是写了这一篇文章,另外也是 ...

  8. 《手把手教你》系列技巧篇(三十二)-java+ selenium自动化测试-select 下拉框(详解教程)

    1.简介 在实际自动化测试过程中,我们也避免不了会遇到下拉选择的测试,因此宏哥在这里直接分享和介绍一下,希望小伙伴或者童鞋们在以后工作中遇到可以有所帮助. 2.select 下拉框 2.1Select ...

  9. 《手把手教你》系列技巧篇(三十三)-java+ selenium自动化测试-单选和多选按钮操作-上篇(详解教程)

    1.简介 在实际自动化测试过程中,我们同样也避免不了会遇到单选和多选的测试,特别是调查问卷或者是答题系统中会经常碰到.因此宏哥在这里直接分享和介绍一下,希望小伙伴或者童鞋们在以后工作中遇到可以有所帮助 ...

随机推荐

  1. while循环结构

    一.循环: 1.场景: ①.用户名和密码,反复输入 ②.计算1-100之间 ③.游戏,重生 ④.-- 2.方式 ①.while ②.for 3.while格式 while 条件:要循环执行的代码 布尔 ...

  2. Django——模板应用

    一.前言 前提:已经用命令提前创建好了项目DjangoTest,以下是基于该项目进行实战演练. 二.项目下创建templates文件夹 1.创建templates文件夹 2.创建HelloWorld. ...

  3. Windows启动谷歌浏览器Chrome失败(应用程序无法启动,因为应用程序的并行配置不正确)解决方法

    目录 一.系统环境 二.问题描述 三.解决方法 一.系统环境 Windows版本 系统类型 浏览器Chrome版本 Windows 10 专业版 64 位操作系统, 基于 x64 的处理器 版本 10 ...

  4. 疫情在校学生之——用python对某校园热水服务app进行测试,实现自动免费用水(仅供参考)

    写在前面的过场话: 本文只是对某校园热水服务app做个测试,其实本人并没有做大坏事,并未传播相关技术,文章以下内容的敏感部分会打码,并且相关厂商已经正在进行漏洞修复,大家看看就好.文章后会提供&quo ...

  5. Python程序入口 __name__ == ‘__main__‘ 有重要功能(多线程)而非编程习惯

    文章来源于互联网(https://jq.qq.com/?_wv=1027&k=rX9CWKg4) 在Python中,被称为「程序的入口」的 if name =='main': 总是出现在各种示 ...

  6. 编译调试Net6源码

    前言 编辑调试DotNet源码可按照官网教程操作,但因为网络问题中间会出现各种下载失败的问题,这里出个简单的教程(以6为版本) 下载源码 下载源码 GitHub下载源码速度极慢,可替换为国内仓库htt ...

  7. NC204859 组队

    NC204859 组队 题目 题目描述 你的团队中有 \(n\) 个人,每个人有一个能力值 \(a_i\),现在需要选择若干个人组成一个团队去参加比赛,由于比赛的规则限制,一个团队里面任意两个人能力的 ...

  8. 今天介绍一下自己的开源项目,一款以spring cloud alibaba为核心的微服务架构项目,为给企业与个人提供一个零开发基础的微服务架构。

    LaoCat-Spring-Cloud-Scaffold 一款以spring cloud alibab 为核心的微服务框架,主要目标为了提升自己的相关技术,也为了给企业与个人提供一个零开发基础的微服务 ...

  9. 【每天学一点-05】使用umi.js代理,解决跨域问题(前端)

    一.user.ts 前端请求接口 import request from 'umi-request'; const getAway = '/user'; // 获取用户列表 export const ...

  10. ShardingSphere数据库读写分离

    码农在囧途 最近这段时间来经历了太多东西,无论是个人的压力还是个人和团队失误所带来的损失,都太多,被骂了很多,也被检讨,甚至一些不方便说的东西都经历了,不过还好,一切都得到了解决,无论好坏,这对于个人 ...