Spring Boot JWT 보안의 모든 것
스프링 부트 REST API, JWT로 철통 보안 구축하기
Spring Boot를 사용해 REST API를 개발하고 있다면, 한 번쯤 '보안' 문제에 부딪히게 됩니다. 사용자 인증과 권한 관리를 어떻게 해야 할지 고민이셨죠? 복잡한 세션 관리 방식은 머리가 아프고, 확장성 문제 때문에 고민이 많으셨을 겁니다.
이 글은 바로 그런 고민을 해결해 드리기 위해 준비했습니다. JWT(JSON Web Token)를 활용하여 Spring Boot 기반 REST API의 보안 시스템을 구축하는 방법을 단계별로 안내합니다. JWT가 무엇인지에 대한 기본적인 개념부터, 실제 Spring Security와 JJWT 라이브러리를 활용해 로그인 및 권한 인가 기능을 구현하는 실질적인 코드 예제까지 모두 담았습니다.
이 글을 끝까지 읽고 나면, 무상태(Stateless) API의 보안을 자신 있게 다룰 수 있게 될 것입니다. 더 이상 복잡한 인증 절차에 주눅 들지 마세요. 이제 직접 JWT 기반의 안전한 API를 만들어낼 차례입니다. 자, 그럼 본격적으로 시작해 볼까요?
1. JWT, 어렵지 않아요! 핵심 개념 이해하기
JWT(JSON Web Token)는 웹 애플리케이션에서 사용자를 인증하고 정보를 안전하게 전달하는 데 사용되는 개방형 표준(RFC 7519)입니다. 특히 REST API와 같이 상태를 유지하지 않는(stateless) 환경에서 빛을 발합니다. 기존의 세션 기반 인증 방식은 서버가 사용자의 상태를 계속해서 저장하고 있어야 했지만, JWT는 그럴 필요가 없습니다. 이것이 바로 JWT가 MSA(Microservice Architecture) 환경에서 각광받는 이유입니다.
JWT는 점(.)으로 구분된 세 부분으로 구성됩니다:
- Header (헤더): 토큰의 타입과 서명에 사용된 알고리즘(예: HMAC SHA256 또는 RSA)을 정의합니다.
- Payload (페이로드): 클레임(Claims)이라고 불리는 실제 정보가 포함됩니다. 사용자 ID, 만료 시간, 권한 등 필요한 데이터를 여기에 담습니다. 민감한 정보는 담지 않는 것이 좋습니다.
- Signature (서명): 헤더와 페이로드를 Base64Url로 인코딩한 값과 서버의 비밀키를 조합하여 생성됩니다. 서버만이 이 비밀키를 알고 있기 때문에, 토큰이 위변조되지 않았음을 검증하는 데 사용됩니다.
초보자를 위한 비유: JWT는 마치 "보안이 걸린 회원증"과 같습니다. 헤더는 회원증의 재질, 페이로드는 이름과 등급 정보, 그리고 서명은 위조 방지를 위한 위조 방지 홀로그램이라고 생각하면 이해하기 쉽습니다.
2. Spring Boot 프로젝트 설정 및 의존성 추가
이제 실제로 JAVA와 Spring Boot를 사용하여 JWT 기반의 보안 시스템을 구축해 보겠습니다. 먼저 pom.xml에 필요한 의존성을 추가해야 합니다. Spring Security는 인증 및 인가와 관련된 기능을 쉽게 구현할 수 있도록 도와주며, JJWT는 JWT를 생성하고 파싱하는 데 필요한 기능을 제공합니다.
pom.xml에 의존성 추가:
<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>
<!-- JJWT (JSON Web Token) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok (optional, but very useful) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
3. 핵심 로직: JWT 토큰 생성 및 검증 구현하기
가장 핵심적인 부분인 JWT 생성 및 검증 로직을 구현해 보겠습니다. 초급 개발자분들을 위해 JwtTokenProvider라는 클래스를 만들어 토큰 관련 로직을 한 곳에 모아 관리하는 것이 좋습니다.
JwtTokenProvider.java 예시:
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret-key}")
private String secretKey;
@Value("${jwt.access-token-expiration-in-milliseconds}")
private long accessTokenExpiration;
public String generateToken(String username, String role) {
Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenExpiration);
return Jwts.builder()
.setSubject(username)
.claim("role", role)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public String getUsernameFromToken(String token) {
Key key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody().getSubject();
}
}
application.properties에 비밀키 및 만료 시간 설정:
jwt.secret-key=super_secure_secret_key_for_jwt_development
jwt.access-token-expiration-in-milliseconds=3600000
4. Spring Security 필터를 통한 인증 및 인가 처리
생성한 JwtTokenProvider를 사용해 Spring Security의 필터 체인에 JWT 검증 로직을 추가해야 합니다. 이렇게 하면 모든 요청이 컨트롤러에 도달하기 전에 JWT 유효성 검사를 거치게 됩니다.
JwtAuthenticationFilter.java 예시:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// ... (JwtTokenProvider와 UserDetailsService 주입)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
SecurityConfig.java에 필터 등록:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ... (필요한 빈 주입)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
이러한 단계별 접근 방식을 통해 초급 개발자도 Spring Boot와 JWT를 활용한 보안 시스템을 효과적으로 이해하고 구현할 수 있습니다.
결론: JWT 보안, 이제 자신감 있게!
지금까지 Spring Boot와 JWT를 활용해 REST API 보안 시스템을 구축하는 방법을 알아보았습니다. JWT의 핵심 개념부터 실제 코드 구현까지, 다소 복잡하게 느껴질 수 있는 주제를 단계별로 살펴보면서 그 과정이 생각보다 어렵지 않다는 것을 확인하셨을 겁니다.
주요 내용 요약 및 Takeaways
- JWT는 무상태(stateless) API 보안의 핵심입니다. 서버가 사용자의 상태를 저장하지 않아도 되기 때문에 마이크로서비스나 분산 환경에 매우 적합합니다.
- JWT는 헤더(Header), 페이로드(Payload), 서명(Signature)으로 구성됩니다. 이 세 가지 요소가 토큰의 정보를 담고, 위변조 여부를 검증하는 역할을 합니다.
- Spring Security 필터와 JWT 토큰 Provider를 함께 사용하면 모든 HTTP 요청에 대해 자동으로 인증 및 인가 처리를 할 수 있습니다. 이를 통해 보안 로직을 비즈니스 로직과 분리하여 깔끔한 코드를 유지할 수 있습니다.
- 초급 개발자도 충분히 구현할 수 있습니다. 필요한 의존성을 추가하고, 토큰 생성 및 검증 로직을 구현한 다음, Spring Security 설정 파일에 필터를 등록하는 세 가지 핵심 단계만 기억하면 됩니다.
다음 단계로 나아가기
오늘 배운 내용을 바탕으로 다음과 같은 주제들을 추가로 학습해 보세요.
- 토큰 갱신(Refresh Token) 구현: 액세스 토큰의 만료 시간을 짧게 설정하고, 리프레시 토큰을 사용해 사용자의 재로그인 없이 토큰을 갱신하는 방법을 배워보세요. 이는 사용자 경험과 보안성을 동시에 높일 수 있는 중요한 기술입니다.
- OAuth2와 OIDC(OpenID Connect): JWT는 인증 토큰의 한 종류이며, 소셜 로그인에 주로 사용되는 OAuth2나 OIDC와 같은 인증 프로토콜을 학습하면 더 넓은 시야를 갖게 될 것입니다.
이번 글이 여러분의 개발 여정에 긍정적인 자극이 되었기를 바랍니다. 이제 Spring Boot와 JWT로 더욱 안전하고 강력한 REST API를 만들어낼 자신감을 가지셔도 좋습니다. 👍
```
댓글
댓글 쓰기