[Spring] JWT 03. Interceptor로 인증 적용하기 (로그인 필요 API 보호)
JWT를 발급했다면 이제 “로그인 필요한 API”를 보호해야 합니다.
Spring에서는 보통 Security Filter를 많이 쓰지만, 프로젝트 구조에 따라 Interceptor로도 충분히 구현할 수 있습니다.
이번 글은 Interceptor 기반 JWT 인증 흐름을 정리합니다.
1. Interceptor로 JWT 인증을 하는 이유
- 컨트롤러 실행 전에 요청을 가로채서 검사할 수 있음
- 특정 URL 패턴만 “로그인 필요”로 지정 가능
- JWT 파싱 후 loginId / 권한을 request에 담아 컨트롤러에서 사용 가능
핵심 목표는 딱 2가지입니다.
- Authorization 헤더에 토큰이 없거나 잘못되면 → 401 Unauthorized
- 정상 토큰이면 → claims(loginId, level)을 꺼내서 request에 저장
2. 요청 헤더에서 토큰이 어떻게 오나?
프론트는 보통 아래처럼 헤더에 JWT를 담아 보냅니다.
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9....
서버는 이 헤더를 받아서 Bearer 를 제거하고 토큰을 검증합니다.
3. TokenService(토큰 검증/파싱) 예시
Interceptor는 “인증 검사 담당”, TokenService는 “JWT 파싱 담당”으로 역할을 나누면 관리가 편합니다.
import java.nio.charset.StandardCharsets;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
@Service
public class TokenService {
@Value("${custom.jwt.key-str}")
private String keyStr;
@Value("${custom.jwt.issuer}")
private String issuer;
public TokenVO parse(String authorization) {
if (authorization == null || !authorization.startsWith("Bearer ")) {
throw new RuntimeException("NO_TOKEN");
}
String token = authorization.substring(7);
SecretKey key = Keys.hmacShaKeyFor(keyStr.getBytes(StandardCharsets.UTF_8));
Claims claims = (Claims) Jwts.parser()
.verifyWith(key)
.requireIssuer(issuer)
.build()
.parse(token)
.getPayload();
TokenVO vo = new TokenVO();
vo.setLoginId((String) claims.get("loginId"));
vo.setLoginLevel((String) claims.get("loginLevel"));
return vo;
}
}
TokenVO는 로그인 정보를 담는 DTO 역할입니다.
public class TokenVO {
private String loginId;
private String loginLevel;
// getter/setter 생략
}
4. Interceptor 구현 (로그인 필요한 요청만 통과)
Interceptor에서 할 일은 아래 순서입니다.
- Authorization 헤더 꺼내기
- TokenService.parse()로 검증/파싱
- 성공하면 request attribute에 loginId, loginLevel 저장
- 실패하면 401 반환
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class MemberInterceptor implements HandlerInterceptor {
private final TokenService tokenService;
public MemberInterceptor(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String authorization = request.getHeader("Authorization");
try {
TokenVO tokenVO = tokenService.parse(authorization);
// 컨트롤러에서 꺼내 쓰도록 저장
request.setAttribute("loginId", tokenVO.getLoginId());
request.setAttribute("loginLevel", tokenVO.getLoginLevel());
request.setAttribute("tokenVO", tokenVO);
return true; // 통과
}
catch (Exception e) {
// 토큰 없거나, 위조/만료/issuer 불일치 등
response.setStatus(401);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().write("{\"message\":\"Unauthorized\"}");
return false;
}
}
}
5. Interceptor 등록 (URL 패턴 지정)
Interceptor는 등록을 해야 동작합니다.
특정 URL만 보호하려면 addPathPatterns()에 패턴을 지정합니다.
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
private final MemberInterceptor memberInterceptor;
public InterceptorConfiguration(MemberInterceptor memberInterceptor) {
this.memberInterceptor = memberInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(memberInterceptor)
.addPathPatterns(
"/api/mypage/**",
"/api/point/**",
"/api/admin/**"
)
.excludePathPatterns(
"/api/account/login",
"/api/account/join",
"/api/account/refresh"
);
}
}
이제 /api/mypage/** 같은 경로는 토큰 없으면 접근 불가입니다.
6. 컨트롤러에서 loginId 사용하기
Interceptor가 request에 저장한 loginId를 컨트롤러에서 꺼내 쓰면 됩니다.
@GetMapping("/api/mypage")
public String mypage(HttpServletRequest request) {
String loginId = (String) request.getAttribute("loginId");
String loginLevel = (String) request.getAttribute("loginLevel");
// loginId 기준으로 DB 조회 등 처리
return "OK : " + loginId + " (" + loginLevel + ")";
}
7. 실행 결과 (Output)
7-1) 토큰 없이 요청하면
GET /api/mypage
Authorization: (없음)
HTTP/1.1 401 Unauthorized
{"message":"Unauthorized"}
7-2) 정상 토큰으로 요청하면
GET /api/mypage
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9....
HTTP/1.1 200 OK
OK : testuser1 (USER)
7-3) 만료된 토큰이면
HTTP/1.1 401 Unauthorized
{"message":"Unauthorized"}
만료되었을 때는 보통 프론트가 refresh API를 호출해서 새 AccessToken을 받은 뒤 재요청합니다.
8. 자주 하는 실수
- Authorization 헤더에 Bearer 없이 토큰만 보내기 → 서버 파싱 실패
- 모든 URL에 인터셉터 걸어버려서 로그인/회원가입/refresh도 막힘
- 401을 받았을 때 프론트에서 refresh 로직이 없어서 “로그인 풀림”처럼 보임
마무리
Interceptor로 JWT 인증을 적용하면 “로그인 필요 API”를 쉽게 보호할 수 있습니다.
다음 글(4편)에서는 Token 만료 임박 시 자동 갱신(renewal) 또는 RefreshToken 재발급 흐름를 실제 운영 방식처럼 정리해보겠습니다.
'Tech Stack > Spring Boot' 카테고리의 다른 글
| [Spring MVC] DispatcherServlet(디스패처 서블릿)이란 무엇인가? (0) | 2026.03.28 |
|---|---|
| [Spring] JWT 05. RefreshToken DB 저장 + 1회성 폐기(로테이션) + 재발급 흐름 완성 (0) | 2026.02.19 |
| [Spring] JWT 04. 만료 임박 자동 갱신(TokenRenewalInterceptor) 적용 (0) | 2026.02.19 |
| [Spring] JWT 02. AccessToken / RefreshToken 구조 + 재발급 흐름 (0) | 2026.02.18 |
| [Spring] JWT 01. Json Web Token 개념정리 (0) | 2026.02.18 |