最近在写一个Web程序,用了.NET8的Blazor。程序里需要让用户进行登录来验证身份,记录操作。由于这个程序只是部署在私有网络内局部使用,不用太过担心被攻击,所以就选用了简单的Cookies身份验证,不用什么高端的OpenIddict或者IdentityServer之类的加大部署难度和项目复杂度了。
问题出现
这时候问题就来了。.NET7和以前的版本,可以在Blazor里面用cshtml,仅仅只用MapBlazorHub就可以了。.NET8的Blazor改动有些大,不用cshtml这些和Razor MVC相关的代码,纯使用Razor组件实现了。
这本来是好的,从Blazor前几个版本来看,就该是这样的。但是对于用Cookies进行验证来说,就不是什么好消息了。
Cookies验证与HttpContext
Cookies验证需要访问HttpContext,理所应当,因为需要把Cookies写到响应里面,带回客户端。但是在Blazor里面,访问HttpContext是有些问题存在的。从组件里面没法正确的访问HttpContext,很好理解,因为HttpContext可以理解为对当前连接的抽象,Blazor组件一般有较长的生命期,当我们想访问HttpContext的时候,连接可能早就结束释放掉了,这时候访问没有任何意义,还可能会造成异常。因此,.NET8的HttpContextAccessor在Blazor“服务器交互”呈现模式中只返回空的HttpContext。
好在MVC可以在调用视图方法时候得到正确的HttpContext啊,因此在.NET7以及以前,可以用mvc的cshtml页面来处理登录。在.NET8里面,MVC和Blazor彻底分家了,这下要再把MVC支持添加回管道里面了…是吗?
.NET8中的纯(?)Blazor方案
当然是有方法不用MVC实现的,只需要稍微编码一下下就可以用纯Blazor实现Cookies登录。其实在之前的版本里面,我用了一点点js,我觉得不是很纯,所以又想出来了另一种方法,规避了js不熟的问题。
我在标题里面的“纯”后面打了问号,其实这个方法也不是太纯,也是稍微用了一点api,不过直接使用了最小api,其实也可以接受了。
方案思路
也就是直接利用最小api,创建两个登录注销的终结点不就完事了。但是这里面要注意,需要让用户浏览器来访问这两个终结点;本着人道的原则,还最好不要带用户敏感数据一起发送;最好只用GET,因为不想用js。
因此我们需要在确认用户身份后,生成一个token,让用户带着token去访问登录结点,在登录结点里面验证token,登入用户。怎么样,是不是十分简单又好用?下面配上代码一起食用。
Code time
首先这是启动代码里面的最小api部分,添加了两个终结点处理登入登出。
// 登录app.MapGet("/login/{token}", AuthService.LoginAction);// 注销app.MapGet("/logout", async (HttpContext context) =>{ if (context.User != null) await context .SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.Redirect("/");});处理登录逻辑的部分,从AuthTokenService中获取已经验证的用户相关信息。
public static Func<string, IConfiguration, AuthService, AuthTokenService, HttpContext, Task<IResult>> LoginAction = async (string token, IConfiguration config, AuthService cookieService, AuthTokenService tokenService, HttpContext context) => { User? user; bool remember; if (!tokenService.VerifyToken(token, out user, out remember)) return Results.Redirect("/login");
var cookieExpirationDays = config .GetValue("Spark:Auth:CookieExpirationDays", 5); var cookieClaims = await cookieService.CreateCookieClaims(user!);
await context.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, cookieClaims, new AuthenticationProperties { IsPersistent = remember, IssuedUtc = DateTimeOffset.UtcNow, ExpiresUtc = DateTimeOffset.UtcNow.AddDays(cookieExpirationDays) });
return Results.Redirect("/"); };负责Token生成以及验证,保存从登录的前端逻辑处带来的用户信息,可以确保敏感信息不以明文形式在网络上传递。
虽然是内网使用且大概率只有一个客户端,但是还是潜在并发可能,所以加了锁。
public class AuthTokenService{ private readonly Dictionary<string,(User,bool)> _tokenDict = new(); private readonly object _lock = new();
public bool VerifyToken(string token, out User? user, out bool remember) { lock (_lock) { if (!_tokenDict.ContainsKey(token)) { user = default; remember = default; return false; }
(user, remember) = _tokenDict[token]; _tokenDict.Remove(token); return true; } }
public string GenerateToken(User user, bool remember) { var token = Guid.NewGuid().ToString(); _tokenDict.Add(token, (user, remember)); return token; }}确认用户身份之后,把信息传递给UserTokenService,获取token,把用户导航到api结点,完成登录过程。
async Task LoginAsync() { if(!_isValid) return;
var user = await _usersService .FindUserAsync(_model.Email, _usersService.GetSha256Hash(_model.Password));
if (user == null) { _errors = _errors.Append("邮箱或密码错误").ToArray(); return; }
var token = _tokenService.GenerateToken(user, _model.RememberMe);
_nav.NavigateTo($"/login/{token}",true); // true确保强制刷新 }经过测试,以上代码就可以不过多引入支持而实现Blazor的Cookies登录。如有其它好的方案,欢迎大家留言交流!
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时