Blazor IDC 单点登录授权实例4 - 部署服务端/独立WASM端授权
目录:
- 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.Server 项目
部署
部署步骤跟平常blazor一样, 这里就不复述了, demo 部署后的域名是 ids2.app.es
新建独立WASM工程
改为以前文章配置的测试点 "applicationUrl": "https://localhost:5002;http://localhost:5003",
Program.cs 文件
using BlazorWasmOIDC;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
var authority = "https://localhost:5001/";
var clientId = "Blazor5002";
var url2 = builder.HostEnvironment.BaseAddress;
//完整的配置
builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = authority;
options.ProviderOptions.ClientId = clientId;
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.RedirectUri = $"{url2}authentication/login-callback";
options.ProviderOptions.PostLogoutRedirectUri = $"{url2}authentication/logout-callback";
options.ProviderOptions.DefaultScopes.Add("BlazorWasmIdentity.ServerAPI");
options.UserOptions.RoleClaim = "role";
});
await builder.Build().RunAsync();
Index.razor 文件
@page "/"
@using System.Diagnostics.CodeAnalysis
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<PageTitle>Index</PageTitle>
<AuthorizeView>
<Authorized>
你好, @context.User.Identity?.Name
</Authorized>
<NotAuthorized>
<span>看起来你还没登录</span>
</NotAuthorized>
</AuthorizeView>
测试
-
先独立启动ssr工程 Blazor SSR/WASM IDS/OIDC 单点登录授权实例1-建立和配置IDS身份验证服务, 监听地址为 https://localhost:5001/
-
独立启动 BlazorWasmOIDC 工程, 监听地址为 https://localhost:5002/
-
登录测试
[附带]控制台授权测试
添加工程 ConsoleOIDC
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" />
</ItemGroup>
</Project>
代码
using IdentityModel.Client;
using IdentityModel.OidcClient;
using Newtonsoft.Json.Linq;
using Serilog;
namespace ConsoleOIDC;
public class Program
{
static string authority = "https://localhost:5001/";
//static string authority = "https://ids2.app1.es/"; //真实环境
static string api = $"{authority}WeatherForecast";
static string clientId = "Blazor5002";
static OidcClient? _oidcClient;
static HttpClient _apiClient = new HttpClient { BaseAddress = new Uri(api) };
public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult();
public static async Task MainAsync()
{
Console.WriteLine("+-----------------------+");
Console.WriteLine("| Sign in with OIDC |");
Console.WriteLine("+-----------------------+");
Console.WriteLine("");
await Login();
}
private static async Task Login()
{
// 使用环回地址上的可用端口创建重定向 URI。
// 要求 OP 允许 127.0.0.1 上的随机端口 - 否则设置静态端口
var browser = new SystemBrowser(5002);
var redirectUri = $"http://localhost:{browser.Port}/authentication/login-callback";
var redirectLogoutUri = $"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 }
};
var serilog = new LoggerConfiguration()
.MinimumLevel.Error()
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}")
.CreateLogger();
options.LoggerFactory.AddSerilog(serilog);
_oidcClient = new OidcClient(options);
var result = await _oidcClient.LoginAsync(new LoginRequest());
ShowResult(result);
await CallApi(result.AccessToken);
await NextSteps(result);
}
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"}");
}
}
private static async Task NextSteps(LoginResult result)
{
var currentAccessToken = result.AccessToken;
var currentRefreshToken = result.RefreshToken;
var menu = " x...exit c...call api ";
if (currentRefreshToken != null) menu += "r...refresh token ";
while (true)
{
Console.WriteLine("\n\n");
Console.Write(menu);
var key = Console.ReadKey();
if (key.Key == ConsoleKey.X) return;
if (key.Key == ConsoleKey.C) await CallApi(currentAccessToken);
if (key.Key == ConsoleKey.R)
{
var refreshResult = await _oidcClient.RefreshTokenAsync(currentRefreshToken);
if (refreshResult.IsError)
{
Console.WriteLine($"Error: {refreshResult.Error}");
}
else
{
currentRefreshToken = refreshResult.RefreshToken;
currentAccessToken = refreshResult.AccessToken;
Console.WriteLine("\n\n");
Console.WriteLine($"access token: {refreshResult.AccessToken}");
Console.WriteLine($"refresh token: {refreshResult?.RefreshToken ?? "none"}");
}
}
}
}
private static async Task CallApi(string currentAccessToken)
{
try
{
_apiClient.SetBearerToken(currentAccessToken);
var response = await _apiClient.GetAsync("");
if (response.IsSuccessStatusCode)
{
var str = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {str}");
var json = JArray.Parse(await response.Content.ReadAsStringAsync());
Console.WriteLine("\n\n");
Console.WriteLine(json);
}
else
{
Console.WriteLine($"Error: {response.ReasonPhrase}");
}
}
catch (Exception e)
{
Console.WriteLine($"Error: {e.Message}");
}
}
}
添加 SystemBrowser.cs
using IdentityModel.OidcClient.Browser;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleOIDC;
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;
}
}
关联项目
FreeSql QQ群:4336577
BA & Blazor QQ群:795206915
Maui Blazor 中文社区 QQ群:645660665
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名AlexChow(包含链接: https://github.com/densen2014 ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系 。
转载声明
本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接:https://www.cnblogs.com/densen2014/p/17969485