[SpringSecurity] JwtAuthenticationFilter 구현

Coastby·2022년 12월 5일
0

LIKELION Back-End School

목록 보기
61/61
post-custom-banner

JwtAuthenticationFilter는 JWT 토큰으로 인증하고 SecurityContextHolder에 추가하는 필터를 설정하는 클래스이다.

⌨️ JwtTokenFilter

@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {      //api 요청을 할 때 한 번만 인증을 거친다??
    private final UserService userService;
    private final String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //Token에서 Claim 꺼내기
        final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        if(authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")){      //header에 AUTHORIZATION이 없거나, Bearer로 시작하지 않으면 filter
            log.error("header가 없거나, 형식이 틀립니다. - {}", authorizationHeader);
            filterChain.doFilter(request, response);
            return;
        }

        String token;
        try {
            token = authorizationHeader.split(" ")[1].trim();
        } catch (Exception e) {
            log.error("토큰을 분리하는데 실패했습니다. - {}", authorizationHeader);
            filterChain.doFilter(request, response);
            return;
        }
        log.info("token : {}", token);

        //토큰이 Valid한지 확인하기
        if(JwtTokenUtil.isExpired(token, secretKey)){
            filterChain.doFilter(request, response);
            return;
        }

        //userName 넣기, 문 열어주기
        String userName = JwtTokenUtil.getUserName(token, secretKey);
        log.info("userName : {}", userName);
        User user = userService.getUserByUserName(userName);

        //AuthenticationToken 만들기
        UsernamePasswordAuthenticationToken authenticationToken =  new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name())));
        //디테일 설정하기
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

✅ GenericFilterBean vs OncePerRequestFilter

GenericFilterBean은 기존 필터에서 가져올 수 없는 스프링의 설정 정보를 가져올 수 있게 확장된 추상 클래스이다. 서블릿은 사용자의 요청을 받으면 서블릿을 생성해서 메모리에 저장해두고 동일한 클라이언트의 요청을 받으면 재활용하는 구조여서 GenericFilterBean을 상속받으면 RequestDispatcher에 의해 다른 서블릿으로 디스패치되면서 필터가 두 번 실행되는 현상이 발생할 수 있다.

이 같은 문제를 해결하기 위해 등장한 것이 OncePerRequestFilter이며, 이 클래스도 GenericFilterBean을 상속받고 있지만, 이 클래스를 상속받아 구현한 필터는 매 요청마다 한 번만 실행되게끔 구현된다.

Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments.

어느 서블릿 컨테이너에서나 요청 당 한 번의 실행을 보장하는 것을 목표로 한다.doFilterInternal메소드와 HttpServletRequest와 HttpServletResponse인자를 제공한다.

중요한 점은 요청 당 한번의 실행을 보장한다는 것이다.

서블릿이 실행되는 동안 다른 서블릿에 요청이 올 수도 있다.

예를들어, 어느 필터에서 헤더를 확인 한 후 특정 url로 포워딩 시킨다고 가정하자.

이때 예외가 발생하지 않았다면, url로 포워딩 시키는 것 자체가 서블릿 실행 중 요청이 온 것이다.

OncePerRequestFilter를 사용하지 않았다면 앞서 거친 필터들을 또 한번 거칠 것이고, 쓸데없는 자원만 낭비하는 셈이다.

결국 동일한 request안에서 한번만 필터링을 할 수 있게 해주는 것이 OncePerRequestFilter의 역할이고 보통 인증 또는 인가와 같이 한번만 거쳐도 되는 로직에서 사용한다.

인증 또는 인가를 거치고나서 특정 url로 포워딩하면, 요청이 들어왔으니 인증 및 인가필터를 다시 실행시켜야 하지만, OncePerRequestFilter를 사용함으로써 인증이나 인가를 한번만 거치고 다음 로직을 진행할 수 있도록 한다.

출처 : https://emgc.tistory.com/119

✅ doFilterInternal()

OncePerRequestFilter에서 구현하는 메서드이다. doFilter()는 다음 filter-chain을 실행하는 것이며, 마지막 filter-chain인 경우 Dispatcher Servlet이 실행된다.

참고: https://pgnt.tistory.com/102

✅ UsernamePasswordAuthenticationToken

1) 사용자가 사용자 이름과 암호를 제출하면 UsernamePasswordAuthenticationFilterHttpServletRequest에서 사용자 이름과 암호를 추출하여 일종의 인증인 UsernamePasswordAuthenticationToken을 생성합니다.

2) 다음으로 UsernamePasswordAuthenticationToken이 인증을 위해 AuthenticationManager로 전달됩니다. AuthenticationManager의 모양에 대한 세부 정보는 사용자 정보가 저장되는 방식에 따라 다릅니다.

3) 인증이 실패하면,

  1. SecurityContextHolder가 지워집니다
  2. RememberMeServices.loginFail이 호출됩니다. 기억하기가 구성되어 있지 않으면 작동하지 않습니다.
  3. 다시 보낼 WWW-Authenticate을 트리거하기 위해 AuthenticationEntryPoint가 호출됩니다.

4) 인증에 성공하면,

  1. SessionAuthenticationStrategy는 새 로그인에 대한 알림을 받습니다.
  2. 인증은 SecurityContextHolder에서 설정됩니다.
  3. RememberMeServices.loginSuccess가 호출됩니다. 기억하기가 구성되어 있지 않으면 작동하지 않습니다.
  4. ApplicationEventPublisher는 InteractiveAuthenticationSuccessEvent를 게시합니다.
  5. AuthenticationSuccessHandler가 호출됩니다. 일반적으로 이것은 로그인 페이지로 리디렉션할 때 ExceptionTranslationFilter에 의해 저장된 요청으로 리디렉션되는 SimpleUrlAuthenticationSuccessHandler입니다.

✅ WebAuthenticationDetailsSource

✅ SecurityContextHolder

  • Principal: 보호 된 리소스에 접근하는 사용자 식별자(아이디)
  • Credentials: 인증을 위해 필요한 정보(비밀번호)
  • GrantedAuthority: 인증 된 사용자의 인증 정보(역할 등)을 표현
  • SecurityContext: Authentication 객체를 포함하고 있으며, SecurityContextHolder를 통해 접근할 수 있다.

Spring Security 주요 모듈 (3 기준)

  1. 아이디/패스워드 사용자 정보를 넣고 실제 가입된 사용자인지 체크한 후 인증에 성공하면 사용자의 principal과 credential 정보를 Authentication에 담는다.
  2. Spring Security에서 Authentication을 SecurityContext에 보관한다.
  3. SecurityContext를 SecurityContextHolder에 담아 보관한다.
profile
훈이야 화이팅
post-custom-banner

0개의 댓글