Spring Security + jwt 설정 + Trouble Shooting

형준·2024년 1월 25일

intro

Spring Security와 Jwt를 이용해서 인증과 인가를 구현했습니다.

개념 정리

https://khjoon372.tistory.com/262

버전

  • Spring boot 2.7.7
  • jwt 0.11.5
  • Spring Security 5점대

의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

Spring Config 파일 생성

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig {



    private final CustomAccessDeniedHandler accessDeniedHandler;
    private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    private final JwtAuthenticationExceptionHandler exceptionFilter;
    private final RedisService redisService;
    private final TokenProvider tokenProvider;
    @Value("${jwt.token.secret}")
    private String secretKey;

    private final UrlBasedCorsConfigurationSource corsConfigurationSource;
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        return (web) -> web.ignoring()
                .antMatchers(
                        "/favicon.ico",
                        "/health",
                        "/",
                        "/swagger-ui.html",
                        "/swagger-ui/**",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/api/members/login"
                );
    }
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {

        return httpSecurity
                .httpBasic().disable()//토큰 인증 방식으로 하기 위해서 HTTP 기본 인증 비활성화
                .csrf().disable()//CSRF 공격 방어 기능 비활성화
                .cors()
                .configurationSource(corsConfigurationSource)

                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/**").permitAll()//모든 접근 허용
                //.antMatchers(HttpMethod.POST, "/api/members/jwt/test").authenticated()//인증 필요로 접근 막기

                .and()
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .and()


                .addFilterBefore(new JwtFilter(tokenProvider, redisService), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(exceptionFilter, JwtFilter.class)
                .build();


    }
}

Jwt 토큰 생성하는 로직

사용자 정보와 email, 역할을 이용해서 jwt를 생성한다.

    public String createAccessToken(Long memberId, String memberRole, String email, Collection<? extends GrantedAuthority> authorities) {
        //30분

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.accessTokenValidityInMilliseconds);


        //토큰 생성
        return Jwts.builder()
                .setSubject(String.valueOf(memberId))
                .claim(AUTHORITIES_KEY, authorities)
                .claim("memberRole", memberRole)
                .claim("email", email)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();



    }

Jwt 토큰 있는지 체크하는 로직

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;

    private final RedisService redisService;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


        HttpServletRequest httpServletRequest = request;
        String jwt = tokenProvider.resolveToken(httpServletRequest);
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt, TokenProvider.TokenType.ACCESS)){

            // jwt는 정상적인 형태이나, 로그아웃 한 토큰인가?
            if(!redisService.validateLoginToken(jwt)) {
                logger.error("이미 로그아웃 된 토큰 발견");
                throw new JwtHandler(ErrorStatus.JWT_BAD_REQUEST);
            }
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }else{
            throw new JwtHandler(ErrorStatus.JWT_TOKEN_NOT_FOUND);
        }
        filterChain.doFilter(httpServletRequest, response);
    }
}

Trouble Shooting

문제 상황: jwt 토큰 발급 후 jwt filter를 통과하면서 회원 Id를 가져오기 위해서 jwt 토큰을 파싱해서 SpringContextHolder의 하위 객체인Authentication에 값이 담겨야 하는데 자꾸 null값이 담기는 상황이다...

해결과정

  1. 어디서부터 값이 안 담기기 시작했는지 디버깅을 해보았다.
    • tokenProvider의 resolve부터 디버깅을 했는데 jwt는 잘 만들어지는데 tokenProvider.getAuthentication에서 Authentication을 만들어내고 SecurityContextHolder에 담는 과정에서 값이 잘 안담기는 것을 확인했다.
  2. SecurityContextHolder에 어떤 값을 정확히 담아야 하는지 찾아보고 spring security에서 제공하는 UsernamePasswordAuthenticationToken객체를 만들어서 담아줬더니 값이 잘 담기는것을 확인할 수 있었다.

어디서 문제가 발생했는지 모르는 상황에선 전체적인 흐름을 알아야 해서 디버깅하면서 공부도 많이 된 것 같다.

profile
백엔드 개발자가 되기 위한 경험을 기록하는 블로그입니다.

0개의 댓글