我最近一直在为 Blazor 服务器 (.NET 6) 编写应用程序,并且正在努力解决用户身份验证问题。
我目前写的如下:
在 Blazor 中,我见过大多数使用本地存储的实现,但听到了各种声音(将它们保存在安全的 cookie 或内存中(?))。为了使令牌不易受到 csrf、xss 等攻击,最好的方法是什么?我应该将两个令牌放在一处还是以某种方式将它们分开?
我知道在 Blazor 中我可以使用基于
HttpContext
和 cookie 的内置授权。我需要数据库中的令牌才能管理用户会话。
我编写了一个处理用户身份验证的
CustomAuthenticationStateProvider
类。一切正常,但我不知道安全方面是否做得很好。
public class CustomAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly Blazored.LocalStorage.ILocalStorageService _localStorage;
private readonly Application.Interfaces.ITokenService _tokenService;
private readonly IHttpContextAccessor _httpContextAccessor;
private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
public CustomAuthenticationStateProvider(Blazored.LocalStorage.ILocalStorageService localStorage, Application.Interfaces.ITokenService tokenService, IHttpContextAccessor httpContextAccessor)
{
_localStorage = localStorage;
_tokenService = tokenService;
_httpContextAccessor = httpContextAccessor;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var userSession = await _localStorage.GetItemAsync<UserSession>("UserSession");
if (userSession == null)
return await Task.FromResult(new AuthenticationState(_anonymous));
if (!_tokenService.ValidateToken(userSession.AuthToken))
{
if (!_tokenService.ValidateToken(userSession.RefreshToken))
{
await this.UpdateAuthenticationState(null);
return await Task.FromResult(new AuthenticationState(_anonymous));
}
else
{
var refreshTokenValidInDb = await _tokenService.CheckIfRefreshTokenIsValid(userSession.RefreshToken);
if (refreshTokenValidInDb)
{
if (_httpContextAccessor.HttpContext == null)
{
return await Task.FromResult(new AuthenticationState(_anonymous));
}
var userAgent = this.GetUserAgent(_httpContextAccessor.HttpContext);
var ipAddress = this.GetIpAddress(_httpContextAccessor.HttpContext);
var (authToken, refreshToken) = await _tokenService.RefreshAuthTokens(userSession.RefreshToken, userAgent, ipAddress);
userSession.AuthToken = authToken;
userSession.RefreshToken = refreshToken;
await _localStorage.SetItemAsync("UserSession", userSession);
}
else
{
await this.UpdateAuthenticationState(null);
return await Task.FromResult(new AuthenticationState(_anonymous));
}
}
}
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userSession.Id.ToString()),
new Claim(ClaimTypes.Name, userSession.Name),
new Claim("token", userSession.AuthToken),
new Claim("refreshToken", userSession.RefreshToken),
new Claim("useragent", userSession.UserAgent),
new Claim("ipv4", userSession.IPv4)
}, "Auth"));
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
}
catch
{
return await Task.FromResult(new AuthenticationState(_anonymous));
}
}
public async Task UpdateAuthenticationState(UserSession? userSession)
{
ClaimsPrincipal claimsPrincipal;
if (userSession != null)
{
await _localStorage.SetItemAsync("UserSession", userSession);
claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, userSession.Id.ToString()),
new Claim(ClaimTypes.Name, userSession.Name),
new Claim("token", userSession.AuthToken),
new Claim("refreshToken", userSession.RefreshToken),
new Claim("useragent", userSession.UserAgent),
new Claim("ipv4", userSession.IPv4)
}));
}
else
{
await _localStorage.RemoveItemAsync("UserSession");
claimsPrincipal = _anonymous;
}
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
}
/// <summary>
/// Get ip address from HttpContext
/// </summary>
/// <param name="httpContext">HttpContext</param>
/// <returns>Client ip address</returns>
private string GetIpAddress(HttpContext httpContext)
{
var ipNullable = httpContext.Connection.RemoteIpAddress;
return (ipNullable != null) ? ipNullable.ToString() : "";
}
/// <summary>
/// Get UserAgent from HttpContext
/// </summary>
/// <param name="httpContext">HttpContext</param>
/// <returns>UserAgent</returns>
private string GetUserAgent(HttpContext httpContext)
{
return httpContext.Request.Headers["User-Agent"];
}
}
最后我是这样做的: