#5 #4 处理枚举和 servers参数

This commit is contained in:
luoyunchong 2020-08-25 02:48:52 +08:00
parent 1d63f2c585
commit 8ea53827f5
28 changed files with 587 additions and 13 deletions

View File

@ -22,6 +22,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwaggerUI_IndexStream_Knife
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IGeekFan.AspNetCore.Knife4jUI", "src\IGeekFan.AspNetCore.Knife4jUI\IGeekFan.AspNetCore.Knife4jUI.csproj", "{6C784918-BE29-4FEF-8AC3-9D34A38DE822}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebSites", "WebSites", "{86851B6C-3504-4879-8464-1DB422D46BA0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OAuth2Integration", "test\WebSites\OAuth2Integration\OAuth2Integration.csproj", "{9E8D8F42-33F0-4F2D-9B56-1AB1B33DE1FA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -44,6 +48,10 @@ Global
{6C784918-BE29-4FEF-8AC3-9D34A38DE822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6C784918-BE29-4FEF-8AC3-9D34A38DE822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6C784918-BE29-4FEF-8AC3-9D34A38DE822}.Release|Any CPU.Build.0 = Release|Any CPU
{9E8D8F42-33F0-4F2D-9B56-1AB1B33DE1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E8D8F42-33F0-4F2D-9B56-1AB1B33DE1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E8D8F42-33F0-4F2D-9B56-1AB1B33DE1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9E8D8F42-33F0-4F2D-9B56-1AB1B33DE1FA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -53,6 +61,8 @@ Global
{86A24FA0-E85D-4BDD-97D7-B990C50A40A9} = {75C51574-4CBD-403B-8182-8BF2A6DCFD43}
{1D6FD5CA-5D58-4895-8545-A93099CE1AD4} = {C146A419-15E0-4475-9623-706C5E2DCE0B}
{6C784918-BE29-4FEF-8AC3-9D34A38DE822} = {929BB2D7-C678-4BE8-8AA9-F271A2AE4545}
{86851B6C-3504-4879-8464-1DB422D46BA0} = {75C51574-4CBD-403B-8182-8BF2A6DCFD43}
{9E8D8F42-33F0-4F2D-9B56-1AB1B33DE1FA} = {86851B6C-3504-4879-8464-1DB422D46BA0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9D77CCB4-F597-421B-9EF9-52D4B0AC382D}

View File

@ -11,14 +11,14 @@
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/luoyunchong/IGeekFan.AspNetCore.Knife4jUI.git</RepositoryUrl>
<RootNamespace>IGeekFan.AspNetCore.Knife4jUI</RootNamespace>
<Version>0.0.4</Version>
<Version>0.0.6</Version>
<Company />
<Authors>igeekfan;xiaoym;</Authors>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Copyright>Apache License 2.0</Copyright>
<PackageLicenseExpression></PackageLicenseExpression>
<AssemblyVersion>0.0.4.0</AssemblyVersion>
<FileVersion>0.0.4.0</FileVersion>
<AssemblyVersion>0.0.6.0</AssemblyVersion>
<FileVersion>0.0.6.0</FileVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">

View File

@ -6,16 +6,17 @@
<meta name=viewport content="width=device-width,initial-scale=1">
<link rel=icon href=favicon.ico>
<title>%(DocumentTitle)</title>
<link href=knife4j/css/app.b2e62e71.css rel=preload as= style>
<link href=knife4j/js/app.ed7acb83.js rel=preload as=script>
<link href=knife4j/css/app.8efd8010.css rel=preload as= style>
<link href=knife4j/js/app.765df824.js rel=preload as=script>
<link href=knife4j/js/chunk-vendors.e86fea24.js rel=preload as=script>
<link href=knife4j/css/app.b2e62e71.css rel=stylesheet>
<link href=knife4j/css/app.8efd8010.css rel=stylesheet>
%(HeadContent)
</head>
<body>
<noscript><strong>We're sorry but knife4j-vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div>
<noscript><strong>We're sorry but knife4j-vue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript>
<div id=app></div>
<script src=knife4j/js/chunk-vendors.e86fea24.js></script>
<script src=knife4j/js/app.ed7acb83.js></script>
<script src=knife4j/js/app.765df824.js></script>
<script>
window.onload = function () {

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,57 @@
using System.Collections.Generic;
using IdentityServer4.Models;
using IdentityServer4.Test;
namespace OAuth2Integration.AuthServer
{
public static class Config
{
internal static IEnumerable<Client> Clients()
{
yield return new Client
{
ClientId = "test-id",
ClientName = "Test client (Code with PKCE)",
RedirectUris = new[] {
"http://localhost:55202/resource-server/swagger/oauth2-redirect.html", // IIS Express
"http://localhost:5000/resource-server/swagger/oauth2-redirect.html", // Kestrel
},
ClientSecrets = { new Secret("test-secret".Sha256()) },
RequireConsent = true,
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
AllowedScopes = new[] { "readAccess", "writeAccess" },
};
}
internal static IEnumerable<ApiResource> ApiResources()
{
yield return new ApiResource
{
Name = "api",
DisplayName = "API",
Scopes = new[]
{
new Scope("readAccess", "Access read operations"),
new Scope("writeAccess", "Access write operations")
}
};
}
internal static List<TestUser> TestUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "joebloggs",
Username = "joebloggs",
Password = "pass123"
}
};
}
}
}

View File

@ -0,0 +1,54 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using IdentityServer4;
using IdentityServer4.Test;
namespace OAuth2Integration.AuthServer.Controllers
{
[ApiExplorerSettings(IgnoreApi = true)]
[Route("account")]
public class AccountController : Controller
{
private readonly TestUserStore _userStore;
public AccountController()
{
_userStore = new TestUserStore(Config.TestUsers());
}
[HttpGet("login")]
public IActionResult Login(string returnUrl)
{
var viewModel = new LoginViewModel { Username = "joebloggs", Password = "pass123", ReturnUrl = returnUrl };
return View("/AuthServer/Views/Login.cshtml", viewModel);
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromForm]LoginViewModel viewModel)
{
if (!_userStore.ValidateCredentials(viewModel.Username, viewModel.Password))
{
ModelState.AddModelError("", "Invalid username or password");
viewModel.Password = string.Empty;
return View("/AuthServer/Views/Login.cshtml", viewModel);
}
// Use an IdentityServer-compatible ClaimsPrincipal
var identityServerUser = new IdentityServerUser(viewModel.Username);
identityServerUser.DisplayName = viewModel.Username;
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, identityServerUser.CreatePrincipal());
return Redirect(viewModel.ReturnUrl);
}
}
public class LoginViewModel
{
public string ReturnUrl { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,68 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using IdentityServer4.Stores;
using IdentityServer4.Services;
using IdentityServer4.Models;
namespace OAuth2Integration.AuthServer.Controllers
{
[ApiExplorerSettings(IgnoreApi = true)]
public class ConsentController : Controller
{
private readonly IIdentityServerInteractionService _interaction;
private readonly IClientStore _clientStore;
private readonly IResourceStore _resourceStore;
public ConsentController(
IIdentityServerInteractionService interaction,
IClientStore clientStore,
IResourceStore resourceStore)
{
_interaction = interaction;
_clientStore = clientStore;
_resourceStore = resourceStore;
}
[HttpGet("consent")]
public async Task<IActionResult> Consent(string returnUrl)
{
var request = await _interaction.GetAuthorizationContextAsync(returnUrl);
var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId);
var resource = await _resourceStore.FindApiResourceAsync("api");
var viewModel = new ConsentViewModel
{
ReturnUrl = returnUrl,
ClientName = client.ClientName,
ScopesRequested = resource.Scopes.Where(s => request.ScopesRequested.Contains(s.Name))
};
return View("/AuthServer/Views/Consent.cshtml", viewModel);
}
[HttpPost("consent")]
public async Task<IActionResult> Consent([FromForm]ConsentViewModel viewModel)
{
var request = await _interaction.GetAuthorizationContextAsync(viewModel.ReturnUrl);
// Communicate outcome of consent back to identityserver
var consentResponse = new ConsentResponse
{
ScopesConsented = viewModel.ScopesConsented
};
await _interaction.GrantConsentAsync(request, consentResponse);
return Redirect(viewModel.ReturnUrl);
}
}
public class ConsentViewModel
{
public string ReturnUrl { get; set; }
public string ClientName { get; set; }
public IEnumerable<Scope> ScopesRequested { get; set; }
public string[] ScopesConsented { get; set; }
}
}

View File

@ -0,0 +1,25 @@
@model OAuth2Integration.AuthServer.Controllers.ConsentViewModel
<style>
</style>
<h1>Consent</h1>
<p>
@Model.ClientName is requesting your permission to ...
</p>
<form asp-controller="Consent" asp-action="Consent">
<input type="hidden" asp-for="ReturnUrl" />
<fieldset>
<ul>
@foreach (var scope in Model.ScopesRequested)
{
<li>
<label>
<input name="ScopesConsented" type="checkbox" value="@scope.Name" checked />@scope.DisplayName
</label>
</li>
}
</ul>
</fieldset>
<button type="submit">Grant Access</button>
</form>

View File

@ -0,0 +1,23 @@
@model OAuth2Integration.AuthServer.Controllers.LoginViewModel
<style>
input {
display: block;
}
</style>
<h1>Login</h1>
<form asp-controller="Account" asp-action="Login">
<input type="hidden" asp-for="ReturnUrl" />
<fieldset>
<label>
Username:
<input type="text" asp-for="Username" />
</label>
<label>
Password:
<input type="text" asp-for="Password" />
</label>
</fieldset>
<button type="submit">Login</button>
</form>

View File

@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="3.0.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.5.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.5.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUi" Version="5.5.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace OAuth2Integration
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55202",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "resource-server/swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"OAuth2Integration": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "resource-server/swagger",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
namespace OAuth2Integration.ResourceServer.Controllers
{
[Route("products")]
[Authorize(AuthenticationSchemes = "Bearer")]
public class ProductsController : Controller
{
[HttpGet]
[Authorize("readAccess")]
public IEnumerable<Product> GetProducts()
{
yield return new Product
{
Id = 1,
SerialNo = "ABC123",
};
}
[HttpGet("{id}")]
[Authorize("readAccess")]
public Product GetProduct(int id)
{
return new Product
{
Id = 1,
SerialNo = "ABC123",
};
}
[HttpPost]
[Authorize("writeAccess")]
public void CreateProduct([FromBody]Product product)
{
}
[HttpDelete("{id}")]
[Authorize("writeAccess")]
public void DeleteProduct(int id)
{
}
}
public class Product
{
public int Id { get; internal set; }
public string SerialNo { get; set; }
public ProductStatus Status { get; set; }
}
public enum ProductStatus
{
InStock, ComingSoon
}
}

View File

@ -0,0 +1,40 @@
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace OAuth2Integration.ResourceServer.Swagger
{
public class SecurityRequirementsOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// Policy names map to scopes
var requiredScopes = context.MethodInfo
.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Select(attr => attr.Policy)
.Distinct();
if (requiredScopes.Any())
{
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[ oAuthScheme ] = requiredScopes.ToList()
}
};
}
}
}
}

View File

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
namespace OAuth2Integration
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Register IdentityServer services to power OAuth2.0 flows
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryClients(AuthServer.Config.Clients())
.AddInMemoryApiResources(AuthServer.Config.ApiResources())
.AddTestUsers(AuthServer.Config.TestUsers());
// The auth setup is a little nuanced because this app provides the auth-server & the resource-server
// Use the "Cookies" scheme by default & explicitly require "Bearer" in the resource-server controllers
// See https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?tabs=aspnetcore2x
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
.AddIdentityServerAuthentication(c =>
{
c.Authority = "http://localhost:55202/auth-server/";
c.RequireHttpsMetadata = false;
c.ApiName = "api";
});
// Configure named auth policies that map directly to OAuth2.0 scopes
services.AddAuthorization(c =>
{
c.AddPolicy("readAccess", p => p.RequireClaim("scope", "readAccess"));
c.AddPolicy("writeAccess", p => p.RequireClaim("scope", "writeAccess"));
});
services.AddControllersWithViews();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "Test API V1" });
// Define the OAuth2.0 scheme that's in use (i.e. Implicit Flow)
c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("/auth-server/connect/authorize", UriKind.Relative),
TokenUrl = new Uri("/auth-server/connect/token", UriKind.Relative),
Scopes = new Dictionary<string, string>
{
{ "readAccess", "Access read operations" },
{ "writeAccess", "Access write operations" }
}
}
}
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
},
new[] { "readAccess", "writeAccess" }
}
});
// Assign scope requirements to operations based on AuthorizeAttribute
//c.OperationFilter<SecurityRequirementsOperationFilter>();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Map("/auth-server", authServer =>
{
authServer.UseRouting();
authServer.UseAuthentication();
authServer.UseIdentityServer();
authServer.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
});
app.Map("/resource-server", resourceServer =>
{
resourceServer.UseRouting();
resourceServer.UseAuthentication();
resourceServer.UseAuthorization();
resourceServer.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
resourceServer.UseSwagger();
resourceServer.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/resource-server/swagger/v1/swagger.json", "My API V1");
c.EnableDeepLinking();
// Additional OAuth settings (See https://github.com/swagger-api/swagger-ui/blob/v3.10.0/docs/usage/oauth2.md)
c.OAuthClientId("test-id");
c.OAuthClientSecret("test-secret");
c.OAuthAppName("test-app");
c.OAuthScopeSeparator(" ");
c.OAuthUsePkce();
});
});
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1 @@
{"KeyId":"08433185bdf583112d2df84b15adc5fe","Parameters":{"D":"G5yB2Ha3ktEmpXH+1qIuJ/dJ8TJKFAj2j1qMLl/Zu0jI1GZJlCyeWHEM0B0IFlbkUq2W82ym9dJUaSGXC3FxSZ/JHwNzgonpv9ELQZDnAwvO0kpnJTw+SQWwDxUxItGNT8giqqm5LcSYWhv+QexepyEOwKr6Efh6hHrTsjxk/2t3xPhZJbutifcBrAaHb0wjV/mw442wXol1cjWmG5qVOj54n16kU2Pxr1i9oukZBJMYLltg8NB99KgvZSVvOoctXiPszEHel4kNdSv0R3yt2lBad+OEZykCgzgbZYmA2aIvUmB8r+yxFUiXEDuM9Wjp3gxBLdAWAdutPhtcXqPXGw==","DP":"fVcFyftVuEEFERT6UzfnIhDW/2A1ckzwutVQAYOjRwC+FyRIGyN6kzc1SSIhpuMECcibvPBp0wbyx0JsUbcUVlo6uKXaYx5h6Qcv4fIrqfFOUfVNuQ2W8mltwpDdFuquwOAEz08zMtkVavZxfghmJgYNeqqQvvuvetjAuL6Tdws=","DQ":"whD6E1yg5fe3FDcq+cVQnj3xeSWB7pYURNIYAI2Oo1y2fCfH7/TDaQbGpIr0nO/RiH2SxH1vJ5O5foKG0JES3wCe7qflJOviOzb3SNlXm3gCdvNZyAruA2k9wyro5hauDMU51saed6xAy3dL+Q5fKE+0fmKOBxVZr2RPMnglTc0=","Exponent":"AQAB","InverseQ":"tQt+mEXZQq9YpAU6Q7/8gpPBy+F3B52KmP3e5bcnvilSDIXOUjye8NMTd8WeX+NmOje+g+5aYW5kVyiXhwoVM1Bz3Iwyq51Kix3JMmaiFBPaC/r9jF/2JveiT3iKlzgi0RJcOM4rnA1pHakr3rz6nVoxpBGe8OMUTrg4cVkM4HU=","Modulus":"y6F+tcOmGz6DAYfooFDMtkeG5/7TqOpW3U3eSu4KmuoCMdc8hiJwspcwu6Yu6OGVbYKW1nZTEP2SizpTixy6W/TYFMIQZC08iBxR8mDlp2HXeGiOTMFJQFdgT+Qe0xgZ9yPdOSbBmih6dATK61k4QvDbrhAf8j7mv7JW4s5T6WCjmx0UOyo9medzP/YhCL6lnaxP3hWA61ncVK38BGHFzo/sZSyA79EXo/091CKm1V0VpZMhpZIt6wyXPg8bLyJh1edXFCqEYDXvVcfuFFhmILfB/g5CUz/YCJu2Z1VDG4OOXPtg6xJMAbcl/67yH0wu/HSNoUcLN1cJkYXAUsg1yQ==","P":"6Tl+cOLjTwUiQbTiVtC4v2uO4FcjsuKbWlqKiqJ7veMJQHdP/AOPNtLq9eKrpKfc6f0eRQHYLf//o1ntweFFBhZ3lgCAdD3gQw02meMCxSPt+BMgylFkNZXW0TfISx/a3q4XGBSLTmu0d5uN3FNxRDNmAKeL8xgvb/ikKQSHnGM=","Q":"34Qr0kjiZkXvxHY6pxfBmzwYitC/bBwxwOrLq8S30FN5G2QQ/KWc7EJnOJz3C+yX9Vep2Fslu5xYAoBjbEofVMRlvsu2T4c+d/KfUQLaualXNlQiR4TKqW13Dau+ZEDEh71NFdJwqhKzJb8ojiRMcMHD1M5AmDApDUfqAOoybuM="}}