Skip to content

Latest commit

ย 

History

History
796 lines (641 loc) ยท 23.7 KB

jwt-auth.md

File metadata and controls

796 lines (641 loc) ยท 23.7 KB

JWT(Json Web Token) ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ ์„ค๊ณ„

1. JWT ํ† ํฐ ๊ด€๋ฆฌ

@Service
public class JwtTokenService {
    
    @Value("${jwt.secret}")
    private String secretKey;
    
    private final long ACCESS_TOKEN_VALIDITY = 3600000; // 1์‹œ๊ฐ„
    private final long REFRESH_TOKEN_VALIDITY = 604800000; // 7์ผ

    // 1. ํ† ํฐ ์ƒ์„ฑ
    public TokenPair generateTokenPair(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", userDetails.getUsername());
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));

        String accessToken = createToken(claims, ACCESS_TOKEN_VALIDITY);
        String refreshToken = createRefreshToken(userDetails.getUsername());

        return new TokenPair(accessToken, refreshToken);
    }

    private String createToken(Map<String, Object> claims, long validity) {
        return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(new Date(System.currentTimeMillis()))
            .setExpiration(new Date(System.currentTimeMillis() + validity))
            .signWith(SignatureAlgorithm.HS512, secretKey)
            .compact();
    }

    // 2. ํ† ํฐ ๊ฒ€์ฆ
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token);
                
            // ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๊ฒ€์ฆ
            return !claims.getBody()
                .getExpiration()
                .before(new Date());
                
        } catch (JwtException | IllegalArgumentException e) {
            throw new InvalidTokenException("Invalid JWT token");
        }
    }

    // 3. ํ† ํฐ์—์„œ ์ •๋ณด ์ถ”์ถœ
    public Authentication getAuthentication(String token) {
        Claims claims = extractClaims(token);
        
        Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get("roles").toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
                
        UserDetails principal = new User(
            claims.get("username").toString(), 
            "", 
            authorities);
            
        return new UsernamePasswordAuthenticationToken(
            principal, 
            "", 
            authorities);
    }
}

2. ํ† ํฐ ๋ณด์•ˆ ๊ฐ•ํ™”

@Component
public class TokenSecurityEnhancer {

    private final RedisTemplate<String, String> redisTemplate;

    // 1. ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ๊ด€๋ฆฌ
    public class TokenBlacklistManager {
        
        public void blacklistToken(String token, Claims claims) {
            long expirationTime = 
                claims.getExpiration().getTime() - 
                System.currentTimeMillis();
                
            redisTemplate.opsForValue().set(
                "blacklist:" + token,
                "blocked",
                expirationTime,
                TimeUnit.MILLISECONDS
            );
        }

        public boolean isBlacklisted(String token) {
            return Boolean.TRUE.equals(redisTemplate.hasKey(
                "blacklist:" + token));
        }
    }

    // 2. ํ† ํฐ Rotation
    public class TokenRotationManager {
        
        public TokenPair rotateTokens(String refreshToken) {
            Claims claims = validateRefreshToken(refreshToken);
            
            // ์ด์ „ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ฌดํšจํ™”
            invalidateRefreshToken(refreshToken);
            
            // ์ƒˆ๋กœ์šด ํ† ํฐ ์Œ ์ƒ์„ฑ
            return generateNewTokenPair(claims.getSubject());
        }

        private void invalidateRefreshToken(String refreshToken) {
            redisTemplate.delete("refresh_token:" + refreshToken);
        }
    }

    // 3. ํ† ํฐ ์ง€๋ฌธ(Fingerprint)
    public class TokenFingerprintManager {
        
        public TokenWithFingerprint generateTokenWithFingerprint(
            UserDetails user) {
            
            String fingerprint = generateSecureFingerprint();
            String fingerprintHash = hashFingerprint(fingerprint);
            
            // ํ† ํฐ์— ์ง€๋ฌธ ํ•ด์‹œ ํฌํ•จ
            Map<String, Object> claims = new HashMap<>();
            claims.put("fph", fingerprintHash);
            
            String token = generateToken(claims);
            
            return new TokenWithFingerprint(token, fingerprint);
        }

        public boolean verifyFingerprint(
            String token, 
            String providedFingerprint) {
            
            Claims claims = extractClaims(token);
            String storedHash = claims.get("fph", String.class);
            String providedHash = hashFingerprint(providedFingerprint);
            
            return storedHash.equals(providedHash);
        }
    }
}

3. ๋ณด์•ˆ ํ•„ํ„ฐ ๊ตฌํ˜„

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenService tokenService;
    private final TokenSecurityEnhancer securityEnhancer;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain) throws ServletException, IOException {
        
        try {
            String token = extractToken(request);
            
            if (token != null && tokenService.validateToken(token)) {
                // ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ํ™•์ธ
                if (securityEnhancer.isBlacklisted(token)) {
                    throw new InvalidTokenException("Token is blacklisted");
                }
                
                // ํ† ํฐ ์ง€๋ฌธ ํ™•์ธ
                String fingerprint = extractFingerprint(request);
                if (!securityEnhancer.verifyFingerprint(token, fingerprint)) {
                    throw new InvalidTokenException(
                        "Invalid token fingerprint");
                }
                
                // ์ธ์ฆ ์ •๋ณด ์„ค์ •
                Authentication auth = tokenService.getAuthentication(token);
                SecurityContextHolder.getContext()
                    .setAuthentication(auth);
            }
            
        } catch (JwtException e) {
            SecurityContextHolder.clearContext();
            response.sendError(
                HttpServletResponse.SC_UNAUTHORIZED, 
                e.getMessage());
            return;
        }
        
        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && 
            bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

4. ํ† ํฐ ๊ฐฑ์‹  ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

@RestController
@RequestMapping("/api/auth")
public class TokenController {

    private final JwtTokenService tokenService;
    private final TokenSecurityEnhancer securityEnhancer;

    // 1. ํ† ํฐ ๊ฐฑ์‹ 
    @PostMapping("/refresh")
    public TokenResponse refreshToken(
        @RequestBody RefreshTokenRequest request) {
        
        try {
            // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๊ฒ€์ฆ
            if (!tokenService.validateRefreshToken(
                request.getRefreshToken())) {
                throw new InvalidTokenException(
                    "Invalid refresh token");
            }
            
            // ์ƒˆ๋กœ์šด ํ† ํฐ ์Œ ์ƒ์„ฑ
            TokenPair newTokens = securityEnhancer
                .rotateTokens(request.getRefreshToken());
                
            return new TokenResponse(newTokens);
            
        } catch (Exception e) {
            throw new TokenRefreshException(
                "Failed to refresh token", e);
        }
    }

    // 2. ํ† ํฐ ๋ฌดํšจํ™”
    @PostMapping("/logout")
    public ResponseEntity<?> logout(
        @RequestHeader("Authorization") String token) {
        
        try {
            String actualToken = token.substring(7);  // "Bearer " ์ œ๊ฑฐ
            
            // ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€
            Claims claims = tokenService.extractClaims(actualToken);
            securityEnhancer.blacklistToken(actualToken, claims);
            
            // ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ฌดํšจํ™”
            securityEnhancer.invalidateRefreshToken(
                claims.getSubject());
                
            return ResponseEntity.ok()
                .body("Successfully logged out");
                
        } catch (Exception e) {
            return ResponseEntity.status(
                HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Logout failed");
        }
    }
}

์ด๋Ÿฌํ•œ JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด:

  1. ํ† ํฐ ๊ด€๋ฆฌ

    • ์•ˆ์ „ํ•œ ํ† ํฐ ์ƒ์„ฑ
    • ์œ ํšจ์„ฑ ๊ฒ€์ฆ
    • ์ •๋ณด ์ถ”์ถœ
  2. ๋ณด์•ˆ ๊ฐ•ํ™”

    • ํ† ํฐ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ
    • ํ† ํฐ ํšŒ์ „
    • ํ† ํฐ ์ง€๋ฌธ
  3. ์ธ์ฆ ํ•„ํ„ฐ

    • ์š”์ฒญ ์ธ์ฆ
    • ํ† ํฐ ๊ฒ€์ฆ
    • ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ
  4. ํ† ํฐ ๊ฐฑ์‹ 

    • ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ฒ˜๋ฆฌ
    • ์•ˆ์ „ํ•œ ๋กœ๊ทธ์•„์›ƒ
    • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉด์ ‘๊ด€: Refresh Token์„ ์‚ฌ์šฉํ•  ๋•Œ์˜ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?

Refresh Token ๋ณด์•ˆ ์ „๋žต

@Service
public class RefreshTokenSecurityService {

    // 1. Refresh Token ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ
    @Service
    public class RefreshTokenStore {
        private final RedisTemplate<String, RefreshTokenData> redisTemplate;
        private final UserDeviceRepository deviceRepository;

        public void storeRefreshToken(String userId, 
                                    RefreshTokenData tokenData) {
            String key = generateRefreshTokenKey(userId, tokenData.getDeviceId());
            
            // ๋””๋ฐ”์ด์Šค๋ณ„ ํ† ํฐ ๊ด€๋ฆฌ
            redisTemplate.opsForHash()
                .put("refresh_tokens:" + userId,
                     tokenData.getDeviceId(),
                     tokenData);

            // ํ† ํฐ ๋งŒ๋ฃŒ ์„ค์ •
            redisTemplate.expire(
                "refresh_tokens:" + userId,
                30,
                TimeUnit.DAYS
            );

            // ๋””๋ฐ”์ด์Šค ์ •๋ณด ์ €์žฅ
            saveDeviceInfo(userId, tokenData.getDeviceInfo());
        }

        public void validateDeviceAndToken(String userId, 
                                         String refreshToken,
                                         DeviceInfo currentDevice) {
            RefreshTokenData storedToken = getStoredToken(userId, 
                currentDevice.getDeviceId());

            if (storedToken == null || 
                !storedToken.getToken().equals(refreshToken)) {
                throw new InvalidRefreshTokenException(
                    "Invalid refresh token for device");
            }

            // ๋””๋ฐ”์ด์Šค ์ง€๋ฌธ ๊ฒ€์ฆ
            if (!verifyDeviceFingerprint(
                storedToken.getDeviceInfo(), 
                currentDevice)) {
                throw new DeviceMismatchException(
                    "Device fingerprint mismatch");
            }
        }
    }

    // 2. ํšŒ์ „(Rotation) ์ •์ฑ… ๊ตฌํ˜„
    @Service
    public class TokenRotationPolicy {
        private final RefreshTokenStore tokenStore;
        private final SecurityEventPublisher eventPublisher;

        public TokenPair rotateTokens(String userId, 
                                    String currentRefreshToken,
                                    DeviceInfo deviceInfo) {
            // ํ˜„์žฌ ํ† ํฐ ๊ฒ€์ฆ
            tokenStore.validateDeviceAndToken(
                userId, 
                currentRefreshToken, 
                deviceInfo);

            // ์žฌ์‚ฌ์šฉ ๊ฐ์ง€
            if (isTokenReused(userId, currentRefreshToken)) {
                handleTokenReuse(userId);
                throw new TokenReuseException(
                    "Refresh token reuse detected");
            }

            // ์ƒˆ ํ† ํฐ ์Œ ์ƒ์„ฑ
            TokenPair newTokens = generateNewTokenPair(userId);

            // Refresh Token ์ €์žฅ
            tokenStore.storeRefreshToken(userId, 
                RefreshTokenData.builder()
                    .token(newTokens.getRefreshToken())
                    .deviceId(deviceInfo.getDeviceId())
                    .deviceInfo(deviceInfo)
                    .issuedAt(Instant.now())
                    .build()
            );

            // ์ด์ „ ํ† ํฐ ๋ฌดํšจํ™” ์ด๋ ฅ ์ €์žฅ
            markTokenAsUsed(currentRefreshToken);

            return newTokens;
        }

        private boolean isTokenReused(String userId, String refreshToken) {
            return redisTemplate.opsForSet()
                .isMember("used_tokens:" + userId, refreshToken);
        }

        private void handleTokenReuse(String userId) {
            // ๋ชจ๋“  ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ฌดํšจํ™”
            tokenStore.revokeAllTokens(userId);
            
            // ๋ณด์•ˆ ์ด๋ฒคํŠธ ๋ฐœํ–‰
            eventPublisher.publishSecurityEvent(
                new TokenReuseEvent(userId));
        }
    }

    // 3. ํ† ํฐ ์ถ”์  ๋ฐ ๊ฐ์‚ฌ
    @Service
    public class TokenAuditService {
        private final AuditEventRepository auditRepository;

        @Async
        public void auditTokenUsage(TokenUsageEvent event) {
            AuditEvent auditEvent = AuditEvent.builder()
                .userId(event.getUserId())
                .deviceId(event.getDeviceInfo().getDeviceId())
                .action(event.getAction())
                .tokenId(event.getTokenId())
                .timestamp(Instant.now())
                .ipAddress(event.getIpAddress())
                .userAgent(event.getUserAgent())
                .build();

            auditRepository.save(auditEvent);
        }

        public void detectSuspiciousActivity(String userId) {
            List<AuditEvent> recentEvents = 
                auditRepository.findRecentByUserId(
                    userId, 
                    Duration.ofHours(24)
                );

            // ์˜์‹ฌ์Šค๋Ÿฌ์šด ํŒจํ„ด ๋ถ„์„
            if (hasMultipleDeviceAccess(recentEvents) || 
                hasUnusualGeographicalAccess(recentEvents)) {
                notifySecurityTeam(userId, recentEvents);
            }
        }
    }

    // 4. ์ž๋™ ํ† ํฐ ์ •๋ฆฌ
    @Service
    public class TokenCleanupService {
        
        @Scheduled(cron = "0 0 * * * *") // ๋งค์‹œ๊ฐ„ ์‹คํ–‰
        public void cleanupExpiredTokens() {
            // ๋งŒ๋ฃŒ๋œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์‚ญ์ œ
            Set<String> expiredUserIds = findUsersWithExpiredTokens();
            
            for (String userId : expiredUserIds) {
                cleanupUserTokens(userId);
            }
        }

        @Scheduled(cron = "0 0 0 * * *") // ๋งค์ผ ์‹คํ–‰
        public void cleanupUnusedTokens() {
            // 30์ผ ์ด์ƒ ๋ฏธ์‚ฌ์šฉ ํ† ํฐ ์ •๋ฆฌ
            LocalDateTime cutoffDate = 
                LocalDateTime.now().minusDays(30);
                
            List<RefreshTokenData> unusedTokens = 
                findUnusedTokens(cutoffDate);
                
            for (RefreshTokenData token : unusedTokens) {
                revokeToken(token);
                notifyUser(token.getUserId(), 
                    "Unused refresh token revoked");
            }
        }
    }
}

์ด๋Ÿฌํ•œ Refresh Token ๋ณด์•ˆ ์ „๋žต์„ ํ†ตํ•ด:

  1. ์•ˆ์ „ํ•œ ํ† ํฐ ์ €์žฅ

    • Redis ๊ธฐ๋ฐ˜ ํ† ํฐ ์ €์žฅ
    • ๋””๋ฐ”์ด์Šค๋ณ„ ํ† ํฐ ๊ด€๋ฆฌ
    • ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •
  2. ํ† ํฐ ํšŒ์ „ ์ •์ฑ…

    • ์žฌ์‚ฌ์šฉ ๊ฐ์ง€
    • ์ž๋™ ๋ฌดํšจํ™”
    • ๋””๋ฐ”์ด์Šค ๊ฒ€์ฆ
  3. ๊ฐ์‚ฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

    • ์‚ฌ์šฉ ๊ธฐ๋ก ์ถ”์ 
    • ์˜์‹ฌ ํ™œ๋™ ๊ฐ์ง€
    • ๋ณด์•ˆ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
  4. ์ž๋™ํ™”๋œ ๊ด€๋ฆฌ

    • ๋งŒ๋ฃŒ ํ† ํฐ ์ •๋ฆฌ
    • ๋ฏธ์‚ฌ์šฉ ํ† ํฐ ์ œ๊ฑฐ
    • ์‚ฌ์šฉ์ž ์•Œ๋ฆผ

ํŠนํžˆ ์ค‘์š”ํ•œ ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ:

  • ๋””๋ฐ”์ด์Šค ๋ฐ”์ธ๋”ฉ
  • ํ† ํฐ ์žฌ์‚ฌ์šฉ ๋ฐฉ์ง€
  • ์ด์ƒ ์ง•ํ›„ ๊ฐ์ง€
  • ์ž๋™ ํด๋ฆฐ์—…

์ด๋ฅผ ํ†ตํ•ด Refresh Token์˜ ์•ˆ์ „ํ•œ ๊ด€๋ฆฌ์™€ ๋ณด์•ˆ ์œ„ํ˜‘ ๋Œ€์‘์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

JWT ํ† ํฐ์˜ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด์•ˆ ์ „๋žต

1. ํ† ํฐ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ

// TokenService.ts
export class TokenService {
    private static readonly ACCESS_TOKEN_KEY = 'access_token';
    private static readonly REFRESH_TOKEN_KEY = 'refresh_token';
    
    // 1. ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์•ก์„ธ์Šค ํ† ํฐ ๊ด€๋ฆฌ
    private static accessToken: string | null = null;

    // 2. HttpOnly ์ฟ ํ‚ค๋ฅผ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค
    public static storeTokens(tokens: TokenResponse) {
        // Access Token์€ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ
        this.accessToken = tokens.accessToken;
        
        // Refresh Token์€ HttpOnly ์ฟ ํ‚ค๋กœ ์ €์žฅ (์„œ๋ฒ„์— ์š”์ฒญ)
        this.storeRefreshToken(tokens.refreshToken);
    }

    private static storeRefreshToken(refreshToken: string) {
        // ์„œ๋ฒ„์— ์ฟ ํ‚ค ์„ค์ • ์š”์ฒญ
        axios.post('/api/auth/cookie', { refreshToken }, {
            withCredentials: true  // CORS ์„ค์ • ํ•„์š”
        });
    }

    // 3. ํ† ํฐ ์•”ํ˜ธํ™” ์ €์žฅ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
    private static encryptToken(token: string): string {
        const encoder = new TextEncoder();
        const data = encoder.encode(token);
        return window.btoa(String.fromCharCode(...new Uint8Array(data)));
    }
}

2. HTTP ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ

// ApiInterceptor.ts
export class ApiInterceptor {
    private static instance: AxiosInstance;

    public static setup() {
        this.instance = axios.create({
            baseURL: process.env.API_BASE_URL,
            timeout: 10000,
            withCredentials: true
        });

        this.setupInterceptors();
    }

    private static setupInterceptors() {
        // 1. ์š”์ฒญ ์ธํ„ฐ์…‰ํ„ฐ
        this.instance.interceptors.request.use(
            async config => {
                const token = TokenService.getAccessToken();
                if (token) {
                    config.headers.Authorization = `Bearer ${token}`;
                }
                
                // CSRF ํ† ํฐ ์ถ”๊ฐ€
                const csrfToken = CsrfTokenService.getToken();
                if (csrfToken) {
                    config.headers['X-CSRF-Token'] = csrfToken;
                }
                
                return config;
            },
            error => Promise.reject(error)
        );

        // 2. ์‘๋‹ต ์ธํ„ฐ์…‰ํ„ฐ
        this.instance.interceptors.response.use(
            response => response,
            async error => {
                const originalRequest = error.config;

                if (error.response?.status === 401 && 
                    !originalRequest._retry) {
                    originalRequest._retry = true;

                    try {
                        // ํ† ํฐ ๊ฐฑ์‹  ์‹œ๋„
                        const newTokens = 
                            await TokenService.refreshTokens();
                            
                        TokenService.storeTokens(newTokens);
                        
                        // ์›๋ž˜ ์š”์ฒญ ์žฌ์‹œ๋„
                        return this.instance(originalRequest);
                    } catch (refreshError) {
                        // ๋ฆฌํ”„๋ ˆ์‹œ ์‹คํŒจ ์‹œ ๋กœ๊ทธ์•„์›ƒ
                        await AuthService.logout();
                        return Promise.reject(refreshError);
                    }
                }

                return Promise.reject(error);
            }
        );
    }
}

3. ๋ณด์•ˆ ์œ ํ‹ธ๋ฆฌํ‹ฐ

// SecurityUtils.ts
export class SecurityUtils {
    // 1. XSS ๋ฐฉ์ง€
    public static sanitizeInput(input: string): string {
        const div = document.createElement('div');
        div.textContent = input;
        return div.innerHTML;
    }

    // 2. ๋””๋ฐ”์ด์Šค ์ง€๋ฌธ
    public static async generateDeviceFingerprint(): Promise<string> {
        const components = [
            navigator.userAgent,
            navigator.language,
            new Date().getTimezoneOffset(),
            screen.width,
            screen.height,
            navigator.hardwareConcurrency,
            // Canvas ์ง€๋ฌธ
            await this.getCanvasFingerprint(),
            // WebGL ์ง€๋ฌธ
            await this.getWebGLFingerprint()
        ];

        // ํ•ด์‹œ ์ƒ์„ฑ
        const fingerprint = await crypto.subtle.digest(
            'SHA-256',
            new TextEncoder().encode(components.join(''))
        );

        return Array.from(new Uint8Array(fingerprint))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }

    // 3. CSRF ๋ณดํ˜ธ
    public static setupCsrfProtection() {
        const token = document.querySelector(
            'meta[name="csrf-token"]'
        )?.getAttribute('content');

        if (token) {
            CsrfTokenService.setToken(token);
        }
    }
}

4. ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ (์˜ˆ: React Context ์‚ฌ์šฉ)

// AuthContext.tsx
interface AuthContextType {
    isAuthenticated: boolean;
    user: User | null;
    login: (credentials: LoginCredentials) => Promise<void>;
    logout: () => Promise<void>;
    refreshAuth: () => Promise<void>;
}

export const AuthContext = createContext<AuthContextType | null>(null);

export const AuthProvider: React.FC = ({ children }) => {
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [user, setUser] = useState<User | null>(null);

    // 1. ์ธ์ฆ ์ƒํƒœ ์ดˆ๊ธฐํ™”
    useEffect(() => {
        const initializeAuth = async () => {
            try {
                const token = TokenService.getAccessToken();
                if (token) {
                    await refreshAuth();
                }
            } catch (error) {
                await logout();
            }
        };

        initializeAuth();
    }, []);

    // 2. ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
    const login = async (credentials: LoginCredentials) => {
        try {
            // ๋””๋ฐ”์ด์Šค ์ง€๋ฌธ ์ƒ์„ฑ
            const deviceFingerprint = 
                await SecurityUtils.generateDeviceFingerprint();

            const response = await axios.post('/api/auth/login', {
                ...credentials,
                deviceFingerprint
            });

            TokenService.storeTokens(response.data.tokens);
            setUser(response.data.user);
            setIsAuthenticated(true);

        } catch (error) {
            console.error('Login failed:', error);
            throw error;
        }
    };

    // 3. ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
    const logout = async () => {
        try {
            await axios.post('/api/auth/logout');
        } finally {
            TokenService.clearTokens();
            setUser(null);
            setIsAuthenticated(false);
        }
    };

    // 4. ์ž๋™ ํ† ํฐ ๊ฐฑ์‹ 
    const refreshAuth = async () => {
        try {
            const deviceFingerprint = 
                await SecurityUtils.generateDeviceFingerprint();
                
            const response = await axios.post('/api/auth/refresh', {
                deviceFingerprint
            }, {
                withCredentials: true
            });

            TokenService.storeTokens(response.data.tokens);
            setUser(response.data.user);
            setIsAuthenticated(true);
        } catch (error) {
            throw error;
        }
    };

    return (
        <AuthContext.Provider value={{
            isAuthenticated,
            user,
            login,
            logout,
            refreshAuth
        }}>
            {children}
        </AuthContext.Provider>
    );
};

์ด๋Ÿฌํ•œ ํด๋ผ์ด์–ธํŠธ ์ธก ๋ณด์•ˆ ์ „๋žต์„ ํ†ตํ•ด:

  1. ํ† ํฐ ๋ณด์•ˆ ์ €์žฅ

    • Access Token์€ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ
    • Refresh Token์€ HttpOnly ์ฟ ํ‚ค๋กœ ๊ด€๋ฆฌ
    • ํ•„์š”์‹œ ์ถ”๊ฐ€ ์•”ํ˜ธํ™”
  2. ์š”์ฒญ/์‘๋‹ต ๋ณด์•ˆ

    • ์ž๋™ ํ† ํฐ ๊ฐฑ์‹ 
    • CSRF ๋ณดํ˜ธ
    • ์—๋Ÿฌ ์ฒ˜๋ฆฌ
  3. ๋””๋ฐ”์ด์Šค ๋ณด์•ˆ

    • ๋””๋ฐ”์ด์Šค ์ง€๋ฌธ ์ƒ์„ฑ
    • XSS ๋ฐฉ์ง€
    • ์ž…๋ ฅ ๊ฒ€์ฆ
  4. ์ƒํƒœ ๊ด€๋ฆฌ

    • ์•ˆ์ „ํ•œ ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ
    • ์ž๋™ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ
    • ํ† ํฐ ๋งŒ๋ฃŒ ์ฒ˜๋ฆฌ

๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.