JWT 로그인 구현하기

이수찬·2023년 5월 15일
0

https://velog.io/@suzhanlee/Oauth2-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

위의 글과 이어지는 글이다.

JWT를 처리하는 인증 필터를 만들어보자!

Header에 담겨오는 token을 통해 인증절차를 수행하려면, 기존의 Spring Security의 기존 필터인 UsernamePasswordAuthenticationFilter를 사용할 수 없다.

그래서 CustomFilter를 구현할 때 자주 사용하는 OncePerRequestFilter를 상속 받아 Jwt를 처리할 수 있는 필터를 만들었다.
말 그대로 하나의 요청마다 작동하는 필터이다.

1. Custom Filter 만들기

1-1. Custom Filter 로직

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenFactory jwtTokenFactory;

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

        try {
            authentication(token);
        } catch (ExpiredJwtException e) {
            request.setAttribute("JWT Exception", JwtExceptionCode.EXPIRED);
        } catch (UnsupportedJwtException e) {
            request.setAttribute("JWT Exception", JwtExceptionCode.UNSUPPORTED);
        } catch (MalformedJwtException e) {
            request.setAttribute("JWT Exception", JwtExceptionCode.MALFORMED);
        } catch (SignatureException e) {
            request.setAttribute("JWT Exception", JwtExceptionCode.INVALID_SIGNATURE);
        } catch (IllegalArgumentException e) {
            request.setAttribute("JWT Exception", JwtExceptionCode.INVALID);
        }

        filterChain.doFilter(request, response);
    }

    private void authentication(String token) {
        if (StringUtils.hasText(token) && token.startsWith(TOKEN_PREFIX)) {
            token = token.substring(TOKEN_PREFIX.length());
            Authentication authentication = jwtTokenFactory.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
    }

}
  1. request의 header에서 token을 가져온다.
  2. header에서 가져온 token을 실제로 검증하는 로직 수행
    2-1. 실제 토큰이 존재하는지 검증
    2-2. 토큰이 Bearer로 시작하는지 검증
    2-3. 인증이 완료된 객체를 SecurityContext에 저장한다.
    이는 나중에 @AuthenticationPrincipal로 자신의 회원 정보 가져오기와 같은 API를 편리하게 사용할 예정이기 때문에 SecurityContextHolder에 저장했다.
  3. 예외가 발생하지 않으면, 다음 필터로 넘어간다.

1-2. Token으로 회원정보 가져와 인증 객체 만들기

JwtTokenFactory

public Authentication getAuthentication(String token) {
        Claims claims = parseClaims(token);
        String email = claims.get(EMAIL, String.class);
        ClientType clientType = ClientType.valueOf(claims.get(CLIENT_TYPE, String.class));
        MemberContext memberContext = MemberContext.create(email, clientType);
        return new UsernamePasswordAuthenticationToken(memberContext, null, null);
    }

private Claims parseClaims(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
  • ParseClaims()로 secretKey를 이용해 token을 Claims로 변환한다.
    claims에서 memberContext라는 객체를 생성해 UsernamePasswordAuthenticationToken에 넣는다.
  • parseClaimsJws에서 throw하는 예외들을 authenticaton(token)에서 try-catch로 처리했다.

2. Security Config

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtTokenFactory jwtTokenFactory;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
                .httpBasic().disable();
        http
                .csrf().disable();
        http
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http
                .authorizeHttpRequests(authorize -> authorize.requestMatchers(AUTH_WHITELIST).permitAll()
                        .anyRequest().authenticated());
        http
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenFactory), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .authenticationEntryPoint(new OauthAuthenticationEntryPoint());

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
  • JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter앞에 두어 JWT를 통해 우선적으로 인증을 수행하게 설정한다.

3. 인증된 회원의 정보 바로 가져오기

MemberController

@GetMapping(ApiPath.MEMBER_MYSELF)
    public FindMySelfRs findMySelf(@AuthenticationPrincipal MemberContext memberContext) {
        return findMemberService.findMySelf(memberContext);
    }
  • 위 API는 로그인한 회원 자신의 정보를 가져오는 API이다.
  • @AuthenticationPrincipal를 사용하면 UserDetailsService에서 리턴한 객체를 파라미터로 직접 받아 사용할 수 있다.
    @AuthenticationPrincipal가 SecurityContextHolder에 존재하는 SecurityContext의 Authentication에 저장한 객체를 가져온다.

4. 정리

  • 클라이언트는 서버에 요청을 보낸다.
  • 만약 요청이 인증을 필요로 하는 요청이면, CustomFilter가 작동한다.
  1. 요청의 header에 토큰이 있는지 확인한다.
  2. 토큰이 존재한다면, 토큰 유효성 검사를 실행한다.
  3. 토큰을 파싱해 회원 정보를 얻어, 인증 객체를 생성한다.
  4. 인증객체를 SecurityContext에 담고, sc를 SecurityContextHolder에 넣는다.

0개의 댓글