如果自学Blazor ,因为增加了许多知识点,有许多问题会是一头雾水,难以理清。Blazor Server 的登录与退出就是一个非常难以理清的问题,因为我有强迫症,既然用了Blazor,我难道又要退回去用Razor Page?新建立的Blazor Server程序,使用Identity的个人标识账户,它的登录与退出默认使用的是Razor Page,因为它能非常清晰地得到ClaimsPrincipal 的User, UserManager和SignInManager几乎包办了一切用户操作而不用怎么操心。而令人遗憾的是 UserManager SignInManager 不支持Razor组件,特别是 SignInManager不支持用于Service, 所以想抛开Razor Page,它的登录与退出只能另想办法,好像一切要推倒重来。虽然Mvc也有User,所以在网上查找大都是用Api Controller。

然而,这2天它又可气的新出了Maui,它的功用更加强大,但学起来不知道又会是怎样?

Blazor的数据操作其实是简便的,它不用Razor Page那样在客户端进行对象字段一一匹配,然后在服务端拼接生成对象,万一那个没有匹配到,一切玩完。Blazor是对对象直接操作,通过时时连通的通道,通过注册的Service,所以,登录与退出其实也是这样,把账户和密码传过去,或操作退出的服务,就可以了。那么, SignInManager它还做好多工作,所以,在Blazor Server 的登录与退出中,要加上一些其他的动作,其实就是实现AuthenticationStateProvider,在网上查找,这个没有让我满意的结果。

直接上代码:

    public class RevalidatingIdentityAuthenticationStateProvider<TUser>
        : RevalidatingServerAuthenticationStateProvider where TUser : class
        private readonly ILoggerFactory _loggerFactory;
        private readonly IServiceScopeFactory _scopeFactory;
        private readonly IdentityOptions _options;
        private readonly ILocalStorageService _localStorage;
        private readonly HttpClient _httpClient;
        private readonly IUserClaimsPrincipalFactory<TUser> _principalFactory;
        private readonly IOptionsMonitor<JwtOption> _jwtOpt;
        private const string authToken = "xxxxxxToken";
        public RevalidatingIdentityAuthenticationStateProvider(
            ILoggerFactory loggerFactory,
            IServiceScopeFactory scopeFactory,
            IOptions<IdentityOptions> optionsAccessor,
            ILocalStorageService localStorage,
            HttpClient httpClient,
            IUserClaimsPrincipalFactory<TUser> principalFactory,
            IOptionsMonitor<JwtOption> jwtOpt)
            : base(loggerFactory)
            _loggerFactory = loggerFactory;
            _scopeFactory = scopeFactory;
            _options = optionsAccessor.Value;
            _localStorage = localStorage;
            _httpClient = httpClient;
            _principalFactory = principalFactory;
            _jwtOpt = jwtOpt;
        protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
        protected override async Task<bool> ValidateAuthenticationStateAsync(
            AuthenticationState authenticationState, CancellationToken cancellationToken)
            // Get the user manager from a new scope to ensure it fetches fresh data
            var scope = _scopeFactory.CreateScope();
                var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
                return await ValidateSecurityStampAsync(userManager, authenticationState.User);
            finally
                if (scope is IAsyncDisposable asyncDisposable)
                    await asyncDisposable.DisposeAsync();
                    scope.Dispose();
        private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
            var user = await userManager.GetUserAsync(principal);
            if (user == null)
                return false;
            else if (!userManager.SupportsUserSecurityStamp)
                return true;
                var principalStamp = principal.FindFirstValue(_options.ClaimsIdentity.SecurityStampClaimType);
                var userStamp = await userManager.GetSecurityStampAsync(user);
                return principalStamp == userStamp;
        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
            var savedToken = await _localStorage.GetItemAsync<string>(authToken);
            if (string.IsNullOrWhiteSpace(savedToken))
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", savedToken);
            return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(savedToken), "jwt")));
        public async Task<bool> MarkUserAsAuthenticated(LoginDto rqtDto)
            var scope = _scopeFactory.CreateScope();
                var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
                var user = await userManager.FindByNameAsync(rqtDto.UserName);
                if (user != null)
                    var result = await userManager.CheckPasswordAsync(user, rqtDto.Password);
                    if (result)
                        var principalUser = await _principalFactory.CreateAsync(user);
                        if (principalUser != null)
                            var jwtSetting = _jwtOpt.CurrentValue;
                            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.Key));
                            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                            var expiry = DateTime.Now.AddHours(jwtSetting.ExpiryInHours);
                            var token = new JwtSecurityToken(jwtSetting.Issuer, jwtSetting.Audience, principalUser.Claims, expires: expiry, signingCredentials: creds);
                            var tokenText = new JwtSecurityTokenHandler().WriteToken(token);
                            await _localStorage.SetItemAsync(authToken, tokenText);
                            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenText);
                            var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(tokenText), "jwt"))));
                            NotifyAuthenticationStateChanged(authState);
                            _loggerFactory.CreateLogger(rqtDto.UserName + " 已登录。");
                            return true;
                return false;
            finally
                if (scope is IAsyncDisposable asyncDisposable)
                    await asyncDisposable.DisposeAsync();
                    scope.Dispose();
        public async Task MarkUserAsLoggedOutAsync()
            await _localStorage.RemoveItemAsync(authToken);
            _httpClient.DefaultRequestHeaders.Authorization = null;
            var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity());
            var authState = Task.FromResult(new AuthenticationState(anonymousUser));
            NotifyAuthenticationStateChanged(authState);
        private static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];
            var jsonBytes = ParseBase64WithoutPadding(payload);
            var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
            if (keyValuePairs != null)
                if (keyValuePairs.TryGetValue(ClaimTypes.Role, out object? roles) && roles is string rolesText)
                    if (rolesText.StartsWith('['))
                        var parsedRoles = JsonSerializer.Deserialize<string[]>(rolesText);
                        if (parsedRoles != null)
                            foreach (var parsedRole in parsedRoles)
                                claims.Add(new Claim(ClaimTypes.Role, parsedRole));
                        claims.Add(new Claim(ClaimTypes.Role, rolesText));
                    keyValuePairs.Remove(ClaimTypes.Role);
                claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
            return claims;
        private static byte[] ParseBase64WithoutPadding(string base64)
            switch (base64.Length % 4)
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            return Convert.FromBase64String(base64);

这个类是继承于Microsoft.AspNetCore.Components.Server中的RevalidatingServerAuthenticationStateProvider,是添加Identity标识自动添加并已注册服务,在Areas/Identity下面,在此基础上修改的,使其具有登录退出功能和JWT令牌生成授权功能。另外说一点,_Host.cshtml中的render-mode设为Server,否则ILocalStorageService获取token会发生错误。

注意:Key要求有一定的长度,建议复制新生成的Guid.NewGuid().ToString();上篇已经实现了登录退出的主服务类,其他还需要一些具体的东西。1、引用Blazored.LocalStorage包;7、登录Razor组件(略)。注意:需要注册服务。... 之前用 Blazor 做了几个小应用,但是一直没有去做身份验证系统,之前也没有ASP.NET的基础,C#也是最近学的,而且重要的是,网上居然几乎没有相关的资料,于是咱就结合网上的一些信息和微软的官方文档(微软的文档是我见过最好的文档了)做了一个身份鉴权的小Demo。在 Provider 下写自定义的基础认证服务,这里使用的session的形式做鉴权认证。Blazor 是微软的新型的web开发方案,用来做全栈开发真是太爽了。然后在项目的目录下创建Auth目录存放鉴权的基础设施。一个简单的登录页面示例; 本示例项目为.Net Core 7.04 Blazor Server 版本。 功能:适合企业内部的账户分发(非开放公众注册)的App。 1、身份控制:分发账户为初始密码,使用初始密码登录后必须修改密码才能使用相应身份的功能;根据身份提供相应菜单。 2、定时检测:使用JWT功能,包括生成和验证jwt,可定义jwt生效时长、验证时间间隔,失效后自动转到登录页面。 3、账户管理:包括添加、编辑、离职管理示例。 本项目只在提供示例功能演示,力求简单明了和安全稳定高效,界面简单美化。 如有错误和不足,请博客中留言,谢谢! 书接上文,昨天我们快速的走了一遍wasm的开发流程(我的『MVP.Blazor』快速创建与部署),总体来说还是很不错的,无论是从技术上,还是从开发上,重点是用C#来开启前端时代,可以开发... James: 《使用Blazor开发内部后台》系列是技术社区中一位朋友投稿的系列文章,介绍自己为公司的 WebForm 遗留系统使用 Blazor 重写前端 UI 的经历。本文为第三篇,如... 在2016年, 本人就开始了一个内部项目, 其特点就是用C#构建DOM树, 然后把DOM同步到浏览器中显示. 并且在一些小工程中使用. 3年下来, 效果很不错, 但因为是使用C#来构建控件树, 在没有特定语法的情况下, 代码风格不是那么好. 典型的风格大概是这样的: 这个模式挺好的, 有点嫌弃C#代码占比太高, HTML代码靠字符串来完成, 在界面的设计上, 比较吃力. 在2019年秋, Asp.Net 3.0出来了, Blazor Server Side 也正式公布, 可以在VS2019中使用. 当时我就去尝尝鲜, 发现这东西, 和我的框架很接近. 不同的是, Blaozr Server Blazor Server组件入门 文章目录Blazor Server组件入门一、组件是什么?二、使用步骤1.新建Blazor Server项目2.新建一个文件夹命名为Components3.右键Components文件夹创建一个Razor组件并命名(组件首字符大写:如MyComponents)4.编辑组件内容,这里我直接使用了自动生成的代码5.使用组件6.Ctrl + F5运行项目7.组件传参更多 一、组件是什么? 组件(Component)是对数据和方法的简单封装。通俗的讲,我们可以把一个网页比作一个玩 转载技术社区中一位朋友最新的文章,介绍自己为公司的 WebForm 遗留系统使用 Blazor 重写前端 UI 的经历。前言啊,又好久没写文章了,这一年一直在接触新的领域,扩展了一下技术面... 欢迎转载、使用、重新发布,但务必保留文章署名AlexChow,不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。本文来自博客园,作者:周创琳 AlexChow,转载请注明原文链接.Maui Blazor 中文社区 QQ群:645660665。BA & Blazor QQ群:795206915。FreeSql QQ群:4336577。作为IDS数据库连接。 现在处于大更新时期,Menu组件要改为泛型模式。本来我们的这一篇应该是把Layout改了,但是改Layout肯定要涉及到菜单,如果现在写了呢,就进入一个发布就过时的状态,就很尴尬,所以后面的就稍微拖一拖。加上昨天有人说我用违反单一性原则,要用策略,所以这里我们说下策略怎么做。 相信很多Blazor的用户在开发内部系统上基本上都选择速度更快,加载更快的模式。但是由于是SignalR实现,所以在访问的时候会建立WebSocket通道,用于js交互和界面渲染,但是由于WebSocket是长连接,这样就会导致用户在界面的时候会一直建立链接,导致服务器宽度占用,所以微软默认会在无操作的情况下自动断开链接,然后会加上该死的重新链接的一个ui,很难看,导致很多用户看到灰色的效果。当然,微软也提供了如何处理这个情况的方案,下面我们会使用微软提供的方案解决这个问题。 将Blazor Server应用使用如http://***.com/admin这样的Url访问.无目录的跟原来一样使用MVC.这种方式不是在二级目录部署,只是URL上区别..比较适合MVC做Api或网站前台页面,Blazor server做后台管理,或者是同一个站点部署多个Blazor Server应用的情况。 这里注意一下,跳转的时候一定不能用Blazor的NavigationManager,因为我们必须刷新一次浏览器,服务端才能拿到对应的Cookie,所以这里还是使用Ajax组件的Goto去跳转,这种跳转实际上是浏览器刷新,会重走一次MVC的逻辑。有个小区别是必须使用MVC去登录Blazor本身是登录不了的。如果str是空的,或者code不是上面返回的20000,即登录成功的话,我们就使用飘窗报错。首先,因为我们说过,登录需要使用MVC的方式,所以这里需要使用浏览器发送Ajax请求的方式来登录。...