[골든리포트!] 6) 필터체인에서 JWT필터 인증 처리하기

2026. 2. 13. 01:42·개인 프로젝트/[2026] 골든리포트!

몇 가지 수정사항이 생겼다.

GDG 플젝트랙에서는 모든 경로를 permitAll() 해주고 인증이 필요한 요청에 대해 헤더에 있는 JWT 토큰을 검사하는 방식으로 구현했다.

 

물론 이 방식은 딱히 좋은 편이 아니라서 이번에는 SpringFilterChain에 직접 JWT 토큰을 검사하는 필터를 넣어주고자한다.

그전에 수정사항들

 

package com.example.GoldenReport.Service.JWT;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.security.Key;
import java.util.*;

@Component
public class JWTProvider {
    @Value("${jwt.secret-key}")
    private String jwtSecretKey;
    private SecretKey key;

    @PostConstruct
    private void init() {
        key = Keys.hmacShaKeyFor(jwtSecretKey.getBytes());
    }

    /**
     *
     * @param userId    유저 식별자
     * @param expiration  유효시간 (단위: 초)
     * @return JWT 토큰
     */
    public String generateJWT(String userId, int expiration) {
        Claims claims = Jwts.claims().build();
        expiration = expiration * 1000;
        Date now = new Date();

        return Jwts.builder()
                .subject(userId)
                .claims(claims)
                .issuedAt(now)
                .expiration(new Date(now.getTime() + expiration))
                .signWith(key)
                .compact();
    }

    public String generateJWT(String userId,
                              int expiration,
                              Collection<Map<String, Object>> claimsContent) {
        Map<String, Object> claims = new HashMap<>();

        for (Map<String, Object> claim : claimsContent) {
            claims.putAll(claim);
        }

        expiration = expiration * 1000;
        Date now = new Date();

        return Jwts.builder()
                .subject(userId)
                .claims(claims)
                .issuedAt(now)
                .expiration(new Date(now.getTime() + expiration))
                .signWith(key)
                .compact();
    }

    public Optional<String> decodeJWT(String jwt) {
        return Optional.ofNullable(Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt).getPayload().getSubject());
    }

    public Optional<Map<String, Object>> decodeJWT(String jwt, List<String> keys) {
        Map<String, Object> results = new HashMap<>();
        Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt).getPayload();
        results.put("subject", claims.getSubject());

        for (String key : keys) {
            if (claims.containsKey(key)) {
                results.put(key, claims.get(key));
            }
        }

        return Optional.of(results);
    }
}

기존 JWTFilter는 사용하지 않고, SuccessHandler나 JWTAuthorizationFilter에서 직접 JWTProvider에 접근한다. 그래서 decodeJWT 메서드를 오버라이드하여 원하는 claim들을 넣어줄 수 있도록 설정했다.

 

기존방식은 generateJWT와 decodeJWT 가 userId를 받아서 JWT토큰을 생성해 반환하거나, JWT토큰에서 userId를 빼내는 역할을 담당했다면, 추가된 메서드들은 Map으로 claim에 넣을 내용을 받아서 generateJWT에서 claim에 넣고 JWT토큰을 생성하고, decodeJWT 메서드에서는 key 값 이름만 리스트로 받아서 Map으로 결과를 변환한 다음에 반환한다.

 

이렇게하면서 자유롭게 claim에 내용을 넣을 수 있게되었다.


 

그리고 본격적인 JWT토큰 사용을 위해서 세션을 일절 사용하지 않기로한다. 즉, SessionPolicy를 STATLESS로 설정. 따라서 세션을 사용하던 기존의 RequestCache는 사용하지 못하게됐다. 관련된 코드는 모두 들어냈다. 그래도 세션로그인할 때 유용하게 써먹을 수 있는 기능들을 잘 파악했다. 나중에 세션로그인을 한다면 유용하게 쓸 생각

 

필터를 사용하면 인증이 필요한 요청이 들어오면 그 요청을 가로채서 원하는 로직을 처리할 수 있다. 즉, JWT토큰을 사용한다면 요청을 가로채서 쿠키 (또는 헤더)에 JWT토큰이 있는지 검사하고 다시 요청이 진행되도록하는 로직을 처리할 수 있겠다.

 

대략적 매커니즘은 인증을 끝내고, 인증정보를 담은 Authentication 객체를 생성하며, 사용되는 필터는

 

1. GenericFilterChain

모든 필터들의 조상격 되는 클래스이며, 간단한 필터기능은 처리할 수 있다.

 

2. UsernamePasswordAuthenticationFilter

폼로그인을 사용할 때, ID와 PW값으로 Authentication 객체를 생성한다.

 

3. OncePerRequestFilter

URL포워딩 (리다이렉트)를 사용할 경우, 인증이 불필요한 상황임에도 불구하고 실제로는 포워딩된 요청에 대해서도 인증을 진행하게된다. 따라서 인증정보에 특별한 표지를 넣어 이미 인증이된 사용자인지 여부를 따질 수 있는 인증방식을 사용하는 것이 OncePerRequestFilter

 

폼로그인 방식을 사용할 경우 2번과 3번을 적절히 사용할 수 있다.

 

근데 나는 JWT토큰 방식을 사용할 것이기 때문에 우선은 OncePerRequestFilter에서 쿠키의 JWT토큰을 확인하고 Authentication 객체를 만들어서 다음 필터로 인증과정을 넘기는 로직을 짜려고한다.

 

package com.example.GoldenReport.Config;

import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Service.JWT.JWTProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.security.sasl.AuthenticationException;
import java.io.IOException;
import java.util.*;

public class JWTAuthorizationFilter extends OncePerRequestFilter {

    private final JWTProvider jwtProvider;
    private final MemberRepository memberRepository;

    public JWTAuthorizationFilter(JWTProvider JWTprovider,
                                  MemberRepository memberRepository) {
        this.jwtProvider = JWTprovider;
        this.memberRepository = memberRepository;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            throw new AuthenticationException("cookies is null");
        }

        String accessToken = Arrays.stream(cookies)
                .filter(cookie -> "Authentication".equals(cookie.getName()))
                .map(Cookie::getValue)
                .findFirst()
                .orElse(null);
        if (accessToken == null) {
            throw new AuthenticationException("There is no Authentication cookie in the request");
        }

        Optional<Map<String, Object>> optionalUser = jwtProvider.decodeJWT(accessToken, List.of("position"));
        if (optionalUser.isEmpty()) {
            throw new AuthenticationException("Username or password is empty");
        }

        Map<String, Object> user = optionalUser.get();
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                user.get("subject"),
                null,
                AuthorityUtils.createAuthorityList(user.get("position").toString()));

        SecurityContextHolder
                .getContext()
                .setAuthentication(authentication);

        filterChain.doFilter(request, response);
    }
}

요청이 들어오면 인증이 필요한 요청을 JWTAuthorizationFilter가 이를 낚아채서 로직을 처리한다.

원래 OncePerRequestFilter는 doFilter() 메서드가 호출되어서 몇 가지 검사를한다. (OncePerRequestFilter는 인증이 된 사용자인지 파악해야하므로 이런 로직을 수행하던가..) 그리고나서 문제가 없으면 doFilterInternal() 메서드가 호출된다. 그래서 doFilterInternal() 메서드를 오버라이드하면 된다.

 

그래서 쿠키를 받아서 JWT토큰을 가져오고, 원래는 memberRepository에서 userId가 유효한지 검증해야하지만, 우선은 제외. 그냥 JWT토큰이 보내는 userId를 신뢰하도록한다. 만약 JWT토큰을 확인하는 과정에서 오류가 발생하면 AuthenticationException을 발생시킨다.

 

그리고 마지막에 doFilter() 메서드를 통해 이후 과정으로 HTTP 요청을 넘긴다.


 

만약 AuthenticationException이 발생했다면, 스프링은 이를 401 에러가 발생했다고 인식한다.

OAuth2 로그인 같은 경우에는 중간에 실패하면 나중에 FailureHandler를 불러와서 후속로직을 처리하지만, 만약 401 에러가 발생하면 AuthenticationEnrtyPoint를 호출한다.

 

https://yoo-dev.tistory.com/28

 

[Spring Security] AuthenticationEntryPoint, AccessDeniedHandler를 통한 인증/인가 오류 처리

인증/인가 오류 처리 우리는 Spring Security를 통해 사용자의 권한을 처리하게 된다. Security 설정을 통해 특정 엔드포인트로의 요청에 필요한 권한 등을 설정할 수 있다. @Bean public SecurityFilterChain oaut

yoo-dev.tistory.com

package com.example.GoldenReport.Service.LogInAndSignUp;

import com.example.GoldenReport.DTO.HTTPResponseDTO.HTTPResponseDTO;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import tools.jackson.databind.ObjectMapper;

import java.io.IOException;

public class JWTAuthorizationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper mapper = new ObjectMapper();

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        HTTPResponseDTO httpResponseDTO = HTTPResponseDTO.builder()
                .status(401)
                .message("Unauthorized")
                .build();

        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(mapper.writeValueAsString(httpResponseDTO));
    }
}

여기서 일괄적으로 처리

 

이외에도

만약 403 forbidden 에러를 터뜨리고싶다면 AccessDeniedException을 터뜨리고, AccessDeniedHandler에서 이를 낚아채면되는 등.. 자유로이 핸들러클래스를 상속받아서 사용하면 된다.

 

package com.example.GoldenReport.Config;

import com.example.GoldenReport.Repository.MemberRepository;
import com.example.GoldenReport.Service.JWT.JWTFilter;
import com.example.GoldenReport.Service.JWT.JWTProvider;
import com.example.GoldenReport.Service.LogInAndSignUp.JWTAuthorizationEntryPoint;
import com.example.GoldenReport.Service.LogInAndSignUp.OAuth2SuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig{
    OAuth2SuccessHandler oauth2SuccessHandler;
    JWTFilter jwtFilter;
    MemberRepository memberRepository;

    public SecurityConfig(OAuth2SuccessHandler oauth2SuccessHandler,
                          JWTFilter jwtFilter,
                          MemberRepository memberRepository) {
        this.oauth2SuccessHandler = oauth2SuccessHandler;
        this.jwtFilter = jwtFilter;
        this.memberRepository = memberRepository;
    }

    @Bean
    public SecurityFilterChain filterChain (HttpSecurity http, JWTProvider jWTProvider, MemberRepository memberRepository) throws Exception {

        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/",
                                "/css/**",
                                "/js/**",
                                "/images/**",
                                "/favicon.ico/**",
                                "/h2-console/**",
                                "/signup/**",
                                "/oauth2/**").permitAll()
                        .requestMatchers(HttpMethod.GET, "/review/**").permitAll()
                        .anyRequest().authenticated()
                )
                .oauth2Login(oauth2 -> oauth2
                        .defaultSuccessUrl("/")
                        .successHandler((request, response, authentication) -> {
                            oauth2SuccessHandler.onAuthenticationSuccess(request, response, authentication);
                        })
                        .failureUrl("/login?error=true")
                )
                .addFilterBefore(new JWTAuthorizationFilter(jWTProvider, memberRepository),
                        UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(ex -> ex
                        .authenticationEntryPoint(new JWTAuthorizationEntryPoint()))
                .sessionManagement((session)-> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .csrf(AbstractHttpConfigurer::disable)
                .headers(headers -> headers
                        .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
                )
                .logout(logout -> logout
                        .logoutSuccessUrl("/")
                );

        return http.build();
    }
}

security config에 추가

.addFilterBefore 를 통해 JWTAuthorizationFilter가 필터역할로 필터체인에 들어간다는 것을 알리고,

exceptionHandling 을 통해 authenticationEntryPoint로 어느 클래스를 사용할지 알려준다.


 

String path = request.getRequestURI();
System.out.println(path);

if (path.startsWith("/signup") || path.startsWith("/oauth2") ||
    path.startsWith("/css") || path.startsWith("/js") || path.startsWith("/images")) {
    filterChain.doFilter(request, response);
    return;
}

추가로 permitAll()을 해도 이 필터를 안거치는 것은 아니라나뭐라나.. 그래서 인증이 필요하지 않은 특정 url은 인증처리를하면 안되는데 그래서 따로 조건문으로 분기시켰다. 다른 방식을 사용할 수 있으면 그걸 사용할 예정

 

테스트 결과도 잘 나온다.


오늘은 예전에 했던 방식이 아니라 스프링 필터체인에 올라타서 직접 필터를 수정해보았다. 아직 좀 아리까리한 부분도 있고해서 한 번 정리가 필요할 것 같다. 내일하지뭐. 내일 책도 좀 빌리고 정리도 좀 하고

 

플젝트랙에서 내가 로그인 기능을 담당하겠다고하자 코어멤버 분들이 로그인 괜찮겠냐고 여쭤보셨는데.. 확실히 어렵긴 어렵다. 단순 세션로그인이라면 스프링에게 많은 것을 일임해도 되지만, JWT로그인 방식이라면 직접 필터체인과 여러 클래스들을 상속해서 가져가야한다.

 

음..

그래서 조만간 이것을 한 번에 정리한 글을 올려볼까 싶다. 세션로그인에 대해서도 좀 조사를하고, 기존보다는 진보된 로그인 기능을 알 수 있었으면 좋겠다. 그리고 실제 배포용으로 써도 문제없도록,

'개인 프로젝트 > [2026] 골든리포트!' 카테고리의 다른 글

[골든리포트!] 8) JwtAuthorizationFilter 최종정리 및 SecurityConfig에 등록하기  (0) 2026.02.18
[골든리포트!] 7) JWT토큰 인증과정을 Spring Security 필터체인에 태워보기  (0) 2026.02.18
[골든리포트!] 5) JWT 토큰 후속기능 처리하기  (0) 2026.02.12
[골든리포트!] 4) 스프링 세션 설정 및 JWT토큰 발급  (0) 2026.02.10
[골든리포트!] 3) Spring Security Config로 스프링 OAuth 다루기  (0) 2026.02.10
'개인 프로젝트/[2026] 골든리포트!' 카테고리의 다른 글
  • [골든리포트!] 8) JwtAuthorizationFilter 최종정리 및 SecurityConfig에 등록하기
  • [골든리포트!] 7) JWT토큰 인증과정을 Spring Security 필터체인에 태워보기
  • [골든리포트!] 5) JWT 토큰 후속기능 처리하기
  • [골든리포트!] 4) 스프링 세션 설정 및 JWT토큰 발급
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59) N
        • JVM (12)
        • 백엔드 (20) N
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[골든리포트!] 6) 필터체인에서 JWT필터 인증 처리하기
상단으로

티스토리툴바