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可以用这种方式实现登录功能。

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

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

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

 

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

沿用2021年DEMO的id4服务器,把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一个claim,context.Subject.FindAll(JwtClaimTypes.Name)根本无法获取到用户名等所需用户属性。我试过注解掉我写的ProfileService,id4会用内置的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项目,把2021年DEMO的MAUI 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主页,还是可以切换到其他页面。

为了解决这个问题,登录页面要引用一个遮盖MainPage的LoginLayout。

 

复制代码
@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


新软师兄 » MAUI Blazor学习7-实现登录跳转页面
50T免费网盘资源大集合【持续更中~~~~】:点击查看

dase kand pornhan.mobi xvideo desi gay pcso 2pm result today pinoytvfriends.com where i can watch bad romeo كلام فى النيك wfporn.com قصص محارم حديثة busporn porngugu.mobi indian sexx vedios sex ka video noticieroporno.com himachal pradesh sex com
nero hentai hentaitgp.com ламия хентай www.mom xxx.com alohaporn.me sahara knite mature fucking tubepatrolporn.com bhabi sex indian girl sex gotporn.mobi xnxx family strocks ang probinsyano july 20 2022 full episode youtube pilipinoteleserye.com ano ang pambansang sasakyan ng pilipinas
احلي سكس محارم pornxporn.org نيك فلاحى multi.xnxx alohaporn.net telugu sex chart سكس قصيرات arabysexy.org نيك نقاب www assames sex com umora.info desi sexy bhabi 8teenx bukaporn.com india hot sex videos