Spring Boot API 보안: OAuth 2.0 & JWT로 안전하게 2026
Spring Boot REST API에 OAuth 2.0과 JWT를 적용하여 강력하고 안전한 인증 및 인가 시스템을 구축하는 방법을 분석합니다.
핵심 키워드: Spring Boot, OAuth 2.0, JWT 인증
목차
1. Spring Boot API 보안, 왜 중요할까요?
2. OAuth 2.0과 JWT, 깊이 있는 이해
3. Spring Boot에 OAuth 2.0 & JWT 적용
4. 흔한 보안 문제와 현명한 해결책
5. 실전! API 엔드포인트 보호 및 테스트
6. 성능과 확장성을 고려한 설계
7. 자주 묻는 질문 (FAQ)
1. Spring Boot API 보안, 왜 중요할까요?
안녕하세요, 권퓨터입니다! 2026년 현재, 디지털 세상은 그 어느 때보다 빠르게 변화하고 있으며, 수많은 서비스들이 REST API를 통해 서로 연결되고 있습니다. 이러한 흐름 속에서 Spring Boot API 보안은 더 이상 선택이 아닌 필수가 되었습니다. 특히 민감한 사용자 데이터와 비즈니스 로직을 다루는 백엔드 시스템에서는 OAuth 2.0과 JWT(JSON Web Token)를 활용한 강력한 인증 및 인가 시스템 구축이 무엇보다 중요합니다.
API는 현대 애플리케이션의 핵심 통신 수단이며, 모바일 앱, 웹 프론트엔드, 그리고 다른 마이크로서비스 간의 데이터 교환을 담당합니다. 만약 API 보안이 취약하다면, 외부 공격자가 쉽게 시스템에 침투하여 데이터를 탈취하거나, 악의적인 조작을 가할 수 있습니다. 이는 기업의 신뢰도 하락은 물론, 막대한 금전적 손실과 법적 문제로 이어질 수 있습니다.
과거에는 세션-쿠키 기반의 인증 방식이 널리 사용되었지만, 스테이트리스(Stateless) 특성을 가진 REST API와 마이크로서비스 아키텍처의 확산으로 인해 새로운 보안 패러다임이 요구되었습니다. 바로 이 지점에서 OAuth 2.0과 JWT가 강력한 대안으로 떠오르게 됩니다. 이 두 기술은 분산 환경에서도 효율적이고 안전하게 사용자를 인증하고, 리소스에 대한 접근 권한을 관리하는 데 최적화되어 있습니다.
이번 글에서는 2026년 최신 기준으로 Spring Boot API에 OAuth 2.0과 JWT를 적용하여 안전하고 견고한 인증 및 인가 시스템을 구축하는 방법에 대해 자세히 알아보겠습니다. 각 기술의 개념부터 Spring Boot 적용 예시, 그리고 실제 운영에서 발생할 수 있는 문제점과 해결책까지, 개발자들이 실질적으로 활용할 수 있는 깊이 있는 분석 리포트를 제공하고자 합니다.
핵심 포인트
2026년 현대 API 환경에서 Spring Boot API 보안은 필수이며, OAuth 2.0과 JWT는 분산 환경에 최적화된 강력한 보안 솔루션입니다.
2. OAuth 2.0과 JWT, 깊이 있는 이해
Spring Boot API 보안의 핵심 축인 OAuth 2.0과 JWT에 대해 자세히 살펴보겠습니다. 이 두 기술은 각각 다른 역할을 수행하며 상호 보완적으로 작동하여 강력한 보안 체계를 구축합니다.
2.1. OAuth 2.0: 권한 위임의 표준 프로토콜
OAuth 2.0은 “권한 위임(Delegated Authorization)”을 위한 개방형 표준 프로토콜입니다. 사용자의 비밀번호를 직접 공유하지 않고도, 특정 애플리케이션이 다른 서비스(예: Google, Facebook)의 리소스에 접근할 수 있도록 권한을 부여하는 방식을 정의합니다. 이는 사용자의 민감한 정보를 보호하면서도 서비스 간 연동을 가능하게 합니다.
주요 역할:
✓ 인증(Authentication)이 아닌 인가(Authorization): OAuth 2.0은 사용자가 누구인지 확인하는 인증 프로토콜이 아닙니다. 대신, 사용자가 특정 리소스에 대한 접근 권한을 다른 애플리케이션에 부여하는 과정을 관리합니다. 인증은 OpenID Connect와 같은 다른 프로토콜이 담당하며, OAuth 2.0 위에 구축될 수 있습니다.
✓ AccessToken 발급: 사용자가 권한을 위임하면, OAuth 2.0 서버는 AccessToken을 발급합니다. 이 토큰은 특정 리소스에 접근할 수 있는 권한을 나타내며, 유효 기간이 짧습니다.
✓ 다양한 Grant Type 지원: 클라이언트 유형(웹 애플리케이션, 모바일 앱, 백엔드 서비스 등)에 따라 적절한 권한 부여 방식(Grant Type)을 선택할 수 있습니다. 가장 흔하게 사용되는 Grant Type은 다음과 같습니다.
- Authorization Code Grant: 웹 애플리케이션에 가장 적합하며, 가장 안전한 방식입니다. 클라이언트가 직접 사용자 인증 정보를 다루지 않고, 인증 서버로부터 받은 ‘인가 코드’를 통해 AccessToken을 교환합니다.
- Client Credentials Grant: 애플리케이션 자체가 리소스 소유자일 때 사용됩니다. 사용자 개입 없이 클라이언트 ID와 Secret을 사용하여 AccessToken을 직접 발급받습니다. 주로 서버 간 통신에 사용됩니다.
- Refresh Token: AccessToken의 유효 기간이 만료되었을 때, 사용자의 재인증 없이 새로운 AccessToken을 발급받기 위해 사용되는 장기 토큰입니다. 보안상 중요한 역할을 합니다.


2.2. JWT (JSON Web Token): 가볍고 안전한 정보 교환 방식
JWT는 웹 표준(RFC 7519)으로, 정보 교환을 위한 간결하고 자체 포함적인(self-contained) 방식을 정의합니다. 클라이언트와 서버 간에 정보를 안전하게 전송하기 위해 주로 사용되며, 특히 RESTful API 환경에서 AccessToken의 형태로 많이 활용됩니다.
JWT의 구조는 세 부분으로 나뉘며, 각 부분은 Base64Url로 인코딩된 후 점(.)으로 연결됩니다.
✓ Header (헤더): 토큰의 타입(JWT)과 서명에 사용된 해싱 알고리즘(예: HS256, RS256)을 명시합니다.
✓ Payload (페이로드): 클레임(Claim)이라고 불리는 실제 정보가 담겨 있습니다. 사용자 ID, 역할(Role), 토큰 만료 시간(exp) 등 다양한 정보를 포함할 수 있습니다. 페이로드 정보는 Base64Url로 인코딩될 뿐 암호화되지 않으므로, 민감한 정보는 담지 않아야 합니다.
✓ Signature (서명): 헤더와 페이로드를 Base64Url 인코딩한 값을 서버에만 알려진 비밀 키(Secret Key)와 헤더에 명시된 알고리즘으로 해싱하여 생성합니다. 이 서명은 토큰의 무결성(Integrity)을 보장하며, 토큰이 위변조되지 않았음을 검증하는 데 사용됩니다.
예시: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c


JWT의 장점:
- Stateless(무상태): 서버가 세션을 유지할 필요가 없어 서버 확장이 용이하고, 마이크로서비스 아키텍처에 적합합니다.
- Self-contained(자체 포함): 토큰 자체에 사용자 정보와 권한 정보가 담겨 있어, 매 요청마다 데이터베이스를 조회할 필요가 없어 서버 부담을 줄입니다.
- 크로스 도메인(Cross-domain): CORS(Cross-Origin Resource Sharing) 문제 없이 여러 도메인에서 API를 사용할 수 있습니다.
JWT의 단점:
- 토큰 탈취 시 위험: 한번 발급된 토큰은 만료되기 전까지 유효하므로, 탈취될 경우 보안에 취약해질 수 있습니다. (Refresh Token, 짧은 만료 시간으로 보완)
- 페이로드 크기: 토큰에 많은 정보를 담을수록 크기가 커져 네트워크 부하가 증가할 수 있습니다.
2.3. 세션/쿠키 기반 vs OAuth 2.0 & JWT 비교
전통적인 세션/쿠키 기반 인증 방식과 OAuth 2.0 & JWT 기반 방식의 주요 차이점을 비교해 보겠습니다.
| 구분 | 세션/쿠키 기반 | OAuth 2.0 & JWT 기반 |
|---|---|---|
| 상태 관리 | Stateful (서버 세션 저장) | Stateless (토큰 자체 포함) |
| 확장성 | 세션 동기화 필요, 확장 어려움 | 서버 확장 용이, 마이크로서비스 적합 |
| 보안 취약점 | CSRF 공격에 취약 | XSS 공격으로 토큰 탈취 위험 (Refresh Token으로 보완) |
| 크로스 도메인 | Same-origin 정책으로 제한 | CORS 문제 없이 유연하게 사용 가능 |
| 모바일 앱 적합성 | 비효율적 (세션 유지 어려움) | 매우 적합 (토큰 기반 인증) |
핵심 포인트
OAuth 2.0은 권한 위임을 위한 표준이며, JWT는 가볍고 자체 포함적인 토큰으로 REST API 환경에 최적화된 무상태 인증/인가를 제공합니다.
3. Spring Boot에 OAuth 2.0 & JWT 적용
이제 Spring Boot 애플리케이션에 OAuth 2.0과 JWT 기반의 보안 시스템을 구축하는 구체적인 방법을 살펴보겠습니다. Spring Security 프레임워크를 활용하여 간결하고 강력한 보안 기능을 구현할 수 있습니다.
3.1. 의존성 추가 (Maven pom.xml)
Spring Boot 프로젝트에 필요한 의존성을 추가합니다. spring-boot-starter-security는 Spring Security의 핵심 기능을, spring-security-oauth2-resource-server는 OAuth 2.0 리소스 서버 기능을 제공합니다. JWT를 처리하기 위해 jjwt-api, jjwt-impl, jjwt-jackson 라이브러리를 추가합니다.
코드 설명
Maven 프로젝트의 pom.xml 파일에 필요한 Spring Security 및 JWT 관련 의존성을 추가하는 설정입니다. 이 의존성들을 통해 인증 및 인가 기능을 구현할 수 있습니다.
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Security OAuth2 Resource Server (JWT 처리용) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- JJWT (JSON Web Token) Library -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version> <!-- 2026년 기준 최신 버전 확인 필요 -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version> <!-- 2026년 기준 최신 버전 확인 필요 -->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version> <!-- 2026년 기준 최신 버전 확인 필요 -->
<scope>runtime</scope>
</dependency>
<!-- Lombok (선택 사항, 코드 간결화) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.2. Spring Security 설정
Spring Security 설정을 통해 JWT 기반 인증을 활성화하고, 특정 경로에 대한 접근 권한을 정의합니다. 우리는 JWT를 사용하여 인증을 처리할 것이므로, 세션은 사용하지 않도록 SessionCreationPolicy.STATELESS로 설정합니다.
코드 설명
이 코드는 Spring Security 설정을 정의하는 클래스입니다. FilterChain을 구성하여 JWT 인증 필터를 추가하고, 세션을 사용하지 않도록 설정하며, 특정 URL 경로에 대한 접근 권한을 지정합니다. 예를 들어, /auth/** 경로는 인증 없이 접근 가능하도록 허용하고, 그 외의 모든 요청은 인증된 사용자만 접근할 수 있도록 합니다.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // @PreAuthorize, @PostAuthorize 등 어노테이션 활성화
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
public SecurityConfig(JwtTokenProvider jwtTokenProvider,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtAccessDeniedHandler jwtAccessDeniedHandler) {
this.jwtTokenProvider = jwtTokenProvider;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화 (JWT는 Stateless이므로 CSRF 보호가 필요 없음)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(jwtAuthenticationEntryPoint) // 인증 실패 시 처리
.accessDeniedHandler(jwtAccessDeniedHandler) // 인가 실패 시 처리
)
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용 안 함
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/auth/**").permitAll() // 로그인, 회원가입 등 인증이 필요 없는 경로
.requestMatchers("/public/**").permitAll() // 공개 API
.requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN 역할만 접근 가능
.anyRequest().authenticated() // 그 외 모든 요청은 인증 필요
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
// JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가하여 JWT 인증 처리
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
3.3. JWT 토큰 생성 및 검증 Provider
JWT를 생성하고 유효성을 검증하는 유틸리티 클래스인 JwtTokenProvider를 구현합니다. 이 클래스는 JWT의 서명에 사용될 비밀 키 관리, 토큰 생성, 토큰 정보 추출, 토큰 유효성 검증 등의 역할을 담당합니다.
코드 설명
이 JwtTokenProvider 클래스는 JWT 생성, 토큰에서 인증 정보 추출, 토큰 유효성 검증 기능을 제공합니다. @Value 어노테이션을 통해 application.properties에서 JWT 비밀 키와 만료 시간을 읽어옵니다. createToken 메서드는 사용자 정보와 역할을 기반으로 JWT를 생성하고, getAuthentication 메서드는 토큰에서 인증 정보를 파싱하여 Spring Security의 Authentication 객체를 생성합니다. validateToken은 토큰의 유효성을 검증합니다.
@Component
@Slf4j // Lombok Logger
public class JwtTokenProvider {
private final Key key;
private final long accessTokenValidityInMilliseconds;
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey,
@Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.accessTokenValidityInMilliseconds = accessTokenValidityInSeconds * 1000;
}
// Access Token 생성
public String createToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity = new Date(now + accessTokenValidityInMilliseconds);
return Jwts.builder()
.setSubject(authentication.getName()) // 사용자 ID 또는 이메일
.claim("auth", authorities) // 권한 정보
.setExpiration(validity) // 만료 시간
.signWith(key, SignatureAlgorithm.HS256) // 서명
.compact();
}
// JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
Claims claims = parseClaims(token);
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("auth").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails principal = new User(claims.getSubject(), "", authorities); // 비밀번호는 필요 없음
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
// JWT 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("잘못된 JWT 서명입니다.", e);
} catch (ExpiredJwtException e) {
log.info("만료된 JWT 토큰입니다.", e);
} catch (UnsupportedJwtException e) {
log.info("지원되지 않는 JWT 토큰입니다.", e);
} catch (IllegalArgumentException e) {
log.info("JWT 토큰이 잘못되었습니다.", e);
}
return false;
}
// JWT 토큰에서 Claims 추출
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims(); // 만료된 토큰이어도 클레임은 가져옴 (Refresh Token 로직에서 활용)
}
}
}
3.4. JWT 인증 필터 구현
모든 HTTP 요청이 들어올 때마다 JWT를 검증하고 Spring Security 컨텍스트에 인증 정보를 설정하는 필터를 구현합니다. 이 필터는 JwtTokenProvider를 사용하여 토큰을 검증합니다.
코드 설명
이 JwtAuthenticationFilter는 모든 HTTP 요청에 대해 실행됩니다. 요청 헤더에서 JWT를 추출하고, JwtTokenProvider를 이용해 토큰의 유효성을 검증합니다. 토큰이 유효하면 해당 토큰에서 사용자 인증 정보를 추출하여 Spring Security의 SecurityContextHolder에 저장하여, 이후 요청 처리 과정에서 인증된 사용자로 인식될 수 있도록 합니다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request); // HTTP 요청 헤더에서 JWT 추출
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication); // SecurityContext에 인증 정보 저장
}
filterChain.doFilter(request, response);
}
// Request Header에서 토큰 정보 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer " 제거
}
return null;
}
}
3.5. 로그인 API 구현 (JWT 발급)
사용자가 로그인 요청을 하면, 아이디와 비밀번호를 검증한 후 JWT를 발급하여 클라이언트에게 응답합니다. 클라이언트는 이 JWT를 이후 모든 API 요청의 Authorization 헤더에 담아 전송하게 됩니다.
코드 설명
이 AuthController는 로그인 요청을 처리합니다. 사용자로부터 받은 아이디와 비밀번호를 사용하여 AuthenticationManager를 통해 인증을 시도합니다. 인증에 성공하면, JwtTokenProvider를 사용하여 AccessToken을 생성하고, 이를 클라이언트에게 응답으로 보냅니다. 이 AccessToken은 향후 인증이 필요한 모든 API 요청에 사용됩니다.
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final UserService userService; // 사용자 정보 조회 및 검증 서비스
public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider, UserService userService) {
this.authenticationManager = authenticationManager;
this.jwtTokenProvider = jwtTokenProvider;
this.userService = userService;
}
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
// 1. 사용자 인증 (아이디/비밀번호 검증)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
// 2. 인증 성공 시 JWT 토큰 생성
String jwt = jwtTokenProvider.createToken(authentication);
// 3. 클라이언트에게 토큰 반환
return ResponseEntity.ok(jwt);
}
// LoginRequest DTO (예시)
@Data // Lombok
public static class LoginRequest {
private String username;
private String password;
}
}
핵심 포인트
Spring Security와 JJWT 라이브러리를 통해 JWT 기반의 Stateless 인증 시스템을 구축할 수 있습니다. JwtTokenProvider로 토큰을 생성하고, JwtAuthenticationFilter로 모든 요청에 대한 토큰 유효성을 검증합니다.
4. 흔한 보안 문제와 현명한 해결책
아무리 강력한 보안 기술이라도 완벽할 수는 없습니다. OAuth 2.0과 JWT를 사용하더라도 발생할 수 있는 일반적인 보안 취약점들을 이해하고, 이에 대한 적절한 대응책을 마련하는 것이 중요합니다.
4.1. JWT 탈취 위험과 Refresh Token 전략
JWT는 탈취될 경우, 토큰이 만료되기 전까지는 유효한 토큰으로 간주되어 공격자가 사용자 행세를 할 수 있습니다. 특히 XSS(Cross-Site Scripting) 공격에 취약하여 클라이언트 측에 저장된 JWT가 탈취될 위험이 있습니다.
문제 01
JWT AccessToken 탈취
클라이언트 측에 저장된 AccessToken이 XSS 공격 등으로 인해 탈취될 경우, 공격자가 토큰 만료 전까지 인증된 사용자 행세를 할 수 있습니다.
해결
1. AccessToken의 짧은 유효 기간: AccessToken의 유효 기간을 짧게 설정(예: 15분~30분)하여 탈취되더라도 악용될 시간을 최소화합니다.
2. Refresh Token 사용: AccessToken이 만료되면, Refresh Token을 사용하여 새로운 AccessToken을 발급받습니다. Refresh Token은 AccessToken보다 긴 유효 기간을 가지지만, 더 안전한 방식으로 저장되어야 합니다 (예: 서버 측 DB 저장, HttpOnly 쿠키 사용).
3. Refresh Token 재사용 감지: Refresh Token이 탈취되어 재사용될 경우, 이를 감지하고 해당 Refresh Token을 무효화하는 로직을 구현합니다. 모든 토큰을 만료시키는 등의 강력한 조치를 취할 수 있습니다.
4. AccessToken 저장 위치: 클라이언트 측에서 AccessToken을 localStorage보다는 메모리에 저장하거나, HttpOnly 쿠키를 사용하여 XSS 공격으로부터 보호하는 것을 고려합니다.
4.2. Rate Limiting (비율 제한)
무차별 대입 공격(Brute-force attack)이나 DoS(Denial of Service) 공격으로부터 API를 보호하기 위해 Rate Limiting을 적용해야 합니다. 특정 IP 주소나 사용자로부터 일정 시간 동안 허용되는 요청 수를 제한하는 것입니다.
- 구현 방법: Spring Cloud Gateway, Nginx와 같은 API 게이트웨이에서 설정하거나, Spring Boot 애플리케이션 내에서
Bucket4j나Resilience4j와 같은 라이브러리를 사용하여 구현할 수 있습니다. - 적용 대상: 로그인 API, 회원가입 API 등 인증/인가와 관련된 민감한 엔드포인트에 우선적으로 적용하는 것이 좋습니다.
4.3. CORS (Cross-Origin Resource Sharing) 설정
웹 브라우저의 Same-Origin Policy에 따라, 다른 도메인에서 API를 호출할 때 CORS 문제가 발생할 수 있습니다. Spring Boot에서는 WebMvcConfigurer를 통해 CORS 설정을 쉽게 할 수 있습니다.
코드 설명
이 코드는 Spring Boot 애플리케이션에서 전역 CORS 설정을 정의합니다. addCorsMappings 메서드를 통해 모든 경로("/**")에 대해 특정 오리진("http://localhost:3000"), HTTP 메서드, 헤더 및 자격 증명(credentials)을 허용하도록 설정합니다. 이는 프론트엔드 애플리케이션이 백엔드 API를 안전하게 호출할 수 있도록 합니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 모든 경로에 대해
.allowedOrigins("http://localhost:3000", "https://your-frontend-domain.com") // 허용할 Origin
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메서드
.allowedHeaders("*") // 모든 헤더 허용
.allowCredentials(true) // 자격 증명 허용 (쿠키, HTTP 인증 등)
.maxAge(3600); // Pre-flight 요청 캐싱 시간
}
}
4.4. HTTPS (SSL/TLS) 적용의 중요성
모든 API 통신은 반드시 HTTPS를 통해 이루어져야 합니다. HTTPS는 클라이언트와 서버 간의 통신을 암호화하여 중간자 공격(Man-in-the-Middle attack)으로부터 데이터를 보호합니다. JWT를 포함한 모든 민감한 정보는 네트워크를 통해 평문으로 전송되어서는 안 됩니다.
- 구현 방법: AWS ELB, Nginx, Apache 등의 리버스 프록시 서버에서 SSL/TLS 인증서를 설정하여 HTTPS를 적용하는 것이 일반적입니다. Spring Boot 자체에서도 HTTPS를 설정할 수 있지만, 프로덕션 환경에서는 프록시를 사용하는 것이 효율적입니다.

핵심 포인트
JWT 탈취 위험은 짧은 AccessToken 유효 기간과 Refresh Token 전략, 그리고 HttpOnly 쿠키 사용으로 완화할 수 있습니다. Rate Limiting, CORS 설정, 그리고 HTTPS 적용은 API 보안의 필수적인 요소입니다.
5. 실전! API 엔드포인트 보호 및 테스트
이제 JWT 기반 인증 시스템이 구축되었다고 가정하고, 실제 API 엔드포인트를 보호하고 테스트하는 방법을 알아보겠습니다. Spring Security의 강력한 인가 기능을 활용하여 특정 역할(Role)을 가진 사용자만 접근할 수 있도록 설정할 수 있습니다.
5.1. 역할 기반 인가 (@PreAuthorize)
@EnableMethodSecurity(prePostEnabled = true) 설정을 통해 @PreAuthorize 어노테이션을 사용할 수 있습니다. 이 어노테이션을 사용하여 메서드 실행 전에 특정 권한이나 역할을 가진 사용자만 접근할 수 있도록 제한할 수 있습니다.
코드 설명
이 컨트롤러 예시는 @PreAuthorize 어노테이션을 사용하여 메서드 수준의 인가(Authorization)를 적용하는 방법을 보여줍니다. /user/me 엔드포인트는 인증된 사용자라면 누구나 접근할 수 있으며, /admin/data 엔드포인트는 ADMIN 역할을 가진 사용자만 접근할 수 있도록 제한됩니다.
@RestController
@RequestMapping("/api")
public class ProtectedController {
@GetMapping("/user/me")
@PreAuthorize("isAuthenticated()") // 인증된 사용자만 접근 가능
public ResponseEntity<String> getUserInfo(Authentication authentication) {
return ResponseEntity.ok("Hello, " + authentication.getName() + "! Your roles: " + authentication.getAuthorities());
}
@GetMapping("/admin/data")
@PreAuthorize("hasRole('ADMIN')") // ADMIN 역할을 가진 사용자만 접근 가능
public ResponseEntity<String> getAdminData() {
return ResponseEntity.ok("This is sensitive admin data!");
}
@PostMapping("/manager/report")
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')") // ADMIN 또는 MANAGER 역할을 가진 사용자만 접근 가능
public ResponseEntity<String> createReport() {
return ResponseEntity.ok("Report created successfully by " + SecurityContextHolder.getContext().getAuthentication().getName());
}
}
5.2. Postman/cURL을 이용한 테스트
구현된 API 보안을 테스트하기 위해 Postman이나 cURL과 같은 도구를 사용할 수 있습니다.
5.2.1. 로그인하여 AccessToken 발급받기
먼저, 로그인 API를 호출하여 AccessToken을 발급받습니다. 응답으로 받은 토큰 문자열을 복사해둡니다.
코드 설명
이 cURL 명령은 /auth/login 엔드포인트로 로그인 요청을 보냅니다. -X POST는 POST 메서드를 사용함을, -H "Content-Type: application/json"은 요청 본문이 JSON 형식임을 나타냅니다. -d 옵션 뒤에 JSON 형식의 사용자 이름과 비밀번호를 전달합니다. 성공적으로 로그인하면 JWT AccessToken이 응답으로 반환됩니다.
curl -X POST \
http://localhost:8080/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "user1",
"password": "password"
}'
5.2.2. 보호된 API 호출하기
발급받은 AccessToken을 Authorization 헤더에 Bearer 접두사와 함께 담아 보호된 API를 호출합니다.
코드 설명
이 cURL 명령은 보호된 API 엔드포인트인 /api/user/me로 GET 요청을 보냅니다. 중요한 부분은 -H "Authorization: Bearer [YOUR_ACCESS_TOKEN]" 헤더입니다. 여기에 앞서 로그인 요청으로 발급받은 JWT AccessToken을 Bearer 접두사와 함께 포함시켜야 합니다. 이 헤더가 없거나 토큰이 유효하지 않으면 401 Unauthorized 또는 403 Forbidden 응답을 받게 됩니다.
curl -X GET \
http://localhost:8080/api/user/me \
-H 'Authorization: Bearer [YOUR_ACCESS_TOKEN]'
만약 유효하지 않은 토큰을 사용하거나, 필요한 역할이 없는 사용자로 접근하면 401 Unauthorized 또는 403 Forbidden 응답을 받게 될 것입니다. 이를 통해 인가 로직이 올바르게 작동하는지 확인할 수 있습니다.