MAUI Blazor学习7-实现登录跳转页面

发布时间 2023-05-23 20:31:14作者: SunnyTrudeau

MAUI Blazor学习7-实现登录跳转页面

 

MAUI Blazor系列目录

  1. MAUI Blazor学习1-移动客户端Shell布局 - SunnyTrudeau - 博客园 (cnblogs.com)
  2. MAUI Blazor学习2-创建移动客户端Razor页面 - SunnyTrudeau - 博客园 (cnblogs.com)
  3. MAUI Blazor学习3-绘制ECharts图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  4. MAUI Blazor学习4-绘制BootstrapBlazor.Chart图表 - SunnyTrudeau - 博客园 (cnblogs.com)
  5. MAUI Blazor学习5-BLE低功耗蓝牙 - SunnyTrudeau - 博客园 (cnblogs.com)
  6. MAUI Blazor学习6-扫描二维码 - SunnyTrudeau - 博客园 (cnblogs.com)

 

登录是APP的基本功能,采用Identity Server 4认证服务器,Blazor Server可以简单配置一下oidc参数,即可跳转到id4服务器登录。APP可以在登录页面填写用户名和密码,发送到id4认证服务器。MAUI Blazor可以用这种方式实现登录功能。

202111月,在MAUI Blazor还在预览版的时候,我写了一个DEMO,实现了访问Id4服务器登录功能。现在把APP的代码直接搬到正式版,也是没问题的。

Blazor MAUI客户端访问Identity Server登录 - SunnyTrudeau - 博客园 (cnblogs.com)

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

 

id4认证服务端支持手机号验证码登录方案

沿用2021DEMOid4服务器,把AspNetId4Web项目复制到本解决方案。

回顾一下方案。config自定义一个PhoneCodeGrantType认证类型,通过手机号和验证码,返回token

 

D:\Software\gitee\blzid4\BlzId4Web\AspNetId4Web\Config.cs
new Client()
                {
                    ClientId="PhoneCode",
                    ClientName = "PhoneCode",
                    ClientSecrets=new []{new Secret("PhoneCode.Secret".Sha256())},

                    AllowedGrantTypes = new string[]{ "PhoneCodeGrantType" },

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

                    AllowedScopes = { "openid", "profile", "scope1", "role", }
                },

 

自定义手机验证码认证处理器。

D:\Software\gitee\blzid4\BlzId4Web\AspNetId4Web\PhoneCodeGrantValidator.cs
    /// <summary>
    /// 自定义手机验证码认证处理器
    /// </summary>
    public class PhoneCodeGrantValidator : IExtensionGrantValidator
    {
        /// <summary>
        /// 认证方式
        /// </summary>
        public string GrantType => "PhoneCodeGrantType";

        private readonly IMemoryCache _memoryCache;
        private readonly ApplicationDbContext _context;
        private readonly ILogger _logger;

        public PhoneCodeGrantValidator(
            IMemoryCache memoryCache,
            ApplicationDbContext context,
            ILogger<PhoneCodeGrantValidator> logger)
        {
            _memoryCache = memoryCache;
            _context = context;
            _logger = logger;
        }

        /// <summary>
        /// 验证自定义授权请求
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async Task ValidateAsync(ExtensionGrantValidationContext context)
        {
            try
            {
                //获取登录参数
                string phoneNumber = context.Request.Raw["PhoneNumber"];
                string verificationCode = context.Request.Raw["VerificationCode"];

                //获取手机号对应的缓存验证码
                if (!_memoryCache.TryGetValue(phoneNumber, out string cacheVerificationCode))
                {
                    //如果获取不到缓存验证码,说明手机号不存在,或者验证码过期,但是发送验证码时已经验证过手机号是存在的,所以只能是验证码过期
                    context.Result = new GrantValidationResult()
                    {
                        IsError = true,
                        Error = "验证码过期",
                    };

                    return;
                }

                if (verificationCode != cacheVerificationCode)
                {
                    context.Result = new GrantValidationResult()
                    {
                        IsError = true,
                        Error = "验证码错误",
                    };

                    return;
                }

                //根据手机号获取用户信息
                var appUser = await GetUserByPhoneNumberAsync(phoneNumber);
                if (appUser == null)
                {
                    context.Result = new GrantValidationResult()
                    {
                        IsError = true,
                        Error = "手机号无效",
                    };

                    return;
                }

                //授权通过返回
                context.Result = new GrantValidationResult(appUser.Id.ToString(), "custom");
            }
            catch (Exception ex)
            {
                context.Result = new GrantValidationResult()
                {
                    IsError = true,
                    Error = ex.Message
                };
            }
        }

        //根据手机号获取用户信息
        private async Task<ApplicationUser> GetUserByPhoneNumberAsync(string phoneNumber)
        {
            var appUser = await _context.Users.AsNoTracking()
                .FirstOrDefaultAsync(x => x.PhoneNumber == phoneNumber);

            return appUser;
        }

    }

把自定义手机验证码认证处理器PhoneCodeGrantValidator注册到id4认证服务。

 

D:\Software\gitee\blzid4\BlzId4Web\AspNetId4Web\Startup.cs
var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;

                // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                options.EmitStaticAudienceClaim = true;
            })
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryClients(Config.Clients)
                .AddExtensionGrantValidator<PhoneCodeGrantValidator>()
                .AddInMemoryApiResources(Config.ApiResources)
                .AddAspNetIdentity<ApplicationUser>();

 

注意要修改一下ProfileService,如果是Code方式访问id4,可以获取到所需的claims,但是自定义的PhoneCodeGrantType方式访问id4,只能获取到nation。调试发现context.Subject也只有nation一个claimcontext.Subject.FindAll(JwtClaimTypes.Name)根本无法获取到用户名等所需用户属性。我试过注解掉我写的ProfileServiceid4会用内置的ProfileService,返回的claims多一点,也不满足需求。我不知道问题出在哪里,但是知道怎么解决这个问题,就是ProfileService直接返回所需的用户属性即可,不判断context.Subject

 

D:\Software\gitee\mauiblazorapp\AspNetId4Web\ProfileService.cs
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            using var scope = _serviceProvider.CreateScope();
            var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
            //按Name找不到
            //var user = await userMgr.FindByNameAsync(context.Subject.Identity.Name);
            //按Sub找得到
            string userId = context.Subject.FindFirstValue(JwtClaimTypes.Subject);
            var user = await userMgr.FindByIdAsync(userId);

            #region 非Code方式访问,context.Subject只有nation,无法获取其他claim
#if false
            var nameClaim = context.Subject.FindAll(JwtClaimTypes.Name);
            context.IssuedClaims.AddRange(nameClaim);

            var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
            context.IssuedClaims.AddRange(roleClaims);

            var emailClaims = context.Subject.FindAll(JwtClaimTypes.Email);
            context.IssuedClaims.AddRange(emailClaims);

            var phoneNumberClaims = context.Subject.FindAll(JwtClaimTypes.PhoneNumber);
            context.IssuedClaims.AddRange(phoneNumberClaims);
#endif
            #endregion

            //手机验证码方式访问,直接获取用户的claims
            var nameClaim = new Claim(JwtClaimTypes.Name, user.UserName);
            context.IssuedClaims.Add(nameClaim);

            var roles = await userMgr.GetRolesAsync(user);
            foreach (var role in roles)
            {
                var roleClaims = new Claim(JwtClaimTypes.Role, role);
                context.IssuedClaims.Add(roleClaims);
            }

            var emailClaims = new Claim(JwtClaimTypes.Email, user.Email);
            context.IssuedClaims.Add(emailClaims);

            var phoneNumberClaims = new Claim(JwtClaimTypes.PhoneNumber, user.PhoneNumber);
            context.IssuedClaims.Add(phoneNumberClaims);

            //获取民族字段
            var nationClaim = new Claim("nation", user.Nation);
            context.IssuedClaims.Add(nationClaim);

            await Task.CompletedTask;
        }

 

 

APP增加用户管理功能

基于本系列MaBlaApp项目,把2021DEMOMAUI Blazor客户端项目BlaMauiApp的代码复制过来使用。

NuGet安装IdentityModel

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

 

登录用户信息类LoginUserInfo不用改。

D:\Software\gitee\blzid4\BlzId4Web\BlaMauiApp\Data\LoginUserInfo.cs

 

/// <summary>
    /// 登录用户信息
    /// </summary>
    public class LoginUserInfo
    {
        /// <summary>
        /// 从Identity Server获取的token结果
        /// </summary>
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
        public DateTimeOffset ExpiresIn { get; set; } = DateTimeOffset.MinValue;

        /// <summary>
        /// 从Identity Server获取的用户信息
        /// </summary>
        public string UserId { get; set; }
        public string Username { get; set; }
        public string UserRole { get; set; }

        public override string ToString() => string.IsNullOrWhiteSpace(Username) ? "没有登录用户" : $"用户[{Username}], 有效期[{ExpiresIn}]";

    }

 

登录用户管理器LoginUserManager稍微优化一下,之前用文件保存登录用户信息,现在改用Preferences,更加方便。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Data\LoginUserManager.cs

/// <summary>
/// 登录用户管理器
/// </summary>
public class LoginUserManager
{
    /// <summary>
    /// 登录用户信息
    /// </summary>
    public LoginUserInfo UserInfo { get; private set; } = new LoginUserInfo();

    /// <summary>
    /// 登录用户信息json
    /// </summary>
    public string UserInfoJson
    {
        get => Preferences.Get(nameof(UserInfoJson), "");
        set => Preferences.Set(nameof(UserInfoJson), value);
    }

    public LoginUserManager()
    {
        if (!string.IsNullOrWhiteSpace(UserInfoJson))
        {
            //如果已经存在登录用户信息json,反序列化登录用户信息
            UserInfo = JsonConvert.DeserializeObject<LoginUserInfo>(UserInfoJson);

            if ((UserInfo is null) || (UserInfo.ExpiresIn < DateTimeOffset.Now))
            {
                //如果登录信息已经过期,清除登录用户信息
                UserInfo = new LoginUserInfo();

                //清除登录用户信息json
                UserInfoJson = "";
            }
        }
        else
        {
            //如果没有登录用户json,新建登录用户信息
            UserInfo = new LoginUserInfo();
        }

        Debug.WriteLine($"{DateTimeOffset.Now}, 初始化登录用户信息: {UserInfo}");
    }

    /// <summary>
    /// 用户是否已经登录?
    /// </summary>
    public bool IsAuthenticated => !string.IsNullOrWhiteSpace(UserInfo.Username);

    /// <summary>
    /// 登录,提取登录用户信息,并保存到APP配置
    /// </summary>
    public void Login(LoginUserInfo userInfo)
    {
        UserInfo = userInfo;

        UserInfoJson = JsonConvert.SerializeObject(UserInfo);

        Debug.WriteLine($"{DateTimeOffset.Now}, 用户登录: {UserInfo}");
    }

    /// <summary>
    /// 退出登录
    /// </summary>
    public void Logout()
    {
        string userName = UserInfo.Username;

        //清除登录用户信息
        UserInfo = new LoginUserInfo();

        //清除登录用户信息json
        UserInfoJson = "";

        Debug.WriteLine($"{DateTimeOffset.Now}, 用户退出登录: {userName}");
    }
}

手机验证码登录功能模块Ids4Client改为从access token解析claims,获取用户属性。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Data\Ids4Client.cs

 

/// <summary>
/// 手机验证码登录功能模块
/// </summary>
public class Ids4Client
{
    private readonly HttpClient _client;

    public Ids4Client(HttpClient httpClient)
    {
        _client = httpClient;
    }

    /// <summary>
    /// 发送验证码到手机号
    /// </summary>
    /// <param name="phoneNumber"></param>
    /// <returns></returns>
    public async Task<string> SendPhoneCodeAsync(string phoneNumber)
    {
        string url = $"api/PhoneCodeLogin/SendPhoneCode?phoneNumber={phoneNumber}";

        string result = await _client.GetStringAsync(url);

        return result;
    }

    /// <summary>
    /// 手机验证码登录
    /// </summary>
    /// <param name="phoneNumber">手机号</param>
    /// <param name="verificationCode">验证码</param>
    /// <returns></returns>
    public async Task<LoginUserInfo> PhoneCodeLogin(string phoneNumber, string verificationCode)
    {
        var request = new DiscoveryDocumentRequest()
        {
            Policy = new DiscoveryPolicy()
            {
                //本地调试抓包
                RequireHttps = false
            }
        };

        //发现端点
        var discovery = await _client.GetDiscoveryDocumentAsync(request);

        if (discovery.IsError)
        {
            Debug.WriteLine($"访问Identity Server 4服务器失败, Error={discovery.Error}");
            return null;
        }

        //填写登录参数,必须跟Identity Server 4服务器Config.cs定义一致
        var requestParams = new Dictionary<string, string>
        {
            ["client_Id"] = "PhoneCode",
            ["client_secret"] = "PhoneCode.Secret",
            ["grant_type"] = "PhoneCodeGrantType",
            ["scope"] = "openid profile scope1 role",
            ["PhoneNumber"] = phoneNumber,
            ["VerificationCode"] = verificationCode
        };

        //请求获取token
        var tokenResponse = await _client.RequestTokenRawAsync(discovery.TokenEndpoint, requestParams);
        if (tokenResponse.IsError)
        {
            Debug.WriteLine($"请求获取token失败, Error={tokenResponse.Error}");
            return null;
        }

        string userInfoJson = "";

        //设置Http认证头
        _client.SetBearerToken(tokenResponse.AccessToken);

        //获取用户信息
        //var userInfoResponse = await _client.GetAsync(discovery.UserInfoEndpoint);
        //if (!userInfoResponse.IsSuccessStatusCode)
        //{
        //    //scope必须包含profile才能获取到用户信息
        //    //如果客户端请求scope没有profile,返回403拒绝访问
        //    Debug.WriteLine($"获取用户信息失败, StatusCode={userInfoResponse.StatusCode}");
        //}
        //else
        //{
        //    // {"sub":"d2f64bb2-789a-4546-9107-547fcb9cdfce","name":"Alice Smith","given_name":"Alice","family_name":"Smith","website":"http://alice.com","role":["Admin","Guest"],"preferred_username":"alice"}
        //    userInfoJson = await userInfoResponse.Content.ReadAsStringAsync();
        //    Debug.WriteLine($"获取用户信息成功, {userInfoJson}");
        //}
        //MAUI Blazor客户端PhoneCodeGrantType方式访问Id4,只获取到sub nation

        var jwtSecurityToken = new JwtSecurityToken(tokenResponse.AccessToken);

        LoginUserInfo loginUserInfo = new LoginUserInfo();

        loginUserInfo.AccessToken = tokenResponse.AccessToken;
        loginUserInfo.RefreshToken = tokenResponse.RefreshToken;
        loginUserInfo.ExpiresIn = DateTimeOffset.Now.AddSeconds(tokenResponse.ExpiresIn);

        //用户名
        loginUserInfo.Username = jwtSecurityToken.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Name)?.Value;

        //用户ID
        var claimId = jwtSecurityToken.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
        if (Guid.TryParse(claimId?.Value, out Guid userId))
            loginUserInfo.UserId = $"{userId}";

        //角色
        //id4返回的角色是字符串数组或者字符串
        var roleNames = jwtSecurityToken.Claims.Where(x => x.Type == JwtClaimTypes.Role).Select(x => x.Value);
        loginUserInfo.UserRole = string.Join(",", roleNames);

        return loginUserInfo;
    }
}

 

注册认证功能模块。NuGet安装Microsoft.Extensions.Http

D:\Software\gitee\mauiblazorapp\MaBlaApp\MauiProgram.cs

 

        builder.Services.AddSingleton<LoginUserManager>();

        //NuGet安装Microsoft.Extensions.Http
        //访问Identity Server 4服务器的HttpClient
        builder.Services.AddHttpClient<Ids4Client>()
            .ConfigureHttpClient(c => c.BaseAddress = new Uri("http://localhost:5000"));//Windows调试
            //.ConfigureHttpClient(c => c.BaseAddress = new Uri("http://10.0.2.2:5000"));//安卓模拟器,AndroidManifest.xml要添加android:usesCleartextTraffic="true"支持访问http网站

 

主页增加显示登录用户信息,如果当前没有登录信息的话,自动跳转到登录页面。如果不需要这个功能,注解OnInitializedAsync即可。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\Index.razor

@page "/"
@using MaBlaApp.Data
@inject LoginUserManager loginUserManager
@inject NavigationManager NavManager

<h1>Hello, world!</h1>

Welcome to your new app.

@*<SurveyPrompt Title="How is Blazor working for you?" />*@

<ul class="list-group" style="overflow:auto">
    <li class="list-group-item">
        <a href="testble" class="btn btn-primary btn-sm">测试低功耗蓝牙</a>
    </li>
    <li class="list-group-item">
        <a href="scanqrcode" class="btn btn-primary btn-sm">扫描二维码</a>
    </li>

    @if (isAuthenticated)
    {
        <li class="list-group-item d-flex justify-content-between mb-1">
            <small class="align-self-center">您已经登录</small>
            <button class="btn btn-warning btn-sm ms-2" @onclick="Logout">退出登录</button>
        </li>
        <li class="list-group-item">
            <strong>用户信息</strong>
        </li>
        <li class="list-group-item d-flex justify-content-between mb-1">
            <strong>AccessToken</strong>
            <small>@userInfo.AccessToken</small>
        </li>
        <li class="list-group-item d-flex justify-content-between mb-1">
            <strong>RefreshToken</strong>
            <small>@userInfo.RefreshToken</small>
        </li>
        <li class="list-group-item d-flex justify-content-between mb-1">
            <strong>ExpiresIn</strong>
            <small>@userInfo.ExpiresIn</small>
        </li>
        <li class="list-group-item d-flex justify-content-between mb-1">
            <strong>UserId</strong>
            <small>@userInfo.UserId</small>
        </li>
        <li class="list-group-item d-flex justify-content-between mb-1">
            <strong>Username</strong>
            <small>@userInfo.Username</small>
        </li>
        <li class="list-group-item d-flex justify-content-between mb-1">
            <strong>UserRole</strong>
            <small>@userInfo.UserRole</small>
        </li>
    }
    else
    {
        <li class="list-group-item d-flex justify-content-between mb-1">
            <small class="align-self-center">您还没有登录,请先登录</small>
            <a class="btn btn-primary btn-sm ms-2" href="login">登录</a>
        </li>
    }
</ul>

@code {
    private bool isAuthenticated => loginUserManager.IsAuthenticated;
    private LoginUserInfo userInfo => loginUserManager.UserInfo;

    protected override async Task OnInitializedAsync()
    {
        if (!isAuthenticated)
        {
            //没有用户登录信息,跳转到登录页面
            //NavManager.NavigateTo("/login");
        }
    }

    private void Logout()
    {
        loginUserManager.Logout();

        //直接跳转到登录页面
        //NavManager.NavigateTo("/login");
    }
}

登录页面PhoneCodeLogin不用改。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\PhoneCodeLogin.razor
@page "/login"

@using MaBlaApp.Data

@layout LoginLayout

<div class="d-flex justify-content-center">

    <div class="card" style="width:500px">

        <div class="card-header">
            <h5>
                手机验证码登录
            </h5>
        </div>

        <div class="card-body">

            <div class="form-group form-inline">
                <label for="PhoneNumber" class="control-label">手机号</label>
                <input id="PhoneNumber" @bind="PhoneNumber" class="form-control" placeholder="请输入手机号" />
            </div>

            <div class="form-group form-inline">
                <label for="VerificationCode" class="control-label">验证码</label>
                <input id="VerificationCode" @bind="VerificationCode" class="form-control" placeholder="请输入验证码" />
                @if (CanGetVerificationCode)
                {
                    <button type="button" class="btn btn-link" @onclick="GetVerificationCode">
                        获取验证码
                    </button>
                }
                else
                {
                    <label>@GetVerificationCodeMsg</label>
                }
            </div>

        </div>

        <div class="card-footer">
            <button type="button" class="btn btn-primary" @onclick="Login">
                登录
            </button>
        </div>

    </div>
</div>

@code {

    [Inject]
    private Ids4Client ids4Client { get; set; }

    [Inject]
    private NavigationManager navigationManager { get; set; }

    [Inject]
    private LoginUserManager loginUserManager { get; set; }

    private string PhoneNumber;

    private string VerificationCode;

    //获取验证码按钮当前状态
    private bool CanGetVerificationCode = true;

    private string GetVerificationCodeMsg;

    //获取验证码
    private async void GetVerificationCode()
    {
        if (CanGetVerificationCode)
        {
            //发送验证码到手机号
            string result = await ids4Client.SendPhoneCodeAsync(PhoneNumber);

            if (result != "发送验证码成功")
                return;

            CanGetVerificationCode = false;

            //1分钟倒计时
            for (int i = 60; i >= 0; i--)
            {
                GetVerificationCodeMsg = $"获取验证码({i})";

                await Task.Delay(1000);

                //通知页面更新
                StateHasChanged();
            }

            CanGetVerificationCode = true;

            //通知页面更新
            StateHasChanged();
        }
    }

    //登录
    private async void Login()
    {
        //手机验证码登录
        var userInfo = await ids4Client.PhoneCodeLogin(PhoneNumber, VerificationCode);

        //登录
        loginUserManager.Login(userInfo);

        //跳转回主页
        navigationManager.NavigateTo("/");
    }
}

测试登录

把服务端AspNetId4Web项目和客户端MaBlaApp项目跑起来。登录页面只是遮盖了index主页,还是可以切换到其他页面。

为了解决这个问题,登录页面要引用一个遮盖MainPageLoginLayout

 

@inherits LayoutComponentBase

<div class="m-4">
    @Body
</div>

@code {

}

 

登录页面引用这个LoginLayout布局。

D:\Software\gitee\mauiblazorapp\MaBlaApp\Pages\PhoneCodeLogin.razor

@layout LoginLayout

 再次运行,登录页面遮盖了整个APP,满足需求。

同时运行AspNetId4Web认证服务器和MaBlaApp客户端项目,输入种子用户alice的手机号13512345001,获取验证码,在AspNetId4Web项目的控制台可以看到验证码,填写到MaBlaApp网页,即可登录。登录成功后,可以显示获取到的用户属性。

查看AspNetId4Web项目控制台输出:

[16:58:37 Information] IdentityServer4.Validation.TokenRequestValidator

Token request validation success, {"ClientId": "PhoneCode", "ClientName": "PhoneCode", "GrantType": "PhoneCodeGrantType", "Scopes": "openid profile role scope1", "AuthorizationCode": null, "RefreshToken": null, "UserName": null, "AuthenticationContextReferenceClasses": null, "Tenant": null, "IdP": null, "Raw": {"client_Id": "PhoneCode", "client_secret": "***REDACTED***", "grant_type": "PhoneCodeGrantType", "scope": "openid profile scope1 role", "PhoneNumber": "13512345001", "VerificationCode": "2747"}, "$type": "TokenRequestValidationLog"}

 

[16:58:37 Debug] IdentityServer4.Services.DefaultClaimsService

Getting claims for access token for client: PhoneCode

 

[16:58:37 Debug] IdentityServer4.Services.DefaultClaimsService

Getting claims for access token for subject: d2f64bb2-789a-4546-9107-547fcb9cdfce

 

[16:58:38 Information] IdentityServer4.Events.DefaultEventService

{"ClientId": "PhoneCode", "ClientName": "PhoneCode", "RedirectUri": null, "Endpoint": "Token", "SubjectId": "d2f64bb2-789a-4546-9107-547fcb9cdfce", "Scopes": "openid profile role scope1", "GrantType": "PhoneCodeGrantType", "Tokens": [{"TokenType": "access_token", "TokenValue": "****Kenw", "$type": "Token"}], "Category": "Token", "Name": "Token Issued Success", "EventType": "Success", "Id": 2000, "Message": null, "ActivityId": "0HMOIATNCVSRE:00000005", "TimeStamp": "2023-02-19T08:58:38.0000000Z", "ProcessId": 17392, "LocalIpAddress": "::1:5000", "RemoteIpAddress": "::1", "$type": "TokenIssuedSuccessEvent"}

 

[16:58:38 Debug] IdentityServer4.Endpoints.TokenEndpoint

Token request success.

 

MaBlaApp网页显示用户属性:

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