| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- using System.IdentityModel.Tokens.Jwt;
- using System.Security.Claims;
- using System.Text;
- using System.Security.Cryptography;
- using Microsoft.IdentityModel.Tokens;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Caching.Memory;
- using bitforum.Models.Account;
- using bitforum.Models;
- using bitforum.Repository;
- namespace bitforum.Services
- {
- public interface IJwtAuthService
- {
- // Access Token 생성
- string GenerateAccessToken(Member member);
- // Refresh Token 생성
- string GenerateRefreshToken();
- // Access Token 검증
- bool ValidateAccessToken(string accessToken);
- // Refresh Token 검증
- Task<bool> ValidateRefreshToken(string refreshToken);
- // Refresh Token DB 저장
- Task SaveRefreshToken(int memberID, string refreshToken);
- // AccessToken에서 회원 ID 추출
- Member? GetMemberFromAccessToken(string accessToken);
- // RefreshToken에서 회원 ID 추출
- Task<Member?> GetMemberFromRefreshToken(string refreshToken);
- // AccessToken 만료 기간 조회
- DateTime GetAccessTokenExpiration();
- // RefreshToken 만료 기간 조회
- DateTime GetRefreshTokenExpiration();
- // AccessToken 설정 및 쿠키 설정
- string SetAccessTokenAndCookieAsync(Member member);
- // RefreshToken 설정 및 쿠키 설정
- Task<string> SetRefreshTokenAndCookieAsync(Member member);
- }
- public class JwtAuthService : IJwtAuthService
- {
- private readonly DefaultDbContext _db;
- private readonly IHttpContextAccessor _httpContextAccessor;
- private readonly IMemoryCache _cache;
- private readonly IRedisRepository _redisRepository;
- private readonly string _secretKey;
- private readonly string _issuer;
- private readonly string _audience;
- private readonly int _accessTokenExpiration;
- private readonly int _refreshTokenExpiration;
- public JwtAuthService(DefaultDbContext db, IHttpContextAccessor httpContextAccessor, IMemoryCache cache, IRedisRepository redisRepository)
- {
- _db = db;
- _httpContextAccessor = httpContextAccessor;
- _cache = cache;
- _redisRepository = redisRepository;
- _secretKey = Setting.Jwt.SecretKey;
- _issuer = Setting.Jwt.Issuer;
- _audience = Setting.Jwt.Audience;
- _accessTokenExpiration = Setting.Jwt.AccessTokenExpiration;
- _refreshTokenExpiration = Setting.Jwt.RefreshTokenExpiration;
- }
- public string GenerateAccessToken(Member member)
- {
- var claims = new[]
- {
- new Claim(ClaimTypes.NameIdentifier, member.ID.ToString()),
- new Claim(ClaimTypes.Email, member.Email),
- new Claim(ClaimTypes.Name, member.Name ?? "")
- };
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
- var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- var token = new JwtSecurityToken(
- issuer: _issuer,
- audience: _audience,
- claims: claims,
- expires: GetAccessTokenExpiration(),
- signingCredentials: creds
- );
- return new JwtSecurityTokenHandler().WriteToken(token);
- }
- public string GenerateRefreshToken()
- {
- return Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
- }
- public bool ValidateAccessToken(string accessToken)
- {
- // 캐싱된 검증 결과 사용 (속도 향상)
- if (_cache.TryGetValue($"valid_token_{accessToken}", out bool isValid))
- {
- return isValid;
- }
- try
- {
- var tokenHandler = new JwtSecurityTokenHandler();
- var key = Encoding.UTF8.GetBytes(_secretKey);
- var principal = tokenHandler.ValidateToken(accessToken, new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(key),
- ValidateIssuer = true,
- ValidateAudience = true,
- ValidIssuer = _issuer,
- ValidAudience = _audience,
- ValidateLifetime = true,
- ClockSkew = TimeSpan.Zero
- }, out SecurityToken validatedToken);
- // 검증된 토큰을 캐싱하여 속도 향상 (만료 시간의 절반으로 유지)
- _cache.Set($"valid_token_{accessToken}", true, TimeSpan.FromMinutes(_accessTokenExpiration / 2));
- return true;
- }
- catch (SecurityTokenExpiredException)
- {
- return false;
- }
- catch (Exception)
- {
- return false;
- }
- }
- public async Task<bool> ValidateRefreshToken(string refreshToken)
- {
- // 속도를 위해 Redis에서 RefreshToken 검증
- return await _redisRepository.ExistsAsync($"refresh_token_{refreshToken}");
- // DB에서 RefreshToken 검증
- // return await _db.RefreshToken.AnyAsync(c => c.Token == refreshToken && c.Expiration > DateTime.UtcNow);
- }
- public async Task SaveRefreshToken(int memberID, string refreshToken)
- {
- await _redisRepository.SetStringAsync($"refresh_token_{refreshToken}", memberID.ToString(), _refreshTokenExpiration);
- var expiration = DateTime.UtcNow.AddDays(_refreshTokenExpiration);
- var existingToken = await _db.RefreshToken.FirstOrDefaultAsync(c => c.MemberID == memberID);
- if (existingToken != null)
- {
- if (existingToken.Token == refreshToken)
- {
- return;
- }
- existingToken.Token = refreshToken;
- existingToken.Expiration = expiration;
- }
- else
- {
- _db.RefreshToken.Add(new RefreshToken
- {
- MemberID = memberID,
- Token = refreshToken,
- Expiration = expiration
- });
- }
- // 만료된 RefreshToken 정리
- _db.RefreshToken.RemoveRange(_db.RefreshToken.Where(c => c.Expiration < DateTime.UtcNow));
- await _db.SaveChangesAsync();
- }
- public Member? GetMemberFromAccessToken(string accessToken)
- {
- var jwtToken = new JwtSecurityTokenHandler().ReadToken(accessToken) as JwtSecurityToken;
- if (jwtToken == null)
- {
- return null;
- }
- var memberIDClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
- var emailClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
- if (memberIDClaim == null || emailClaim == null)
- {
- return null;
- }
- return new Member
- {
- ID = int.Parse(memberIDClaim.Value),
- Email = emailClaim.Value
- };
- }
- public async Task<Member?> GetMemberFromRefreshToken(string refreshToken)
- {
- var memberID = await _db.RefreshToken.Where(rt => rt.Token == refreshToken && rt.Expiration > DateTime.UtcNow).Select(c => c.MemberID).FirstOrDefaultAsync();
- if (memberID == 0)
- {
- return null;
- }
- return await _db.Member.FindAsync(memberID);
- }
- public DateTime GetAccessTokenExpiration()
- {
- return DateTime.UtcNow.AddMinutes(_accessTokenExpiration);
- }
- public DateTime GetRefreshTokenExpiration()
- {
- return DateTime.UtcNow.AddDays(_refreshTokenExpiration);
- }
- public string SetAccessTokenAndCookieAsync(Member member)
- {
- var accessToken = GenerateAccessToken(member);
- var cookies = _httpContextAccessor.HttpContext?.Response.Cookies;
- var headers = _httpContextAccessor.HttpContext?.Response.Headers;
- // AccessToken을 HttpOnly 쿠키로 설정
- if (cookies != null)
- {
- cookies.Append("accessToken", accessToken, new CookieOptions
- {
- HttpOnly = true,
- Secure = true,
- SameSite = SameSiteMode.None,
- Expires = GetAccessTokenExpiration()
- });
- }
- if (headers != null)
- {
- headers["Authorization"] = $"Bearer {accessToken}";
- }
- return accessToken;
- }
- public async Task<string> SetRefreshTokenAndCookieAsync(Member member)
- {
- var refreshToken = GenerateRefreshToken();
-
- await SaveRefreshToken(member.ID, refreshToken);
- var cookies = _httpContextAccessor.HttpContext?.Response.Cookies;
- // RefreshToken을 HttpOnly 쿠키로 설정
- if (cookies != null)
- {
- cookies.Append("refreshToken", refreshToken, new CookieOptions
- {
- HttpOnly = true,
- Secure = true,
- SameSite = SameSiteMode.None,
- Expires = GetRefreshTokenExpiration()
- });
- }
- return refreshToken;
- }
- }
- }
|