본문 바로가기
스프링

스프링 시큐리티 - JWT와 유저 인증 절차

by 개발자 포비 2024. 12. 25.

Spring Security와 JWT 인증 구현 상세 가이드

1. JWT (JSON Web Token) 개요

1.1 특징

  • 자가수용적(Self-contained): 필요한 모든 정보를 토큰 자체에 포함
  • Stateless: 서버에 상태 저장 불필요
  • Base64URL 인코딩된 문자열 형태

1.2 장점

  • 서버 확장성 증가: 세션 저장/관리 불필요
  • 다중 서버 환경에서 효율적
  • 모바일 애플리케이션 통합 용이
  • CORS 문제 해결 용이

1.3 단점

  • 토큰 크기: 모든 요청에 추가 overhead
  • 저장된 정보 변경 불가: 토큰 재발급 필요
  • 토큰 탈취 시 즉각 무효화 어려움

2. 구현 상세

2.1 JWT 구조 및 생성 (JwtProvider)

@Component
public class JwtProvider {
    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private final long tokenValidityInMilliseconds = 3600000; // 1시간

    public String createToken(String email, String role) {
        Claims claims = Jwts.claims().setSubject(email);
        claims.put("role", role);  // 사용자 권한 정보 저장

        Date now = new Date();
        Date validity = new Date(now.getTime() + tokenValidityInMilliseconds);

        return Jwts.builder()
                .setClaims(claims)      // 사용자 정보
                .setIssuedAt(now)       // 발급 시간
                .setExpiration(validity) // 만료 시간
                .signWith(key)          // 비밀키로 서명
                .compact();             // 토큰 생성
    }

    // 토큰으로부터 인증 정보 추출
    public Authentication getAuthentication(String token) {
        Claims claims = extractClaims(token);

        Collection<? extends GrantedAuthority> authorities =
                Collections.singleton(new SimpleGrantedAuthority(claims.get("role").toString()));

        UserDetails principal = new User(claims.getSubject(), "", authorities);
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    // 토큰 유효성 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;  // 서명 불일치, 만료, 형식 오류 등
        }
    }
}

2.2 인증 필터 동작 원리 (JwtAuthenticationFilter)

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);  // Authorization 헤더에서 토큰 추출

        // 토큰 검증 및 인증 처리
        if (token != null && jwtProvider.validateToken(token)) {
            Authentication auth = jwtProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
            // 인증 정보를 SecurityContext에 저장하여 이후 필터에서 인증됨을 확인
        }
        // 토큰이 없거나 유효하지 않으면 SecurityContext는 비어있음
        // -> 인증이 필요한 경로 접근 시 예외 발생

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);  // "Bearer " 제거
        }
        return null;
    }
}

2.3 Security 설정 상세 (SecurityConfig)

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtProvider jwtProvider;
    private final CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)  // JWT는 CSRF 공격에 안전
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))  // 세션 미사용
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()  // 인증 없이 접근 가능
                .requestMatchers("/api/v1/**").authenticated())   // 인증 필요
            .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), 
                UsernamePasswordAuthenticationFilter.class)  // JWT 필터 추가
            .authenticationProvider(authenticationProvider());  // 로그인 시 사용될 Provider

        return http.build();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);  // 사용자 정보 조회
        authProvider.setPasswordEncoder(passwordEncoder());      // 비밀번호 검증
        return authProvider;
    }
}

3. 상세 인증 프로세스

3.1 로그인 프로세스

1.로그인 요청 (/api/v1/auth/login)

   {
     "username": "user@example.com",
     "password": "password123"
   }

2.CustomUserDetailsService에서 사용자 조회

  • DB에서 사용자 정보 조회
  • UserDetails 객체로 변환

3.DaoAuthenticationProvider

  • 비밀번호 검증 (PasswordEncoder 사용)
  • 인증 성공/실패 결정

4.JWT 토큰 생성

  • 사용자 정보, 권한 포함
  • 만료 시간 설정
  • 비밀키로 서명

5.응답 반환

   {
     "token": "eyJhbGciOiJIUzI1NiJ9..."
   }

3.2 인증된 요청 처리

1.JwtAuthenticationFilter

  • Authorization 헤더에서 토큰 추출
  • 토큰 유효성 검증
  • SecurityContext에 인증 정보 저장

2.Security Filter Chain

  • URL 패턴 매칭
  • 권한 검사
  • Controller 메소드 실행

3.3 토큰 없는 요청 처리

1.JwtAuthenticationFilter

  • 토큰 없음 확인
  • SecurityContext 비워짐

2.Security Filter Chain

  • 보호된 리소스 접근 시도
  • 인증 정보 없음 확인
  • 401 Unauthorized 응답

3.4 예외 처리

@RestControllerAdvice
public class AuthenticationExceptionHandler {
    @ExceptionHandler(BadCredentialsException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Result<?> handleBadCredentials() {
        return Result.failure("잘못된 인증 정보입니다.");
    }

    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public Result<?> handleAuthentication(AuthenticationException e) {
        return Result.failure("인증에 실패했습니다.");
    }
}

4. 보안 고려사항

4.1 토큰 관리

  • 적절한 만료 시간 설정
  • Refresh Token 구현 고려
  • 안전한 클라이언트 저장소 사용 (HttpOnly 쿠키 등)

4.2 비밀키 관리

  • 환경변수로 관리
  • 정기적인 키 교체
  • 적절한 키 길이 사용

4.3 추가 보안

  • HTTPS 사용 필수
  • XSS 공격 대비
  • 토큰 블랙리스트 구현 고려

5. 코드 연동 및 흐름

1.SecurityConfig에서 전체 보안 설정

  • JWT 필터 등록
  • URL 별 접근 권한 설정
  • 인증 Provider 설정

2.로그인 시

  • CustomUserDetailsService로 사용자 검증
  • JWT 토큰 발급

3.API 요청 시

  • JwtAuthenticationFilter에서 토큰 검증
  • SecurityContext에 인증 정보 저장
  • 컨트롤러에서 인증 정보 사용

이러한 구조로 Stateless하고 확장 가능한 인증 시스템이 구현되며, 각 컴포넌트는 단일 책임을 가지고 독립적으로 동작하면서도 전체적으로 유기적으로 연결됩니다.

'스프링' 카테고리의 다른 글

스프링 - JPA 다대일 처리  (0) 2024.12.22
스프링 - OSIV  (1) 2024.12.22
스프링 - N + 1 문제  (1) 2024.12.17
스프링 - REST API  (0) 2024.12.17
스프링 시큐리티 - 간단한 인증절차 정리  (0) 2024.12.16

댓글