{"id":297360,"date":"2020-01-20T03:00:10","date_gmt":"2020-01-20T03:00:10","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=297360"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=297360","title":{"rendered":"Blazor Client Side \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u041c\u0430\u0433\u0430\u0437\u0438\u043d: \u0427\u0430\u0441\u0442\u044c 1 \u2014 \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f oidc (oauth2) + Identity Server4"},"content":{"rendered":"\n<div class=\"post__text post__text-html\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/484596\/\">\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u0422\u0430\u043a\u0438 \u0434\u0430, \u0432 \u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0441\u0432\u043e\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u043b \u0441\u0434\u0435\u043b\u0430\u0442\u044c Todo List \u043d\u0430 Blazor Wasm \u0438 \u043e\u0441\u0442\u0430\u043b\u0441\u044f \u0434\u043e\u0432\u043e\u043b\u0435\u043d. \u0422\u0435\u043f\u0435\u0440\u044c \u044f \u0440\u0435\u0448\u0438\u043b \u0432\u0437\u044f\u0442\u044c\u0441\u044f \u0437\u0430 \u0447\u0442\u043e-\u0442\u043e \u043f\u043e \u0441\u0435\u0440\u044c\u0435\u0437\u043d\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u0432 \u0434\u0435\u043b\u0435. \u0411\u0443\u0434\u0443 \u0434\u0435\u043b\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0442\u0435\u043d\u044c\u043a\u0438\u0439 SPA UI \u043d\u0430 Blazor \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e \u0432\u044b\u043c\u044b\u0448\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u043c\u0430\u0433\u0430\u0437\u0438\u043d\u0430. \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u0438\u0431\u043b\u0438\u0436\u0435\u043d\u043d\u044b\u0439 \u043a \u0431\u043e\u0435\u0432\u043e\u043c\u0443 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044e \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u041d\u0430\u0447\u043d\u0443 \u044f \u0441 \u0442\u043e\u0433\u043e \u0447\u0442\u043e \u0437\u0430\u043f\u0438\u043b\u044e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0438\u0445 \u043f\u043e \u0440\u043e\u043b\u044f\u043c \u0442. \u0435. \u0447\u0442\u043e\u0431\u044b \u0430\u0434\u043c\u0438\u043d \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u0438\u0434\u0435\u043b\u0438 \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441. \u0415\u0449\u0435 \u044f \u044d\u0442\u043e \u0432\u0441\u0435 \u0432 docker \u043e\u0431\u0440\u0430\u0437\u044b \u0441\u043e\u0431\u0440\u0430\u043b \u0438 \u043d\u0430 docker registry \u0432\u044b\u043b\u043e\u0436\u0438\u043b. \u0417\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u0434\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u043a\u0430\u0442. <a name=\"habracut\"><\/a><\/p>\n<h2>\u0421\u0441\u044b\u043b\u043a\u0438<\/h2>\n<p>  <a href=\"https:\/\/gitlab.com\/VictorWinbringer\/blazoreshop\" rel=\"nofollow\">\u0418\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0438<\/a><br \/>  <a href=\"https:\/\/hub.docker.com\/u\/victorcallidus\" rel=\"nofollow\">\u041e\u0431\u0440\u0430\u0437\u044b \u043d\u0430 Docker Registry <\/a><\/p>\n<p>  \u0427\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442, \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b docker-compose.yml \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u0443 docker-compose up \u0432 \u0442\u043e\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438, \u0433\u0434\u0435 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u044d\u0442\u043e\u0442 \u0444\u0430\u0439\u043b. \u0415\u0449\u0435 \u043a\u043e\u043d\u0435\u0447\u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0447\u0442\u043e\u0431\u044b \u0443 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0431\u044b\u043b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0434\u043e\u043a\u0435\u0440 \u043d\u0430 \u043a\u043e\u043c\u043f\u0435 \u0438 \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0435\u043c\u0443 \u043d\u0430\u0434\u043e \u0431\u0443\u0434\u0435\u0442 \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043c\u043e\u0438 \u043e\u0431\u0440\u0430\u0437\u044b. \u041c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u044b \u0441\u043b\u0443\u0448\u0430\u044e\u0442 \u0430\u0434\u0440\u0435\u0441\u0430 <b><a href=\"https:\/\/localhost:8000\" rel=\"nofollow\">https:\/\/localhost:8000<\/a><\/b> <b><a href=\"https:\/\/localhost:8001\" rel=\"nofollow\">https:\/\/localhost:8001<\/a><\/b> <b><a href=\"https:\/\/localhost:8002\" rel=\"nofollow\">https:\/\/localhost:8002<\/a><\/b> \u0438 <b><a href=\"https:\/\/localhost:8003\" rel=\"nofollow\">https:\/\/localhost:8003<\/a><\/b>.<br \/>  \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0435 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u043c\u0438\u043a\u0440\u043e\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432 \u0442\u0435\u0440\u043c\u0438\u043d\u0430\u043b\u0435 Windows. \u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c net core \u043f\u0435\u0440\u0435\u0434 \u044d\u0442\u0438\u043c.  <\/p>\n<pre><code class=\"bash\"> dotnet --info  dotnet dev-certs https --trust  dotnet dev-certs https -ep $env:APPDATA\\ASP.NET\\Https\\blazor-eshop-api.pfx -p 1234Qwert  dotnet dev-certs https -ep $env:APPDATA\\ASP.NET\\Https\\blazor-eshop-spa-angular.pfx -p 1234Qwert dotnet dev-certs https -ep $env:APPDATA\\ASP.NET\\Https\\blazor-eshop-spa-blazor.pfx -p 1234Qwert dotnet dev-certs https -ep $env:APPDATA\\ASP.NET\\Https\\blazor-eshop-sso.pfx -p 1234Qwert <\/code><\/pre>\n<h2>\u0411\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 WASM \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 <\/h2>\n<p>  \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c <a href=\"https:\/\/www.nuget.org\/packages\/Sotsera.Blazor.Oidc\/\" rel=\"nofollow\">www.nuget.org\/packages\/Sotsera.Blazor.Oidc<\/a> \u0438 \u0440\u0430\u0434\u0443\u0435\u043c\u0441\u044f \u0436\u0438\u0437\u043d\u0438<\/p>\n<h2>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Identity Server4<\/h2>\n<p>  \u0412 \u043e\u0431\u0449\u0435\u043c \u044f \u0432\u0437\u044f\u043b \u0433\u043e\u0442\u043e\u0432\u044b\u0439 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u0442\u0443\u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u0441\u0432\u043e\u0435\u0433\u043e SPA \u043a\u043b\u0438\u0435\u043d\u0442\u0430. \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0430\u043c\u043e\u0433\u043e Identity Server4 \u0432\u044b\u0445\u043e\u0434\u0438\u0442 \u0437\u0430 \u0440\u0430\u043c\u043a\u0438 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d\u0430 \u043f\u0440\u043e Blazor. \u0415\u0441\u043b\u0438 \u0432\u0430\u043c \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e, \u0442\u043e \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0432 \u043c\u043e\u0438\u0445 \u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0430\u0445.<br \/>  \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043d\u0430\u0448 \u043a\u043b\u0438\u0435\u043d\u0442 \u043a \u0441\u043f\u0438\u0441\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432  <\/p>\n<pre><code class=\"cs\">              new Client                 {                     ClientId = &quot;spaBlazorClient&quot;,                     ClientName = &quot;SPA Blazor Client&quot;,                      RequireClientSecret = false,                     RequireConsent = false,                      RedirectUris = new List&lt;string&gt;                     {                         $&quot;{clientsUrl[&quot;SpaBlazor&quot;]}\/oidc\/callbacks\/authentication-redirect&quot;,                         $&quot;{clientsUrl[&quot;SpaBlazor&quot;]}\/_content\/Sotsera.Blazor.Oidc\/silent-renew.html&quot;,                         $&quot;{clientsUrl[&quot;SpaBlazor&quot;]}&quot;,                     },                     PostLogoutRedirectUris = new List&lt;string&gt;                     {                         $&quot;{clientsUrl[&quot;SpaBlazor&quot;]}\/oidc\/callbacks\/logout-redirect&quot;,                         $&quot;{clientsUrl[&quot;SpaBlazor&quot;]}&quot;,                     },                     AllowedCorsOrigins = new List&lt;string&gt;                     {                         $&quot;{clientsUrl[&quot;SpaBlazor&quot;]}&quot;,                     },                      AllowedGrantTypes = GrantTypes.Code,                     AllowedScopes = { &quot;openid&quot;, &quot;profile&quot;, &quot;email&quot;, &quot;api&quot; },                      AllowOfflineAccess = true,                     RefreshTokenUsage = TokenUsage.ReUse                 } <\/code><\/pre>\n<p>  \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0435\u0449\u0435 \u0438 \u0440\u043e\u043b\u0438 \u0432 JWT \u0442\u043e\u043a\u0435\u043d\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u0435\u043c \u0441\u0432\u043e\u0439 IProfileService   <\/p>\n<pre><code class=\"cs\">using IdentityModel; using IdentityServer4.Models; using IdentityServer4.Services; using Microsoft.AspNetCore.Identity; using Microsoft.eShopOnContainers.Services.Identity.API.Models; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Threading.Tasks;  namespace Microsoft.eShopOnContainers.Services.Identity.API.Services {     public class ProfileService : IProfileService     {         private readonly UserManager&lt;ApplicationUser&gt; _userManager;          public ProfileService(UserManager&lt;ApplicationUser&gt; userManager)         {             _userManager = userManager;         }          async public Task GetProfileDataAsync(ProfileDataRequestContext context)         {             var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));              var subjectId = subject.Claims.Where(x =&gt; x.Type == &quot;sub&quot;).FirstOrDefault().Value;              var user = await _userManager.FindByIdAsync(subjectId);             if (user == null)                 throw new ArgumentException(&quot;Invalid subject identifier&quot;);             var claims = GetClaimsFromUser(user);             context.IssuedClaims = claims.ToList();             var roles = await _userManager.GetRolesAsync(user);             foreach (var role in roles)             {                 context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));             }                     }          async public Task IsActiveAsync(IsActiveContext context)         {             var subject = context.Subject ?? throw new ArgumentNullException(nameof(context.Subject));              var subjectId = subject.Claims.Where(x =&gt; x.Type == &quot;sub&quot;).FirstOrDefault().Value;             var user = await _userManager.FindByIdAsync(subjectId);              context.IsActive = false;              if (user != null)             {                 if (_userManager.SupportsUserSecurityStamp)                 {                     var security_stamp = subject.Claims.Where(c =&gt; c.Type == &quot;security_stamp&quot;).Select(c =&gt; c.Value).SingleOrDefault();                     if (security_stamp != null)                     {                         var db_security_stamp = await _userManager.GetSecurityStampAsync(user);                         if (db_security_stamp != security_stamp)                             return;                     }                 }                  context.IsActive =                     !user.LockoutEnabled ||                     !user.LockoutEnd.HasValue ||                     user.LockoutEnd &lt;= DateTime.Now;             }         }          private IEnumerable&lt;Claim&gt; GetClaimsFromUser(ApplicationUser user)         {             var claims = new List&lt;Claim&gt;             {                 new Claim(JwtClaimTypes.Subject, user.Id),                 new Claim(JwtClaimTypes.PreferredUserName, user.UserName),                 new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName)             };              if (!string.IsNullOrWhiteSpace(user.Name))                 claims.Add(new Claim(&quot;name&quot;, user.Name));              if (!string.IsNullOrWhiteSpace(user.LastName))                 claims.Add(new Claim(&quot;last_name&quot;, user.LastName));              if (!string.IsNullOrWhiteSpace(user.CardNumber))                 claims.Add(new Claim(&quot;card_number&quot;, user.CardNumber));              if (!string.IsNullOrWhiteSpace(user.CardHolderName))                 claims.Add(new Claim(&quot;card_holder&quot;, user.CardHolderName));              if (!string.IsNullOrWhiteSpace(user.SecurityNumber))                 claims.Add(new Claim(&quot;card_security_number&quot;, user.SecurityNumber));              if (!string.IsNullOrWhiteSpace(user.Expiration))                 claims.Add(new Claim(&quot;card_expiration&quot;, user.Expiration));              if (!string.IsNullOrWhiteSpace(user.City))                 claims.Add(new Claim(&quot;address_city&quot;, user.City));              if (!string.IsNullOrWhiteSpace(user.Country))                 claims.Add(new Claim(&quot;address_country&quot;, user.Country));              if (!string.IsNullOrWhiteSpace(user.State))                 claims.Add(new Claim(&quot;address_state&quot;, user.State));              if (!string.IsNullOrWhiteSpace(user.Street))                 claims.Add(new Claim(&quot;address_street&quot;, user.Street));              if (!string.IsNullOrWhiteSpace(user.ZipCode))                 claims.Add(new Claim(&quot;address_zip_code&quot;, user.ZipCode));              if (_userManager.SupportsUserEmail)             {                 claims.AddRange(new[]                 {                     new Claim(JwtClaimTypes.Email, user.Email),                     new Claim(JwtClaimTypes.EmailVerified, user.EmailConfirmed ? &quot;true&quot; : &quot;false&quot;, ClaimValueTypes.Boolean)                 });             }              if (_userManager.SupportsUserPhoneNumber &amp;&amp; !string.IsNullOrWhiteSpace(user.PhoneNumber))             {                 claims.AddRange(new[]                 {                     new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber),                     new Claim(JwtClaimTypes.PhoneNumberVerified, user.PhoneNumberConfirmed ? &quot;true&quot; : &quot;false&quot;, ClaimValueTypes.Boolean)                 });             }              return claims;         }     } } <\/code><\/pre>\n<p>  \u0422\u0443\u0442 \u0432\u0441\u044f \u0441\u0443\u0442\u044c \u0432 \u044d\u0442\u043e\u043c \u043a\u0443\u0441\u043a\u0435 \u043a\u043e\u0434\u0430   <\/p>\n<pre><code class=\"cs\">   var roles = await _userManager.GetRolesAsync(user);   foreach (var role in roles)   {      context.IssuedClaims.Add(new Claim(JwtClaimTypes.Role, role));   }<\/code><\/pre>\n<p>  \u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0435\u0433\u043e \u0432 asp.net   <\/p>\n<pre><code class=\"cs\">services.AddIdentityServer().AddProfileService&lt;ProfileService&gt;()<\/code><\/pre>\n<h2>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430<\/h2>\n<p>  \u0422\u0443\u0442 \u044f \u0432\u044b\u0431\u0440\u0430\u043b ASP.NET Core hosted \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043c\u043d\u0435 \u0442\u0430\u043a \u043f\u0440\u043e\u0449\u0435 \u0431\u044b\u043b\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c. \u041f\u0440\u043e\u0449\u0435 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043a\u0435\u0440 \u043e\u0431\u0440\u0430\u0437. \u041c\u043e\u0436\u043d\u043e \u0438 \u043d\u0430 nginx \u0440\u0430\u0437\u043c\u0435\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u0432\u043d\u0443\u0442\u0440\u0438 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430.<br \/>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/eh\/ez\/5e\/ehez5ei_v0untezw-n132dvrhb4.png\"><br \/>  <img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/i1\/0a\/sc\/i10ascbtjvdueaqdyzvsmhl5yfu.png\"><\/p>\n<h2>\u041f\u0435\u0440\u0435\u0434\u0430\u0447\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f<\/h2>\n<h3>\u041d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430<\/h3>\n<p>  \u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a   <\/p>\n<pre><code class=\"cs\">     public class ConfigModel     {         public string SsoUri { get; set; } = string.Empty;         public string ApiUri { get; set; } = string.Empty;     } <\/code><\/pre>\n<p>  \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u043c \u0435\u0435 \u0432 Startup.cs<\/p>\n<pre><code class=\"cs\">         public void ConfigureServices(IServiceCollection services)         {             services.AddMvc();             services.AddResponseCompression(opts =&gt;             {                 opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(                     new[] { &quot;application\/octet-stream&quot; });             });             services.Configure&lt;ConfigModel&gt;(Configuration);         } <\/code><\/pre>\n<p>  \u041f\u0435\u0440\u0435\u0434\u0430\u0435\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u0443 \u0432 \u0432\u0438\u0434\u0435 json <\/p>\n<pre><code class=\"cs\"> using BlazorEShop.Shared.Presentation; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options;  namespace BlazorEShop.Spa.BlazorWasm.Server.Controllers {     [Route(&quot;api\/v1\/config&quot;)]     [ApiController]     public class ConfigController : ControllerBase     {         private readonly IOptionsSnapshot&lt;ConfigModel&gt; _configuration;          public ConfigController(IOptionsSnapshot&lt;ConfigModel&gt; configuration)         {             _configuration = configuration;         }         \/\/ GET: api\/&lt;controller&gt;         [HttpGet]         public ConfigModel Get()         {             return _configuration.Value;         }     } } <\/code><\/pre>\n<h3>\u041d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430<\/h3>\n<p>  \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0438\u0445 \u0432 \u043d\u0430\u0448 DI \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440  <\/p>\n<pre><code class=\"cs\"> using System; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using BlazorEShop.Shared.Presentation; using Microsoft.AspNetCore.Blazor.Hosting; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection;  namespace BlazorEShop.Spa.BlazorWasm.Client {     public class Program     {         public static void Main(string[] args)         {             Task.Run(async () =&gt;             {                 ConfigModel cfg = null;                 var host = BlazorWebAssemblyHost.CreateDefaultBuilder().Build();                 using (var scope = host.Services.CreateScope())                 {                     var nm = scope.ServiceProvider.GetRequiredService&lt;NavigationManager&gt;();                     var uri = nm.BaseUri;                     Console.WriteLine($&quot;BASE URI: {uri}&quot;);                     cfg = await GetConfig($&quot;{(uri.EndsWith('\/') ? uri : uri + &quot;\/&quot;)}api\/v1\/config&quot;);                 }                 await BlazorWebAssemblyHost                     .CreateDefaultBuilder()                     .ConfigureServices(x =&gt; x.AddScoped&lt;ConfigModel&gt;(y =&gt; cfg))                     .UseBlazorStartup&lt;Startup&gt;()                     .Build()                     .StartAsync()                      .ContinueWith((a, b) =&gt; Console.WriteLine(a.Exception), null);             });             Console.WriteLine(&quot;END MAIN&quot;);         }          private static async Task&lt;ConfigModel&gt; GetConfig(string url)         {             using var client = new HttpClient();             var cfg = await client                 .GetJsonAsync&lt;ConfigModel&gt;(url);             return cfg;         }     } } <\/code><\/pre>\n<p>  \u0422\u0430\u043a \u043a\u0430\u043a Blazor Wasm \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 async void Main, \u0430 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c Result \u0443 Task \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442 \u043a \u0434\u0435\u0434\u043a\u043b\u043e\u043a\u0443 \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u043e\u0442\u043e\u043a \u0443 \u043d\u0430\u0441 \u043e\u0434\u0438\u043d \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0432 Task.Run( async () =&gt;{});<\/p>\n<h2>\u0410\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u044f oidc(oauth2) \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430<\/h2>\n<p>  \u0412\u044b\u0437\u044b\u0432\u0430\u0435\u043c services.AddOidc \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 ConfigModel.  <\/p>\n<pre><code class=\"cs\"> using Microsoft.AspNetCore.Components.Builder; using Microsoft.Extensions.DependencyInjection; using Sotsera.Blazor.Oidc; using System; using System.Net.Http; using System.Threading.Tasks; using BlazorEShop.Shared.Presentation; using Microsoft.AspNetCore.Components;  namespace BlazorEShop.Spa.BlazorWasm.Client {     public class Startup     {         public async void ConfigureServices(IServiceCollection services)         {             var provider = services.BuildServiceProvider();             var cfg = provider.GetService&lt;ConfigModel&gt;();             services.AddOidc(new Uri(cfg.SsoUri), (settings, siteUri) =&gt;             {                 settings.UseDefaultCallbackUris(siteUri);                 settings.ClientId = &quot;spaBlazorClient&quot;;                 settings.ResponseType = &quot;code&quot;;                 settings.Scope = &quot;openid profile email api&quot;;                 settings.UseRedirectToCallerAfterAuthenticationRedirect();                 settings.UseRedirectToCallerAfterLogoutRedirect();                 settings.MinimumLogeLevel = Microsoft.Extensions.Logging.LogLevel.Information;                 settings.LoadUserInfo = true;                 settings.FilterProtocolClaims = true;                 settings.MonitorSession = true;                 settings.StorageType = Sotsera.Blazor.Oidc.Configuration.Model.StorageType.LocalStorage;             });         }          public void Configure(IComponentsApplicationBuilder app)         {             app.AddComponent&lt;App&gt;(&quot;app&quot;);         }     } } <\/code><\/pre>\n<h2>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0433\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 App.razor<\/h2>\n<p>   App.blazor -\u0418\u0437\u043c\u0435\u043d\u044f\u0435\u043c \u0435\u0433\u043e \u0442\u0430\u043a \u0447\u0442\u043e\u0431\u044b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0438 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0432\u0438\u0434\u0435\u043b\u0438 \u0440\u0430\u0437\u043d\u044b\u0439 \u0442\u0435\u043a\u0441\u0442 \u0438 \u0447\u0442\u043e\u0431\u044b \u0431\u044b\u043b\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u044b \u0438\u0437 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0434\u043b\u044f oidc  <\/p>\n<pre><code class=\"cs\"> @using BlazorEShop.Shared.Presentation @using Microsoft.AspNetCore.Components @using Microsoft.Extensions.DependencyInjection @using Sotsera.Blazor.Oidc @inject IUserManager UserManager  &lt;Router AppAssembly=&quot;@typeof(Program).Assembly&quot; AdditionalAssemblies=&quot;new[] { typeof(IUserManager).Assembly }&quot;&gt;     &lt;Found Context=&quot;routeData&quot;&gt;         &lt;AuthorizeRouteView RouteData=&quot;@routeData&quot; DefaultLayout=&quot;@typeof(MainLayout)&quot;&gt;             &lt;NotAuthorized&gt;                 &lt;h3&gt;Sorry&lt;\/h3&gt;                 &lt;p&gt;You're not authorized to reach this page.&lt;\/p&gt;                 &lt;p&gt;You may need to log in as a different user.&lt;\/p&gt;             &lt;\/NotAuthorized&gt;             &lt;Authorizing&gt;                 &lt;h3&gt;Authentication in progress&lt;\/h3&gt;             &lt;\/Authorizing&gt;         &lt;\/AuthorizeRouteView&gt;     &lt;\/Found&gt;     &lt;NotFound&gt;         &lt;CascadingAuthenticationState&gt;             &lt;LayoutView Layout=&quot;@typeof(MainLayout)&quot;&gt;                 &lt;h3&gt;Sorry&lt;\/h3&gt;                 &lt;p&gt;Sorry, there's nothing at this address.&lt;\/p&gt;             &lt;\/LayoutView&gt;         &lt;\/CascadingAuthenticationState&gt;     &lt;\/NotFound&gt; &lt;\/Router&gt; <\/code><\/pre>\n<h2>\u0412\u0445\u043e\u0434 \u0438 \u0432\u044b\u0445\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f<\/h2>\n<p>  \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u044f\u043b\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 IUserManager. \u0415\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u0437 DI \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:   <\/p>\n<pre><code class=\"cs\"> @inject IUserManager UserManager @using Sotsera.Blazor.Oidc @using Microsoft.Extensions.DependencyInjection &lt;AuthorizeView&gt;     &lt;Authorized&gt;         &lt;span class=&quot;login-display-name mr-3&quot;&gt;             Hello, @context.User.Identity.Name!         &lt;\/span&gt;         &lt;button type=&quot;button&quot; class=&quot;btn btn-primary btn-sm&quot; @onclick=&quot;LogoutRedirect&quot;&gt;             Log out         &lt;\/button&gt;     &lt;\/Authorized&gt;     &lt;NotAuthorized&gt;         &lt;button type=&quot;button&quot; class=&quot;btn btn-primary btn-sm&quot; @onclick=&quot;LoginRedirect&quot;&gt;             Log in         &lt;\/button&gt;     &lt;\/NotAuthorized&gt; &lt;\/AuthorizeView&gt;  @code {     public async void LoginRedirect() =&gt; await UserManager.BeginAuthenticationAsync(p =&gt; p.WithRedirect());      public async void LogoutRedirect() =&gt; await UserManager.BeginLogoutAsync(p =&gt; p.WithRedirect()); } <\/code><\/pre>\n<h2>\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0438 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f<\/h2>\n<p>  \u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u043d\u043e \u0432 \u043b\u044e\u0431\u043e\u0439 \u0447\u0430\u0441\u0442\u0438 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e AuthorizeView \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u0432\u0438\u0434\u0435\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438. \u041c\u043e\u0436\u043d\u043e \u0442\u0430\u043a\u0436\u0435 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Roles \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0441 \u043a\u0430\u043a\u0438\u043c\u0438 \u0440\u043e\u043b\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0432\u0438\u0434\u0435\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442.<\/p>\n<pre><code class=\"xml\">&lt;AuthorizeView Roles=&quot;admin, administrator&quot;&gt;     &lt;Authorized&gt;         &lt;p&gt;User Info&lt;\/p&gt;         &lt;p&gt;@context.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)&lt;\/p&gt;         @foreach (var c in context.User.Claims)         {             &lt;p&gt;@c.Type : @c.Value : @string.Join(&quot;;&quot;, c.Properties.Select(x =&gt; $&quot;{x.Key} : {x.Value}&quot;))&lt;\/p&gt;         }      &lt;\/Authorized&gt;     &lt;NotAuthorized&gt;     &lt;p&gt;\u0412\u044b \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u044b \u0438\u043b\u0438 \u043d\u0435 \u0438\u043c\u0435\u0435\u0442\u0435 \u0440\u043e\u043b\u0438 admin \u0438\u043b\u0438 \u0440\u043e\u043b\u0438 administrator&lt;\/p&gt;     &lt;\/NotAuthorized&gt; &lt;\/AuthorizeView&gt; <\/code><\/pre>\n<h2>\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u043c \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0441\u0442\u0438 \u0438 \u0440\u043e\u043b\u0435\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f <\/h2>\n<p>  \u0414\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u0432\u0441\u0435 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u043c \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u043e\u043c Authorize. \u041a\u043e\u043d\u0435\u0447\u043d\u043e \u0436\u0435 \u0432\u0430\u043c \u043b\u0443\u0447\u0448\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043f\u0440\u0430\u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0442\u043e\u0436\u0435. <br \/>  \u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u0445\u043e\u0434\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0441 \u043b\u044e\u0431\u044b\u043c\u0438 \u0440\u043e\u043b\u044f\u043c\u0438 <\/p>\n<pre><code class=\"xml\">@page &quot;\/user&quot; @attribute [Authorize]  &lt;h1&gt;\u0412\u044b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0441 \u043b\u044e\u0431\u044b\u043c\u0438 \u0440\u043e\u043b\u044f\u043c\u0438&lt;\/h1&gt; <\/code><\/pre>\n<p>  \u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u0445\u043e\u0434\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0435\u0441\u0442\u044c \u0440\u043e\u043b\u044c admin \u0438\u043b\u0438 boss <\/p>\n<pre><code class=\"xml\">@page &quot;\/admin&quot; @attribute [Authorize(Roles=&quot;admin, boss&quot;)]  &lt;h1&gt;\u0412\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0435\u0441\u0442\u044c \u0440\u043e\u043b\u044c admin \u0438\u043b\u0438 boss&lt;\/h1&gt; <\/code><\/pre>\n<h2>\u041e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a API<\/h2>\n<p>  \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043b\u0443\u0436\u0438\u0442 OidcHttpClient \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u0437 DI \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430. \u041e\u043d \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u043e\u043c \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u0442\u043e\u043a\u0435\u043d \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440:  <\/p>\n<pre><code class=\"cs\"> @page &quot;\/fetchdata&quot; @inject Sotsera.Blazor.Oidc.OidcHttpClient Http @inject BlazorEShop.Shared.Presentation.ConfigModel Config  @using BlazorEShop.Shared.Presentation &lt;h1&gt;Weather forecast&lt;\/h1&gt;  &lt;p&gt;This component demonstrates fetching data from the server.&lt;\/p&gt;  @if (products == null) {     &lt;p&gt;&lt;em&gt;Loading...&lt;\/em&gt;&lt;\/p&gt; } else {     &lt;table class=&quot;table&quot;&gt;         &lt;thead&gt;             &lt;tr&gt;                 &lt;th&gt;Id&lt;\/th&gt;                 &lt;th&gt;Version&lt;\/th&gt;             &lt;\/tr&gt;         &lt;\/thead&gt;         &lt;tbody&gt;             @foreach (var product in products.Value)             {                 &lt;tr&gt;                     &lt;td&gt;@product.Id&lt;\/td&gt;                     &lt;td&gt;@product.Version&lt;\/td&gt;                 &lt;\/tr&gt;             }         &lt;\/tbody&gt;     &lt;\/table&gt; }  @code {     private PageResultModel&lt;ProductModel&gt; products;      protected override async Task OnInitializedAsync()     {         var uri = Config.ApiUri;         products = await Http.GetJsonAsync&lt;PageResultModel&lt;ProductModel&gt;&gt;($&quot;{(uri.EndsWith('\/') ? uri : uri + &quot;\/&quot;)}api\/v1\/products?take=100&skip;=0&quot;);     } } <\/code><\/pre>\n<\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/484596\/\"> https:\/\/habr.com\/ru\/post\/484596\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text-html\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/484596\/\">\u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u0422\u0430\u043a\u0438 \u0434\u0430, \u0432 \u043f\u0440\u043e\u0448\u043b\u043e\u0439 \u0441\u0432\u043e\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u043b \u0441\u0434\u0435\u043b\u0430\u0442\u044c Todo List \u043d\u0430 Blazor Wasm \u0438 \u043e\u0441\u0442\u0430\u043b\u0441\u044f \u0434\u043e\u0432\u043e\u043b\u0435\u043d. \u0422\u0435\u043f\u0435\u0440\u044c \u044f \u0440\u0435\u0448\u0438\u043b \u0432\u0437\u044f\u0442\u044c\u0441\u044f \u0437\u0430 \u0447\u0442\u043e-\u0442\u043e \u043f\u043e \u0441\u0435\u0440\u044c\u0435\u0437\u043d\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u0432 \u0434\u0435\u043b\u0435. \u0411\u0443\u0434\u0443 \u0434\u0435\u043b\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0442\u0435\u043d\u044c\u043a\u0438\u0439 SPA UI \u043d\u0430 Blazor \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e \u0432\u044b\u043c\u044b\u0448\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u043c\u0430\u0433\u0430\u0437\u0438\u043d\u0430. \u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e \u043f\u0440\u0438\u0431\u043b\u0438\u0436\u0435\u043d\u043d\u044b\u0439 \u043a \u0431\u043e\u0435\u0432\u043e\u043c\u0443 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044e \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u041d\u0430\u0447\u043d\u0443 \u044f \u0441 \u0442\u043e\u0433\u043e \u0447\u0442\u043e \u0437\u0430\u043f\u0438\u043b\u044e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0438\u0445 \u043f\u043e \u0440\u043e\u043b\u044f\u043c \u0442. \u0435. \u0447\u0442\u043e\u0431\u044b \u0430\u0434\u043c\u0438\u043d \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u0438\u0434\u0435\u043b\u0438 \u043d\u0435\u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441. \u0415\u0449\u0435 \u044f \u044d\u0442\u043e \u0432\u0441\u0435 \u0432 docker \u043e\u0431\u0440\u0430\u0437\u044b \u0441\u043e\u0431\u0440\u0430\u043b \u0438 \u043d\u0430 docker registry \u0432\u044b\u043b\u043e\u0436\u0438\u043b. \u0417\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u0434\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u043a\u0430\u0442. <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-297360","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/297360","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=297360"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/297360\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=297360"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=297360"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=297360"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}