JWT 인증

odada·2025년 1월 20일
0

JWT 인증 구현 가이드 (React + Spring Boot)

JWT 샘플

0. JWT 인증 흐름도

sequenceDiagram
    participant Client as React Client
    participant API as Spring Boot API
    participant DB as Database

    %% 로그인 프로세스
    rect rgb(191, 223, 255)
    Note over Client,DB: 로그인 프로세스
    Client->>+API: 1. POST /api/auth/login (email, password)
    API->>DB: 2. 사용자 검증
    DB-->>API: 3. 사용자 정보 반환
    API->>API: 4. JWT 토큰 생성 (Access + Refresh)
    API-->>-Client: 5. JWT 토큰 반환
    Client->>Client: 6. 토큰 저장 (localStorage/Cookie)
    end

    %% API 요청 프로세스
    rect rgb(200, 255, 200)
    Note over Client,DB: 인증된 API 요청
    Client->>+API: 7. API 요청 + Authorization 헤더
    API->>API: 8. JWT 토큰 검증
    API->>DB: 9. 데이터 요청
    DB-->>API: 10. 데이터 반환
    API-->>-Client: 11. 응답 데이터 반환
    end

    %% 토큰 갱신 프로세스
    rect rgb(255, 228, 191)
    Note over Client,DB: 토큰 갱신 프로세스
    Client->>+API: 12. API 요청 (만료된 Access Token)
    API-->>Client: 13. 401 Unauthorized
    Client->>API: 14. POST /api/auth/refresh (Refresh Token)
    API->>API: 15. Refresh Token 검증
    API-->>-Client: 16. 새로운 Access Token 발급
    Client->>Client: 17. 새 Access Token 저장
    end

1. JWT(JSON Web Token) 개요

JWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준(RFC 7519)입니다.

1.1 JWT의 구조

  • Header: 토큰 유형과 사용된 알고리즘 정보
  • Payload: 실제 전달할 데이터 (클레임)
  • Signature: 토큰의 유효성 검증을 위한 서명
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2. 프론트엔드-백엔드 간 JWT 흐름

2.1 인증 프로세스

  1. 로그인 요청 (프론트엔드 → 백엔드)

    // React (프론트엔드)
    const login = async (email, password) => {
      try {
        const response = await axios.post('/api/auth/login', {
          email,
          password
        });
        const { accessToken, refreshToken } = response.data;
        // 토큰 저장
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', refreshToken);
      } catch (error) {
        console.error('Login failed:', error);
      }
    };
  2. 토큰 생성 (백엔드)

    // Spring Boot (백엔드)
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
      // 사용자 인증
      Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(
          loginRequest.getEmail(),
          loginRequest.getPassword()
        )
      );
    
      // JWT 토큰 생성
      String accessToken = jwtUtils.generateAccessToken(authentication);
      String refreshToken = jwtUtils.generateRefreshToken(authentication);
    
      return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
    }

2.2 인증된 요청 처리

  1. API 요청 시 토큰 포함 (프론트엔드)

    // Axios Interceptor 설정
    axios.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('accessToken');
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
  2. 토큰 검증 (백엔드)

    @Component
    public class JwtAuthenticationFilter extends OncePerRequestFilter {
      @Override
      protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) {
        try {
          String jwt = parseJwt(request);
          if (jwt != null && jwtUtils.validateToken(jwt)) {
            String username = jwtUtils.getUserNameFromToken(jwt);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication =
              new UsernamePasswordAuthenticationToken(userDetails,
                                                    null,
                                                    userDetails.getAuthorities());
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
          }
        } catch (Exception e) {
          logger.error("Cannot set user authentication: {}", e);
        }
        
        filterChain.doFilter(request, response);
      }
    }

2.3 토큰 갱신 프로세스

  1. 토큰 만료 확인 (프론트엔드)

    axios.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;
        
        if (error.response.status === 401 && !originalRequest._retry) {
          originalRequest._retry = true;
          
          try {
            const refreshToken = localStorage.getItem('refreshToken');
            const response = await axios.post('/api/auth/refresh', { refreshToken });
            const { accessToken } = response.data;
            
            localStorage.setItem('accessToken', accessToken);
            originalRequest.headers.Authorization = `Bearer ${accessToken}`;
            
            return axios(originalRequest);
          } catch (error) {
            // 리프레시 토큰도 만료된 경우
            localStorage.removeItem('accessToken');
            localStorage.removeItem('refreshToken');
            window.location.href = '/login';
          }
        }
        
        return Promise.reject(error);
      }
    );
  2. 리프레시 토큰 처리 (백엔드)

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
      String requestRefreshToken = request.getRefreshToken();
      
      return refreshTokenService.findByToken(requestRefreshToken)
        .map(refreshToken -> {
          if (jwtUtils.validateRefreshToken(refreshToken.getToken())) {
            String username = refreshToken.getUser().getUsername();
            String newAccessToken = jwtUtils.generateAccessToken(username);
            
            return ResponseEntity.ok(new TokenRefreshResponse(newAccessToken));
          } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
          }
        })
        .orElse(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
    }

3. 보안 고려사항

3.1 토큰 저장

  • AccessToken은 짧은 만료 시간 설정 (예: 15분~1시간)
  • RefreshToken은 긴 만료 시간 설정 (예: 2주~1달)
  • 민감한 정보는 JWT Payload에 포함하지 않음
  • HTTPS 사용 필수

3.2 XSS & CSRF 대응

  • HttpOnly 쿠키 사용 고려
  • API 요청 시 CSRF 토큰 포함
  • 입력값 검증 및 이스케이프 처리

4. 구현 시 협의사항

4.1 프론트엔드-백엔드 협의 필요 사항

  1. 토큰 관련

    • Access Token 만료 시간
    • Refresh Token 사용 여부 및 만료 시간
    • 토큰 저장 방식 (localStorage vs Cookie)
  2. API 엔드포인트

    • 로그인: /api/auth/login
    • 토큰 갱신: /api/auth/refresh
    • 로그아웃: /api/auth/logout
  3. 응답 형식

    // 로그인 성공 응답
    {
      "accessToken": "eyJhbGc...",
      "refreshToken": "eyJhbGc...",
      "tokenType": "Bearer",
      "expiresIn": 3600 // 선택사항
    }
    
    // 에러 응답
    {
      "error": "invalid_credentials",
      "message": "Invalid email or password",
      "status": 401
    }
  4. 헤더 형식

    Authorization: Bearer {token}
    Content-Type: application/json

5. 테스트 시나리오

  1. 정상 로그인/로그아웃

    • 올바른 자격증명으로 로그인
    • 토큰 발급 확인
    • 보호된 리소스 접근
    • 로그아웃 및 토큰 삭제
  2. 토큰 만료

    • Access Token 만료 시나리오
    • Refresh Token을 이용한 갱신
    • Refresh Token 만료 시나리오
  3. 에러 처리

    • 잘못된 자격증명
    • 유효하지 않은 토큰
    • 만료된 토큰
    • 서버 에러

이 문서는 JWT 기반 인증 구현을 위한 기본 가이드라인입니다. 실제 구현 시에는 프로젝트의 요구사항과 보안 정책에 따라 적절히 수정하여 사용해주세요.

0개의 댓글