Blazor server에서 사용자 인증 부분을 처리하고 작동은 잘 하는것처럼 보이는데…
로그인된 사용자의 정보를 활용하는 부분에서 Clean하게 구성하고 싶어서 질문 올립니다.
분명 깔끔하고 심플한 방법이 있을 것 같은데, 잘 안되네요…
아래는 제가 처리하는 방식입니다.
- 로그인 정보 입력 후 로그인 시도
// Login.razor
@code{
private async Task Authenticate()
{
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(password))
{
snackbarRef.Show("필수입력값 누락", Severity.Warning, variant: Variant.Outlined);
return;
}
var userSession = jwtAuthenticationManager.GenerateJwtToken(userId, password);
if (userSession == null)
{
snackbarRef.Show("토큰 생성 실패!", Severity.Error, variant: Variant.Outlined);
return;
}
var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
await customAuthStateProvider.LoginUser(userSession.Result);
_navi.NavigateTo("/", true);
}
}
2. JWT 생성 후 암호화
// JwtAuthenticationManager.cs
public async Task<UserSession?> GenerateJwtToken(string userId, string password)
{
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(password))
{
return null;
}
var tokenExpiryTimeStamp = DateTime.Now.AddMinutes(JWT_TOKEN_VALIDITY_MINS);
var tokenKey = Encoding.ASCII.GetBytes(JWT_SECURITY_KEY);
var claimsIdentity = new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, userId),
new Claim(ClaimTypes.Hash, password),
});
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(tokenKey),
SecurityAlgorithms.HmacSha256Signature);
var securityTokenDescriptor = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
Expires = tokenExpiryTimeStamp,
SigningCredentials = signingCredentials
};
var jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
var securityToken = jwtSecurityTokenHandler.CreateToken(securityTokenDescriptor);
var token = jwtSecurityTokenHandler.WriteToken(securityToken);
// returning User session
var userSession = new UserSession
{
UserId = userId,
Role = "Member",
Token = token,
ExpiresIn = (int)tokenExpiryTimeStamp.Subtract(DateTime.Now).TotalSeconds
};
return userSession;
}
3. 암호화된 JWT를 로컬 스토리지에 저장하며,
AuthenticationStateManager를 상속받은 CustomAuthenticationStateManager.cs 를 아래와 같이 구성했습니다.
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var userSession = await _localStorage.ReadEncryptedItemAsync<UserSession>("UserSession");
if (userSession == null)
return await Task.FromResult(new AuthenticationState(_anonymous));
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
{
new Claim(ClaimTypes.Name, userSession.UserId),
new Claim(ClaimTypes.Role, userSession.Role),
}, "JwtAuth"));
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
}
catch (Exception ex)
{
Log.Write(Serilog.Events.LogEventLevel.Error, ex, "");
return await Task.FromResult(new AuthenticationState(_anonymous));
}
}
public async Task LoginUser(UserSession userSession)
{
ClaimsPrincipal claimsPrincipal;
claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>()
{
new Claim(ClaimTypes.Name, userSession.UserId),
new Claim(ClaimTypes.Role, userSession.Role),
}));
userSession.ExpiryTimeStamp = DateTime.Now.AddSeconds(userSession.ExpiresIn);
await _localStorage.SaveItemEncryptedAsync("UserSession", userSession);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
}
public async Task LogoutUser()
{
await _localStorage.RemoveItemAsync("UserSession");
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(_anonymous)));
}
public async Task<string> GetTokenAsync()
{
var result = string.Empty;
try
{
var userSession = await _localStorage.ReadEncryptedItemAsync<UserSession>("UserSession");
if (userSession != null && DateTime.Now < userSession.ExpiryTimeStamp)
result = userSession.Token;
}
catch
{
}
return result;
}
그리고 아래와 같이 MainLayout.razor에서 로그인, 로그아웃 기능과 세션 만료여부를 파악하도록 해놨습니다.
제 질문은 아래와 같습니다.
1. 세션 만료 여부를 파악하는 방법을 아래와 같은 방식으로 하는게 맞는지요? (GetToken이라는 함수를 호출해서 토큰 내부에 저장된 세션 만료기한과 비교)
2. 로그인, 로그아웃을 아래와 같이 처리허는게 맞는지?
3. Local storage에 저장된 토큰 내부의 id를 읽어서 내 정보와 권한을 가지고 올 때, 거의 모든 페이지에 User정보를 읽고 쓰는 Repository를 inject해야하는데… 이게 맞는건지요?
@inject AuthenticationStateProvider authStateProvider
@code{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
var token = await customAuthStateProvider.GetTokenAsync();
if (string.IsNullOrWhiteSpace(token))
{
// 유효한 토큰이 없으면 강제로 로그아웃한다.
await customAuthStateProvider.LogoutUser();
if (_navi.Uri.EndsWith("/signin") == false
&& _navi.Uri.EndsWith("/signup") == false
&& _navi.Uri.EndsWith("/") == false)
{
_navi.NavigateTo("/signin", true);
}
}
}
public async Task LoginUser(UserSession userSession)
{
ClaimsPrincipal claimsPrincipal;
claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>()
{
new Claim(ClaimTypes.Name, userSession.UserId),
new Claim(ClaimTypes.Role, userSession.Role),
}));
userSession.ExpiryTimeStamp = DateTime.Now.AddSeconds(userSession.ExpiresIn);
await _localStorage.SaveItemEncryptedAsync("UserSession", userSession);
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
}
private async Task Logout()
{
var customAuthStateProvider = (CustomAuthenticationStateProvider)authStateProvider;
await customAuthStateProvider.LogoutUser();
_navi.NavigateTo("/", true);
}
public async Task<string> GetTokenAsync()
{
var result = string.Empty;
try
{
var userSession = await _localStorage.ReadEncryptedItemAsync<UserSession>("UserSession");
if (userSession != null && DateTime.Now < userSession.ExpiryTimeStamp)
result = userSession.Token;
}
catch
{
}
return result;
}
}
모르는 것은 많은데, 이상적인걸 원하다보니 질문 내용도 너무 답변하기 어려우실 것 같습니다…ㅠ
확실히 알고 구현하고자 의견을 여쭤봅니다. 덮어놓고 대충 구현하기엔 나중에 감당 안될 것 같아서요…
감사합니다.