1044 字
3 分钟
Blazor Cookies 身份验证简易方案
2023-12-30

最近在写一个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登录。如有其它好的方案,欢迎大家留言交流!

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Blazor Cookies 身份验证简易方案
https://kami11.top/posts/blazor-cookies-auth/
作者
kami
发布于
2023-12-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

随机文章 随机推荐
暂无数据

目录