우당탕탕

Spring Boot JWT 로그인 구현하다 겪은 삽질과 해결 과정 공유합니다 본문

Tech/Spring

Spring Boot JWT 로그인 구현하다 겪은 삽질과 해결 과정 공유합니다

모찌모찝 2026. 5. 27. 19:41

사실 Spring Boot로 JWT 로그인 기능을 구현하다가 예상보다 꽤 삽질을 많이 했어요. 처음에는 토큰 발급만 하면 되는 줄 알았는데, 시큐리티 설정부터 인증, 토큰 검증까지 막히는 부분이 한두 가지가 아니더라고요.

이 글에서는 제가 직접 구현하면서 겪은 문제와 그 해결법, 그리고 자주 헷갈리는 부분에 대해 차근차근 설명해드릴게요. Spring Security 설정부터 JWT 토큰 생성, 필터 적용, 그리고 인증 예외 처리까지 모두 담았습니다.

Spring Boot JWT 로그인 구현하다 삽질한 것들 관련 이미지

Spring Boot JWT 로그인 구현하다 삽질한 것들 관련 정보

개발 환경 / 버전 정보

이 프로젝트는 Java 17, Spring Boot 3.2로 진행했습니다. 의존성으로는 spring-boot-starter-securityjjwt 라이브러리를 쓰고 있어요.

JWT 토큰 생성은 이렇게 했어요

JWT 토큰 만드는 부분에서 처음에 헤더와 클레임 설정을 제대로 못 해서 토큰이 이상하게 나왔거든요. 그래서 제가 쓴 코드를 공개할게요.

private static final String SECRET_KEY = "mysecretkey1234567890";

public String generateToken(UserDetails userDetails) {
    return Jwts.builder()
        .setSubject(userDetails.getUsername())
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60)) // 1시간 유효
        .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
        .compact();
}

이 부분에서 중요한 건 토큰 만료시간을 잘 설정하는 거랑 비밀키를 byte 배열로 넘겨야 한다는 점이에요. 이걸 문자열 그대로 넣으면 경고도 뜨고 나중에 토큰 검증에서 오류가 나더라고요.

Spring Boot JWT 로그인 구현하다 삽질한 것들 관련 이미지

Spring Boot JWT 로그인 구현하다 삽질한 것들 관련 정보

시큐리티 필터에 JWT 넣으니 여기서 많이 틀립니다

사실 이 부분이 제일 헤맸어요. JWT 토큰을 HTTP 요청 헤더에서 뽑아서 인증 정보를 세팅하는 필터를 직접 만들었는데, 스프링 시큐리티의 필터 체인에 어떻게 넣어야 할지 헷갈렸거든요.

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7);
        String username = jwtService.extractUsername(token);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(token, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        filterChain.doFilter(request, response);
    }
}

핵심은 인증정보가 SecurityContext에 정확히 세팅돼야 클라이언트 요청을 인증된 상태로 처리할 수 있다는 점이에요. 그리고 필터를 시큐리티 필터 체인에서 UsernamePasswordAuthenticationFilter 앞에 넣어야 제대로 동작하더라고요.

여기서 삽질했던 부분들

이 에러가 왜 나는지 한참 찾았는데, 토큰 검증 실패 시 예외 처리를 안 해서 500 에러가 났던 적이 있어요. 아래처럼 예외 메시지가 나왔죠.

io.jsonwebtoken.ExpiredJwtException: JWT expired at ...

토큰 만료나 서명 오류 때 모두 401 Unauthorized 응답을 주도록 JwtAuthenticationFilter 내부에 예외 처리를 넣었더니 한결 안정적이었어요.

심화: 이 부분도 알면 좋아요

JWT 토큰의 보안 강화를 위해서 비밀키는 환경변수로 관리하고, 토큰 재발급 로직도 만들어두는 게 좋아요. 그리고 토큰에 너무 많은 정보를 담지 말고 최소한의 식별 정보만 넣는 게 성능이나 보안 측면에서 유리하더라고요.

자주 물어보시는 것들

Q. JWT 토큰을 클라이언트 쿠키에 저장해도 될까요?

A. 쿠키에 저장해도 되지만 HttpOnly 옵션을 꼭 붙여야 XSS 공격을 막을 수 있어요. 로컬 스토리지에 저장하면 JS에서 접근하기 쉬워 위험할 수 있거든요.

Q. 토큰 만료 후 자동 로그인은 어떻게 구현하나요?

A. 보통 액세스 토큰과 리프레시 토큰을 나누어 관리해요. 액세스 토큰 만료 시 리프레시 토큰으로 새 토큰을 발급받게 하는 방식이 일반적입니다.

Spring Boot JWT 로그인 구현하다 삽질한 것들 관련 이미지

Spring Boot JWT 로그인 구현하다 삽질한 것들 관련 정보

직접 JWT 로그인 구현하며 겪은 문제와 해결책을 정리해봤는데요, 이 글 하나로 기본적인 궁금증은 다 해결되실 거예요. 다음에는 OAuth2와 JWT를 같이 쓸 때 주의할 점도 공유할게요.

Comments