Net 8 Blazor Web App项目访问Identity Server 4

Net 8 Blazor Web App项目访问Identity Server 4

Identity Server系列目录

  1. Blazor Server访问Identity Server 4单点登录 - SunnyTrudeau - 博客园 (cnblogs.com)
  2. Blazor Server访问Identity Server 4单点登录2-集成Asp.Net角色 - SunnyTrudeau - 博客园 (cnblogs.com)
  3. Blazor Server访问Identity Server 4-手机验证码登录 - SunnyTrudeau - 博客园 (cnblogs.com)
  4. Blazor MAUI客户端访问Identity Server登录 - SunnyTrudeau - 博客园 (cnblogs.com)
  5. Identity Server 4项目集成Blazor组件 - SunnyTrudeau - 博客园 (cnblogs.com)
  6. Identity Server 4退出登录自动跳转返回 - SunnyTrudeau - 博客园 (cnblogs.com)
  7. Identity Server通过ProfileService返回用户角色 - SunnyTrudeau - 博客园 (cnblogs.com)
  8. Identity Server 4返回自定义用户Claim - SunnyTrudeau - 博客园 (cnblogs.com)
  9. Blazor Server获取Token访问外部Web Api - SunnyTrudeau - 博客园 (cnblogs.com)
  10. Blazor Server通过RefreshToken更新AccessToken - SunnyTrudeau - 博客园 (cnblogs.com)
  11. Blazor WebAssembly项目访问Identity Server 4 - SunnyTrudeau - 博客园 (cnblogs.com)

 

.Net 8发布了新的Blazor WebApp项目模板,支持服务端和客户端混合呈现。初步使用了一下,感觉是把Blazor ServerBlazor WebAssembly合二为一了。使用Blazor WebApp项目模板有一个好处,可以先用服务端razor页面调用service的方法快速实现功能,验证需求,如果后期在线客户端数量多给服务端造成压力过大,可以再把部分razor页面改为在Web Assembly客户端运行,通过HttpClient访问服务端Web Api

Blazor ServerBlazor WebAssembly项目访问Identity Server 4的方法是不一样的。为了研究Blazor WebApp项目的认证方法,新建了一个带有身份验证的Blazor WebApp项目,选择Auto混合呈现模式。该项目的服务端内置了一套认证方案代码,非常值得学习,有些代码和文件可以复制后改一下用于oidc方案。经过研究和测试,确定Blazor WebApp的方法跟Blazor Server是一致的。

下文基于已有的Identity Server4解决方案https://gitee.com/woodsun/blzid4增加项目,修改代码。

 

Identity Server 4服务端增加项目配置

AspNetId4Web项目增加Blazor WebApp项目的客户端配置,同时对比一下Blazor ServerBlazor WebAssembly项目的配置。

D:\Software\gitee\blzid4\BlzId4Web\AspNetId4Web\Config.cs

                // Blazor WebAssembly客户端
                new Client
                {
                    ClientId = "WebAssemblyOidc",
                    ClientName = "WebAssemblyOidc",
                    RequireClientSecret = false,

                    AllowedGrantTypes = GrantTypes.Code,

                    AllowedScopes ={ "openid", "profile", "scope1", },
                    
                    //网页客户端运行时的URL
                    AllowedCorsOrigins = { "https://localhost:5801", },

                    //登录成功之后将要跳转的网页客户端的URL
                    RedirectUris = { "https://localhost:5801/authentication/login-callback", },

                    //退出登录之后将要跳转的网页客户端的URL
                    PostLogoutRedirectUris = { "https://localhost:5801", },
                },

                // Blazor Server客户端
                new Client()
                {
                    ClientId = "BlazorServerOidc",
                    ClientName = "BlazorServerOidc",
                    ClientSecrets = new []{ new Secret("BlazorServerOidc.Secret".Sha256()) },

                    AllowedGrantTypes = GrantTypes.Code,

                    AllowedCorsOrigins = { "https://localhost:5501" },
                    RedirectUris = { "https://localhost:5501/signin-oidc" },
                    PostLogoutRedirectUris = { "https://localhost:5501/signout-callback-oidc" },

                    //效果等同客户端项目配置options.GetClaimsFromUserInfoEndpoint = true
                    //AlwaysIncludeUserClaimsInIdToken = true,

                    //AllowedScopes = { "openid", "profile", "scope1", "role", }
                    //通过ProfileService返回用户角色
                    AllowedScopes = { "openid", "profile", "scope1", },

                    //如果要获取refresh_tokens ,必须把AllowOfflineAccess设置为true
                    AllowOfflineAccess = true,

                    //AccessToken有效期,默认1小时,改为1分钟做试验
                    AccessTokenLifetime = 60,
                },

                // Blazor WebApp客户端
                new Client()
                {
                    ClientId = "BlazorWebAppOidc",
                    ClientName = "BlazorWebAppOidc",
                    ClientSecrets = new []{ new Secret("BlazorWebAppOidc.Secret".Sha256()) },

                    AllowedGrantTypes = GrantTypes.Code,

                    AllowedCorsOrigins = { "https://localhost:5581" },
                    RedirectUris = { "https://localhost:5581/signin-oidc" },
                    PostLogoutRedirectUris = { "https://localhost:5581/signout-callback-oidc" },

                    //效果等同客户端项目配置options.GetClaimsFromUserInfoEndpoint = true
                    //AlwaysIncludeUserClaimsInIdToken = true,

                    //AllowedScopes = { "openid", "profile", "scope1", "role", }
                    //通过ProfileService返回用户角色
                    AllowedScopes = { "openid", "profile", "scope1", },

                    //如果要获取refresh_tokens ,必须把AllowOfflineAccess设置为true
                    AllowOfflineAccess = true,

                    //AccessToken有效期,默认1小时,改为1分钟做试验
                    AccessTokenLifetime = 60,
                },

 

创建Blazor WebApp项目

在解决方案添加Blazor WebApp项目WebAppOidc,身份验证类型=无,呈现模式=Auto(Server and WebAssemby)interactive location=per page/component(注意不要选global,否则很多文件位置会有变化)。

 

修改WebAppOidc.Client客户端项目

WebAppOidc.Client客户端项目NuGet安装id4认证相关的库。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\WebAppOidc.Client.csproj

<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.0" />

 

增加用户信息类

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\UserInfo.cs

namespace WebAppOidc.Client;

// Add properties to this class and update the server and client AuthenticationStateProviders
// to expose more information about the authenticated user to the client.
public class UserInfo
{
    public required string UserId { get; set; }
    public required string Email { get; set; }
    public required string Name { get; set; }
    public required string PhoneNumber { get; set; }
    public required string Nation { get; set; }
    public required string Roles { get; set; }
}

增加认证状态管理者PersistentAuthenticationStateProvider,这个类是从带有身份验证的Blazor WebApp客户端项目同名文件改过来的。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\PersistentAuthenticationStateProvider.cs

 

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;

namespace WebAppOidc.Client;

// This is a client-side AuthenticationStateProvider that determines the user's authentication state by
// looking for data persisted in the page when it was rendered on the server. This authentication state will
// be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full
// page reload is required.
//
// This only provides a user name and email for display purposes. It does not actually include any tokens
// that authenticate to the server when making subsequent requests. That works separately using a
// cookie that will be included on HttpClient requests to the server.
internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider
{
    private static readonly Task<AuthenticationState> defaultUnauthenticatedTask =
        Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));

    private readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask;

    public PersistentAuthenticationStateProvider(PersistentComponentState state)
    {
        if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
        {
            return;
        }

        Claim[] claims = [
            new Claim(ClaimTypes.NameIdentifier, userInfo.UserId),
            //new Claim(ClaimTypes.Name, userInfo.Email),
            new Claim(ClaimTypes.Email, userInfo.Email),
            new Claim(ClaimTypes.Name, userInfo.Name),
            new Claim(ClaimTypes.MobilePhone, userInfo.PhoneNumber),
            new Claim("nation", userInfo.Nation),
            new Claim(ClaimTypes.Role, userInfo.Roles) ];

        authenticationStateTask = Task.FromResult(
            new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims,
                authenticationType: nameof(PersistentAuthenticationStateProvider)))));
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync() => authenticationStateTask;
}

 

增加登录跳转razor组件,从带有身份验证的Balzor WebApp客户端项目复制过来。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\RedirectToLogin.razor

@inject NavigationManager NavigationManager

@code {
    protected override void OnInitialized()
    {
        NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true);
    }
}

Program添加oidc认证的代码,从带有身份验证的Balzor WebApp客户端项目复制过来。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc.Client\Program.cs

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

namespace WebAppOidc.Client;

internal class Program
{
    static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);

        //参考带有身份验证的Blazor WebApp客户端项目
        builder.Services.AddAuthorizationCore();
        builder.Services.AddCascadingAuthenticationState();
        builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();

        await builder.Build().RunAsync();
    }
}

WebAppOidc服务端项目

WebAppOidc服务端项目NuGet安装id4认证相关的库。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\WebAppOidc.csproj

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />

<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.0" />

<PackageReference Include="IdentityModel" Version="5.2.0" />

 

添加oidc认证的代码,有些代码可以从之前的Blazor Server项目复制过来,有的从带有身份验证的Balzor WebApp服务端项目复制过来。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Program.cs

 

using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Authorization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using WebAppOidc.Client.Pages;
using WebAppOidc.Components;

namespace WebAppOidc;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddRazorComponents()
            .AddInteractiveServerComponents()
            .AddInteractiveWebAssemblyComponents();

        //参考带有身份验证的Blazor WebApp服务端项目
        builder.Services.AddCascadingAuthenticationState();
        builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAuthenticationStateProvider>();
        builder.Services.AddAuthorization();

        //参考Blazor Server项目
        //添加认证相关的服务
        ConfigureAuthServices(builder.Services);

        builder.Services.AddControllers();

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseWebAssemblyDebugging();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();

        app.UseStaticFiles();
        app.UseAntiforgery();

        //参考Blazor Server项目
        //添加认证与授权中间件
        app.UseAuthentication();
        app.UseAuthorization();

        app.MapRazorComponents<App>()
            .AddInteractiveServerRenderMode()
            .AddInteractiveWebAssemblyRenderMode()
            .AddAdditionalAssemblies(typeof(Client._Imports).Assembly);

        app.MapControllers();

        app.Run();
    }

    //添加认证相关的服务
    private static void ConfigureAuthServices(IServiceCollection services)
    {
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        //清除微软定义的clamis
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

        //默认采用cookie认证方案,添加oidc认证方案
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "cookies";
            options.DefaultChallengeScheme = "oidc";
        })
        //配置cookie认证
        .AddCookie("cookies", options =>
        {
            // Cookie settings
            options.Cookie.HttpOnly = true;
            //options.ExpireTimeSpan = TimeSpan.FromMinutes(2);

            //OptionsValidationException: Cookie.Expiration is ignored, use ExpireTimeSpan instead.
            //options.Cookie.Expiration = TimeSpan.FromMinutes(3);

            //options.LoginPath = "/Account/Login";
            //options.AccessDeniedPath = "/Account/AccessDenied";
            //options.SlidingExpiration = true;
        })
        .AddOpenIdConnect("oidc", options =>
        {
            //id4服务的地址
            options.Authority = "https://localhost:5001";

            //id4配置的ClientId以及ClientSecrets
            options.ClientId = "BlazorWebAppOidc";
            options.ClientSecret = "BlazorWebAppOidc.Secret";

            //认证模式
            options.ResponseType = "code";

            //保存token到本地
            options.SaveTokens = true;

            //很重要,指定从Identity Server的UserInfo地址来取Claim
            //效果等同id4配置AlwaysIncludeUserClaimsInIdToken = true
            options.GetClaimsFromUserInfoEndpoint = true;

            //指定要取哪些资料(除Profile之外,Profile是默认包含的)
            options.Scope.Add("scope1");
            //获取RefreshToken
            options.Scope.Add("offline_access");
            //通过ProfileService返回用户角色
            //options.Scope.Add("role");

            //映射自定义用户声明
            options.ClaimActions.MapJsonKey(JwtClaimTypes.PhoneNumber, JwtClaimTypes.PhoneNumber);
            options.ClaimActions.MapJsonKey("nation", "nation");

            //这里是个ClaimType的转换,Identity Server的ClaimType和Blazor中间件使用的名称有区别,需要统一。
            //User.Identity.Name=JwtClaimTypes.Name
            options.TokenValidationParameters.NameClaimType = "name";
            options.TokenValidationParameters.RoleClaimType = "role";

            options.Events.OnUserInformationReceived = (context) =>
            {
                //id4返回的角色是字符串数组或者字符串,blazor server的角色是字符串,需要转换,不然无法获取到角色
                ClaimsIdentity claimsId = context.Principal.Identity as ClaimsIdentity;

                var roleElement = context.User.RootElement.GetProperty(JwtClaimTypes.Role);
                if (roleElement.ValueKind == System.Text.Json.JsonValueKind.Array)
                {
                    var roles = roleElement.EnumerateArray().Select(e => e.ToString());
                    claimsId.AddClaims(roles.Select(r => new Claim(JwtClaimTypes.Role, r)));
                }
                else
                {
                    claimsId.AddClaim(new Claim(JwtClaimTypes.Role, roleElement.ToString()));
                }

                return Task.CompletedTask;
            };

            //设置登录状态有效期,模拟token失效后需要频繁登录
            //options.MaxAge = TimeSpan.FromMinutes(3);
        });

    }
}

 

增加认证状态管理者PersistingRevalidatingAuthenticationStateProvider,这个类是从带有身份验证的Blazor WebApp服务端项目同名文件改过来的。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\PersistingServerAuthenticationStateProvider.cs

using WebAppOidc.Client;
using IdentityModel;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Diagnostics;

namespace WebAppOidc;

// This is a server-side AuthenticationStateProvider that uses PersistentComponentState to flow the
// authentication state to the client which is then fixed for the lifetime of the WebAssembly application.
internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider, IDisposable
{
    private readonly PersistentComponentState state;
    private readonly IdentityOptions options;

    private readonly PersistingComponentStateSubscription subscription;

    private Task<AuthenticationState>? authenticationStateTask;

    public PersistingRevalidatingAuthenticationStateProvider(
            ILoggerFactory loggerFactory,
            PersistentComponentState persistentComponentState,
            IOptions<IdentityOptions> optionsAccessor)
            : base(loggerFactory)
    {
        state = persistentComponentState;
        options = optionsAccessor.Value;

        AuthenticationStateChanged += OnAuthenticationStateChanged;
        subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
    }

    protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);

    protected override async Task<bool> ValidateAuthenticationStateAsync(
            AuthenticationState authenticationState, CancellationToken cancellationToken)
    {
        //项目模板代码判断用户是否存在,SecurityStamp是否改变
        return (authenticationState.User.Identity?.IsAuthenticated == true);
    }

    private void OnAuthenticationStateChanged(Task<AuthenticationState> task)
    {
        authenticationStateTask = task;
    }

    private async Task OnPersistingAsync()
    {
        if (authenticationStateTask is null)
        {
            throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}().");
        }

        var authenticationState = await authenticationStateTask;
        var principal = authenticationState.User;

        if (principal.Identity?.IsAuthenticated == true)
        {
            //Asp.Net Core定义的ClaimType跟id4定义的不同,要改用id4的
            var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value;
            //var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value;
            var email = principal.FindFirst(JwtClaimTypes.Email)?.Value;
            var name = principal.FindFirst(JwtClaimTypes.Name)?.Value;
            var phoneNumber = principal.FindFirst(JwtClaimTypes.PhoneNumber)?.Value;
            var nation = principal.FindFirst("nation")?.Value;
            var roles = principal.FindAll(JwtClaimTypes.Role)?.Select(x => x.Value);

            if (userId != null && email != null)
            {
                state.PersistAsJson(nameof(UserInfo), new UserInfo
                {
                    UserId = userId,
                    Email = email,
                    Name = name,
                    PhoneNumber = phoneNumber,
                    Nation = nation,
                    Roles = string.Join(",", roles)
                }); ;
            }
        }
    }

    protected override void Dispose(bool disposing)
    {
        subscription.Dispose();
        AuthenticationStateChanged -= OnAuthenticationStateChanged;
        base.Dispose(disposing);
    }
}

修改Routes.razor路由组件,需要登录时自动跳转到登录路由。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Components\Routes.razor

 

<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
    <Found Context="routeData">
     <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
            <NotAuthorized>
                <RedirectToLogin />
            </NotAuthorized>
        </AuthorizeRouteView>
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
</Router>

 

Home页面增加显示当前登录用户信息。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Components\Pages\Home.razor

@page "/"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authorization

@inject IHttpContextAccessor httpContextAccessor

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<AuthorizeView>
    <Authorized>

        <p>您已经登录</p>

        <div class="card">
            <div class="card-header">
                <h2>context.User.Claims</h2>
            </div>
            <div class="card-body">
                <dl>
                    <dt>context.User.Identity.Name</dt>
                    <dd>@context.User.Identity?.Name</dd>
                    @foreach (var claim in context.User.Claims)
                    {
                        <dt>@claim.Type</dt>
                        <dd>@claim.Value</dd>
                    }
                </dl>
            </div>
        </div>

        @if (AuthResult is not null)
        {
            <p>AuthResult.Principal.Identity.Name: <strong>@AuthResult.Principal?.Identity?.Name</strong></p>

            <div class="card">
                <div class="card-header">
                    <h2>AuthenticateResult.Principal</h2>
                </div>
                <div class="card-body">
                    <dl>
                        @foreach (var claim in AuthResult.Principal?.Claims!)
                        {
                            <dt>@claim.Type</dt>
                            <dd>@claim.Value</dd>
                        }
                    </dl>
                </div>
            </div>

            <div class="card">
                <div class="card-header">
                    <h2>AuthenticateResult.Properties.Items</h2>
                </div>
                <div class="card-body">
                    <dl>
                        @foreach (var prop in AuthResult.Properties?.Items!)
                        {
                            <dt>@prop.Key</dt>
                            <dd>@prop.Value</dd>
                        }
                    </dl>
                </div>
            </div>
        }

        <form action="Account/Logout" method="post">
            <AntiforgeryToken />
            <button type="submit" class="btn btn-link">退出登录(Post方法调用1次Action,推荐)</button>
        </form>
        <a class="nav-link" href="Account/Logout">退出登录(Get方法会调用2次Action,不推荐)</a>
    </Authorized>

    <NotAuthorized>
        <p>您还没有登录,请先登录</p>
        <form action="Account/Login" method="post">
            <AntiforgeryToken />
            <button type="submit" class="btn btn-link">登录(Post方法调用1次Action,推荐)</button>
        </form>
        <a class="nav-link" href="Account/Login">登录(Get方法会调用2次Action,不推荐)</a>
    </NotAuthorized>

</AuthorizeView>

@code {
    private AuthenticateResult AuthResult;

    //依赖注入的参数为null
    [CascadingParameter]
    private HttpContext HttpContext { get; set; } = default!;

    //设置为InteractiveServer才触发
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine($"依赖注入的参数HttpContext={HttpContext}");

            AuthResult = await httpContextAccessor.HttpContext.AuthenticateAsync();

            StateHasChanged();
        }
    }

}

增加AccountController账号控制器,从之前的Blazor Server项目复制过来,给Action增加了GetPost方法属性,同时支持GetPost方式访问。

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\AccountController.cs

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebAppOidc.Controllers;

[ApiController]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
    private readonly ILogger _logger;

    public AccountController(ILogger<AccountController> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 跳转到Identity Server 4统一登录
    /// </summary>
    /// <param name="returnUrl">登录成功后,返回之前的网页路由</param>
    /// <returns></returns>
    [HttpGet]
    [HttpPost]
    public IActionResult Login(string returnUrl = "")
    {
        if (string.IsNullOrEmpty(returnUrl))
            returnUrl = "./";

        var properties = new AuthenticationProperties
        {
            //记住登录状态
            IsPersistent = true,

            RedirectUri = returnUrl
        };

        _logger.LogInformation($"id4跳转登录, returnUrl={returnUrl}");

        //跳转到Identity Server 4统一登录
        return Challenge(properties, "oidc");
    }

    /// <summary>
    /// 退出登录
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        var userName = HttpContext.User.Identity?.Name;

        _logger.LogInformation($"{userName}退出登录。");

        //删除登录状态cookies
        await HttpContext.SignOutAsync("cookies");

        var properties = new AuthenticationProperties
        {
            RedirectUri = "./"
        };

        //跳转到Identity Server 4统一退出登录
        return SignOut(properties, "oidc");
    }

}

修改https侦听端口号5581,跟id4的配置一致

D:\Software\gitee\blzid4\BlzId4Web\WebAppOidc\WebAppOidc\Properties\launchSettings.json

"https": {

  "commandName": "Project",

  "dotnetRunMessages": true,

  "launchBrowser": true,

  "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",

  "applicationUrl": "https://localhost:5581",

  "environmentVariables": {

    "ASPNETCORE_ENVIRONMENT": "Development"

  }

},

 

 

测试

同时运行AspNetId4Web项目、WebAppOidc项目。

WebAppOidc项目登录,可以跳转到Identity Server 4登录页面,输入种子用户alice的手机号13512345001,在id4项目查看验证码,再填入登录页面验证码,登录成功,返回WebAppOidc主页,可以看到登录用户的信息。可以点击退出登录。

 

问题

采用Get方法从razor页面通过超级链接跳转到控制器Action路由的时候,会调用2Action,我不知道为什么会这样?以前Blazor Server项目这样做是没问题的,只会调用1次。我看带有身份验证的Blazor WebApp项目是采用post方法执行控制器路由的,所以跟着改了,也算解决了问题,但是原因没想明白。

 另外,Blazor Web App页面的依赖注入跟Blazor Server行为不一样了,Blazor Web App页面可以客户端和服务端动态呈现,页面初始化的时候,依赖注入的对象是空,需要在页面初始化之后,比如OnAfterRenderAsync,在这里可以依赖注入对象。

 

DEMO代码地址:https://gitee.com/woodsun/blzid4

 

posted on 2024-02-15 21:57  SunnyTrudeau  阅读(608)  评论(2编辑  收藏  举报