我最近一直在为blazor服务器(dotnet 6)编写应用程序,并且正在努力解决用户身份验证问题。
我现在把它写成如下:
- 我有一个访问令牌和一个刷新令牌,访问令牌有效期为1分钟,刷新令牌有效期为14天。
- 当访问令牌过期时,应用程序检查刷新令牌在数据库中是否有效,如果有效,则刷新并生成新的令牌。
- 两个令牌都存储在本地存储,这里我的问题是,我做得对吗?
在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"];
}
}
最后我这样做了:
- 访问令牌生成,有效期为10分钟,并存储在ProtectedLocalStorage 中
- 每次在网站上执行操作时,令牌都会刷新
- 令牌也存储在数据库中,并在那里刷新
- 如果令牌无效或不在数据库中,则用户未登录