目錄:
- OpenID 與 OAuth2 基礎知識
- Blazor wasm Google 登入
- Blazor wasm Gitee 碼雲登入
- Blazor OIDC 單點登入授權例項1-建立和配置IDS身份驗證服務
- Blazor OIDC 單點登入授權例項2-登入資訊元件wasm
- Blazor OIDC 單點登入授權例項3-服務端管理元件
- Blazor OIDC 單點登入授權例項4 - 部署服務端/獨立WASM端授權
- Blazor OIDC 單點登入授權例項5 - 獨立SSR App (net8 webapp)端授權
- Blazor OIDC 單點登入授權例項6 - Winform 端授權
- Blazor OIDC 單點登入授權例項7 - Blazor hybird app 端授權
(目錄暫時不更新,跟隨合集標題往下走)
原始碼
BlazorOIDC.WinForms
建立 BlazorOIDC.WinForms 工程
自行安裝 Vijay Anand E G 模板,快速建立 Blazor WinForms 工程, 命名為 BlazorOIDC.WinForms
引用以下庫
<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("\n\nError:\n{0}", result.Error);
return;
}
Console.WriteLine("\n\nClaims:");
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();
}
}
NavMenu.razor 加入選單
<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
}
}