BackEND/Java

Spring Security - API 경로별 인증 방식 설정 (JWT + Basic Auth 혼합 적용)

mingmingIT 2025. 4. 19. 10:53

Spring Boot 프로젝트에서 전체 API는 Basic 인증을 사용하면서, 특정 API (/api/token/**)만 Bearer 토큰(JWT 등)을 사용하는 구조를 어떻게 구현할 수 있을까요?

Spring Security FilterChain을 분리하여 특정 API에만 토큰 인증을 적용하는 방법을 단계별로 설명 드리겠습니다.


1. 목표

  • /api/token/** 경로: Bearer 토큰(JWT) 인증 적용
  • 그 외 모든 경로: Basic Authentication 적용

2. 프로젝트 환경

  • Java 11
  • Spring Boot 3.x
  • Spring Security 6.x 이상
  • JWT 직접 구현 or 라이브러리(JJWT 등) 사용 가능

3. 디렉토리 구성 예시

src/
 └─ main/
     └─ java/
         └─ com.example.demo/
             ├─ config/
             │   └─ SecurityConfig.java
             ├─ filter/
             │   └─ JwtAuthenticationFilter.java
             ├─ util/
             │   └─ JwtTokenUtil.java
             └─ controller/
                 └─ TokenApiController.java

4. Security 설정 – FilterChain 분리

Spring Security 6에서는 SecurityFilterChain을 여러 개 선언하고 @Order를 사용해 우선순위를 지정할 수 있습니다.

✅ SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // [1] JWT 인증이 적용될 FilterChain
    @Bean
    @Order(1)
    public SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/token/**") // 이 경로에만 적용
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new JwtAuthenticationEntryPoint())
            );

        return http.build();
    }

    // [2] 나머지 경로는 Basic Auth 사용
    @Bean
    @Order(2)
    public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}

securityMatcher()는 해당 FilterChain이 적용될 URL 경로를 지정합니다. 경로 외 요청은 다음 FilterChain으로 넘어갑니다.


5. JwtAuthenticationFilter 구현

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String token = extractToken(request);

        if (token != null && JwtTokenUtil.validateToken(token)) {
            Authentication auth = JwtTokenUtil.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        filterChain.doFilter(request, response);
    }

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

6. JwtTokenUtil 유틸 (간단 예시)

public class JwtTokenUtil {

    private static final String SECRET = "mySecretKey";

    public static boolean validateToken(String token) {
        // 실제로는 서명 검증, 만료 체크 등 포함
        return true;
    }

    public static Authentication getAuthentication(String token) {
        // 사용자 정보를 token에서 파싱 후 Authentication 객체 생성
        UserDetails userDetails = User.withUsername("jwtUser").password("").roles("USER").build();
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

7. JWT 인증 실패 시 처리 (Optional)

public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("Unauthorized - Invalid or missing token");
    }
}

8. 테스트용 컨트롤러 예시

@RestController
@RequestMapping("/api/token")
public class TokenApiController {

    @GetMapping("/secure-data")
    public ResponseEntity<String> secureData() {
        return ResponseEntity.ok("JWT 인증에 성공한 사용자만 접근 가능!");
    }
}

✅ 9. 결과 확인

요청 경로 인증 방식
/api/token/secure-data Bearer Token (JWT)
/api/other Basic Auth

마무리 정리

  • Spring Security는 여러 개의 FilterChain을 선언하여 인증 방식을 분리할 수 있습니다.
  • securityMatcher()를 사용하여 특정 경로에만 JWT 적용이 가능하며, 나머지는 기본 방식(Basic Auth)을 유지할 수 있습니다.
  • 실제 JWT 검증은 JwtTokenUtil에 구현해두고, 서명 검증, 만료 체크 등을 추가해야 합니다.