Blazor OIDC 单点登录授权实例7 – Blazor hybird app 端授权

目录:

  1. OpenID 与 OAuth2 基础知识
  2. Blazor wasm Google 登录
  3. Blazor wasm Gitee 码云登录
  4. Blazor OIDC 单点登录授权实例1-建立和配置IDS身份验证服务
  5. Blazor OIDC 单点登录授权实例2-登录信息组件wasm
  6. Blazor OIDC 单点登录授权实例3-服务端管理组件
  7. Blazor OIDC 单点登录授权实例4 - 部署服务端/独立WASM端授权
  8. Blazor OIDC 单点登录授权实例5 - 独立SSR App (net8 webapp)端授权
  9. Blazor OIDC 单点登录授权实例6 - Winform 端授权
  10. Blazor OIDC 单点登录授权实例7 - Blazor hybird app 端授权

(目录暂时不更新,跟随合集标题往下走)

源码

BlazorOIDC.WinForms

建立 BlazorOIDC.WinForms 工程

自行安装 Vijay Anand E G 模板,快速建立 Blazor WinForms 工程, 命名为 BlazorOIDC.WinForms

Blazor OIDC 单点登录授权实例7 - Blazor hybird app 端授权

引用以下库

    <ItemGroup>         <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.4" />         <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="8.*" />         <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />         <FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference>         <PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" />     </ItemGroup> 

_Imports.razor 加入引用

@using Microsoft.AspNetCore.Components.Authorization 

Main.razor 加入授权

完整代码

<CascadingAuthenticationState>     <Router AppAssembly="@GetType().Assembly">         <Found Context="routeData">             <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />         </Found>         <NotFound>             <LayoutView Layout="@typeof(MainLayout)">                 <p>Sorry, there's nothing at this address.</p>             </LayoutView>         </NotFound>     </Router> </CascadingAuthenticationState> 

添加Oidc授权配置

新建文件 ExternalAuthStateProvider.cs

完整代码

using IdentityModel.OidcClient; using Microsoft.AspNetCore.Components.Authorization; using System.Security.Claims;  namespace BlazorOIDC.WinForms;  public class ExternalAuthStateProvider : AuthenticationStateProvider {     private readonly Task<AuthenticationState> authenticationState;      public ExternalAuthStateProvider(AuthenticatedUser user) =>         authenticationState = Task.FromResult(new AuthenticationState(user.Principal));      private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());      public override Task<AuthenticationState> GetAuthenticationStateAsync() =>         Task.FromResult(new AuthenticationState(currentUser));      public Task<AuthenticationState> LogInAsync()     {         var loginTask = LogInAsyncCore();         NotifyAuthenticationStateChanged(loginTask);          return loginTask;          async Task<AuthenticationState> LogInAsyncCore()         {             var user = await LoginWithExternalProviderAsync();             currentUser = user;              return new AuthenticationState(currentUser);         }     }      private async Task<ClaimsPrincipal> LoginWithExternalProviderAsync()     {         /*             提供 Open ID/MSAL 代码以对用户进行身份验证。查看您的身份             提供商的文档以获取详细信息。              根据新的声明身份返回新的声明主体。         */          string authority = "https://localhost:5001/";         //string authority = "https://ids2.app1.es/"; //真实环境         string api = $"{authority}WeatherForecast";         string clientId = "Blazor5002";          OidcClient? _oidcClient;         HttpClient _apiClient = new HttpClient { BaseAddress = new Uri(api) };          var browser = new SystemBrowser(5002);         var redirectUri = string.Format($"http://localhost:{browser.Port}/authentication/login-callback");         var redirectLogoutUri = string.Format($"http://localhost:{browser.Port}/authentication/logout-callback");          var options = new OidcClientOptions         {             Authority = authority,             ClientId = clientId,             RedirectUri = redirectUri,             PostLogoutRedirectUri = redirectLogoutUri,             Scope = "BlazorWasmIdentity.ServerAPI openid profile",             //Scope = "Blazor7.ServerAPI openid profile",             Browser = browser,             Policy = new Policy { RequireIdentityTokenSignature = false }          };          _oidcClient = new OidcClient(options);         var result = await _oidcClient.LoginAsync(new LoginRequest());         ShowResult(result);          var authenticatedUser = result.User;          return authenticatedUser;     }      private static void ShowResult(LoginResult result, bool showToken = false)     {         if (result.IsError)         {             Console.WriteLine("nnError:n{0}", result.Error);             return;         }          Console.WriteLine("nnClaims:");         foreach (var claim in result.User.Claims)         {             Console.WriteLine("{0}: {1}", claim.Type, claim.Value);         }          if (showToken)         {             Console.WriteLine($"nidentity token: {result.IdentityToken}");             Console.WriteLine($"access token:   {result.AccessToken}");             Console.WriteLine($"refresh token:  {result?.RefreshToken ?? "none"}");         }     }      public Task Logout()     {         currentUser = new ClaimsPrincipal(new ClaimsIdentity());         NotifyAuthenticationStateChanged(             Task.FromResult(new AuthenticationState(currentUser)));         return Task.CompletedTask;     } }  public class AuthenticatedUser {     public ClaimsPrincipal Principal { get; set; } = new(); }  

添加Oidc浏览器授权方法

新建文件 SystemBrowser.cs

完整代码

using IdentityModel.OidcClient.Browser; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; #nullable disable  namespace BlazorOIDC.WinForms;  public class SystemBrowser : IBrowser {     public int Port { get; }     private readonly string _path;      public SystemBrowser(int? port = null, string path = null)     {         _path = path;          if (!port.HasValue)         {             Port = GetRandomUnusedPort();         }         else         {             Port = port.Value;         }     }      private int GetRandomUnusedPort()     {         var listener = new TcpListener(IPAddress.Loopback, 0);         listener.Start();         var port = ((IPEndPoint)listener.LocalEndpoint).Port;         listener.Stop();         return port;     }      public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)     {         using (var listener = new LoopbackHttpListener(Port, _path))         {             OpenBrowser(options.StartUrl);              try             {                 var result = await listener.WaitForCallbackAsync();                 if (string.IsNullOrWhiteSpace(result))                 {                     return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = "Empty response." };                 }                  return new BrowserResult { Response = result, ResultType = BrowserResultType.Success };             }             catch (TaskCanceledException ex)             {                 return new BrowserResult { ResultType = BrowserResultType.Timeout, Error = ex.Message };             }             catch (Exception ex)             {                 return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = ex.Message };             }         }     }      public static void OpenBrowser(string url)     {         try         {             Process.Start(url);         }         catch         {             // hack because of this: https://github.com/dotnet/corefx/issues/10361             if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))             {                 url = url.Replace("&", "^&");                 Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });             }             else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))             {                 Process.Start("xdg-open", url);             }             else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))             {                 Process.Start("open", url);             }             else             {                 throw;             }         }     } }  public class LoopbackHttpListener : IDisposable {     const int DefaultTimeout = 60 * 5; // 5 mins (in seconds)      IWebHost _host;     TaskCompletionSource<string> _source = new TaskCompletionSource<string>();      public string Url { get; }      public LoopbackHttpListener(int port, string path = null)     {         path = path ?? string.Empty;         if (path.StartsWith("/")) path = path.Substring(1);          Url = $"http://localhost:{port}/{path}";          _host = new WebHostBuilder()             .UseKestrel()             .UseUrls(Url)             .Configure(Configure)             .Build();         _host.Start();     }      public void Dispose()     {         Task.Run(async () =>         {             await Task.Delay(500);             _host.Dispose();         });     }      void Configure(IApplicationBuilder app)     {         app.Run(async ctx =>         {             if (ctx.Request.Method == "GET")             {                 await SetResultAsync(ctx.Request.QueryString.Value, ctx);             }             else if (ctx.Request.Method == "POST")             {                 if (!ctx.Request.ContentType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))                 {                     ctx.Response.StatusCode = 415;                 }                 else                 {                     using (var sr = new StreamReader(ctx.Request.Body, Encoding.UTF8))                     {                         var body = await sr.ReadToEndAsync();                         await SetResultAsync(body, ctx);                     }                 }             }             else             {                 ctx.Response.StatusCode = 405;             }         });     }      private async Task SetResultAsync(string value, HttpContext ctx)     {         try         {             ctx.Response.StatusCode = 200;             ctx.Response.ContentType = "text/html; charset=utf-8";             await ctx.Response.WriteAsync("<h1>您现在可以返回应用程序.</h1>");             await ctx.Response.Body.FlushAsync();              _source.TrySetResult(value);         }         catch(Exception ex)         {             Console.WriteLine(ex.ToString());              ctx.Response.StatusCode = 400;             ctx.Response.ContentType = "text/html; charset=utf-8";             await ctx.Response.WriteAsync("<h1>无效的请求.</h1>");             await ctx.Response.Body.FlushAsync();         }     }      public Task<string> WaitForCallbackAsync(int timeoutInSeconds = DefaultTimeout)     {         Task.Run(async () =>         {             await Task.Delay(timeoutInSeconds * 1000);             _source.TrySetCanceled();         });          return _source.Task;     } }  

Shared 文件夹新建登录/注销页面组件

LoginComponent.razor

完整代码

@inject AuthenticationStateProvider AuthenticationStateProvider @page "/Login" @using System.Security.Claims  <button @onclick="Login">Log in</button>  <p>@Msg</p>    <AuthorizeView>     <Authorized>          你好, @context.User.Identity?.Name           <br /><br /><br />         <h5>以下是用户的声明</h5><br />          @foreach (var claim in context.User.Claims)         {             <p>@claim.Type: @claim.Value</p>         }         </Authorized>   </AuthorizeView>   <p>以下是基于角色或基于策略的授权,未登录不显示 </p>  <AuthorizeView Roles="Admin, Superuser">     <p>只有管理员或超级用户才能看到.</p> </AuthorizeView>  @code {     [Inject]     private AuthenticatedUser? authenticatedUser { get; set; }      /// <summary>     /// 级联参数获取身份验证状态数据     /// </summary>     [CascadingParameter]     private Task<AuthenticationState>? authenticationStateTask { get; set; }      private string? Msg { get; set; }      private ClaimsPrincipal? User { get; set; }      public async Task Login()     {         var authenticationState = await ((ExternalAuthStateProvider)AuthenticationStateProvider).LogInAsync();          User = authenticationState?.User;          if (User != null)         {             if (User.Identity != null && User.Identity.IsAuthenticated)             {                 Msg += "已登录." + Environment.NewLine;             }         }     } } 

LogoutComponent.razor

完整代码

@inject AuthenticationStateProvider AuthenticationStateProvider @page "/Logout"  <button @onclick="Logout">Log out</button>  @code {     public async Task Logout()     {         await ((ExternalAuthStateProvider)AuthenticationStateProvider).Logout();     } } 
		<div class="nav-item px-3">             <NavLink class="nav-link" href="Login">                 <span class="oi oi-plus" aria-hidden="true"></span> Login             </NavLink> 		</div> 		<div class="nav-item px-3">             <NavLink class="nav-link" href="Logout">                 <span class="oi oi-plus" aria-hidden="true"></span> Logout             </NavLink> 		</div>  

Form1.cs 修改首页

         var blazor = new BlazorWebView()         {             Dock = DockStyle.Fill,             HostPage = "wwwroot/index.html",             Services = Startup.Services!,             StartPath = "/Login"         };         blazor.RootComponents.Add<Main>("#app");         Controls.Add(blazor); 

Startup.cs 注册服务

完整代码

using BlazorOIDC.WinForms.Data; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting;  namespace BlazorOIDC.WinForms; public static class Startup {     public static IServiceProvider? Services { get; private set; }      public static void Init()     {         var host = Host.CreateDefaultBuilder()                        .ConfigureServices(WireupServices)                        .Build();         Services = host.Services;     }      private static void WireupServices(IServiceCollection services)     {         services.AddWindowsFormsBlazorWebView();         services.AddSingleton<WeatherForecastService>();          services.AddAuthorizationCore();         services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();         services.AddSingleton<AuthenticatedUser>();       #if DEBUG         services.AddBlazorWebViewDeveloperTools(); #endif     } } 

运行

Blazor OIDC 单点登录授权实例7 - Blazor hybird app 端授权

发表评论

评论已关闭。

相关文章