[Back-End] 스프링 프로젝트에 Jwt토큰 적용해보기!(1) - 설정 적용

    Intro


    프로젝트에 세션 인증 방식을 적용할 지, 토큰 인증 방식을 적용할지 여부에 대해 고민을 해보다가, 최종적으로는 토큰 인증 방식을 진행해보기로 했다. 사실 제대로 된 토큰 인증 방식을 적용해보는 것이 처음이기 때문에 내가 옳은 방법인지 아닌지에 대해서는 정확히 판단할 수 없지만, 내가 생각한 의식의 흐름과 진행과정을 적어보고자 한다.

     

    인증 / 인가를 판별하는 위치는 어디가 적당할까?


    Jwt토큰을 사용하는 것은 결국에는 사용자의 인증 및 인가를 관리하기 위함이다.

    그렇다면 스프링에서 인증 인가를 어느 부분에서 진행해야 할까? 생각해볼 수 있는 부분은 3가지이다.

    1. 컨트롤러 진입 이전
    2. 컨트롤러에 진입하고 서비스 로직에 들어가기 이전
    3. 서비스 로직 부분

    결국 사용자를 식별하는 것은, 서비스를 이용하기 위함이다. 서비스를 이용하기 위해서는 요청에 맞는 컨트롤러에 진입하고 서비스 과정을 처리하는 부분으로 진입하게 된다.

    맨 처음 이런 고민을 진행하게 된다면 생각할 수 있는 부분은 다음과 같을 것이다.

    "어차피 서비스를 사용할 때에 유저를 식별하면 되니까 2번이나 3번 부분에서 유저를 찾고 그에 맞게 진행을 하면 되지 않을까?"

    하지만 그렇게 된다면, 모든 컨트롤러 혹은 서비스 로직에 유저를 찾는 부분이 항상 공통적으로 들어가야 하게 된다.

    또한, 컨트롤러 단계에서 불필요한 Repository의 의존성을 가져가야 할 수도 있으며, 매번 유저를 찾는 부분을 공통적으로 처리할 수 있는 방안을 선택하는 것이 조금 더 적절해 보인다.

    추가적인 문제로는, 비회원 / 회원이 사용할 수 있는 서비스가 별개로 있는 경우에도, 비회원인지 회원인지 여부를 컨트롤러까지 직접 도달해야만 알 수 있다. (예를 들어, 게시글 조회는 누구나 가능하지만, 게시글 작성은 로그인한 유저만 가능하다)

    이러한 방법 또한 해결방안이 될 수 있지만, 만약 프로젝트의 크기가 크고 컨트롤러가 많을 경우 중복 코드의 개수가 늘어나게 되고, 추후 코드를 수정하는 부분에 있어서 많은 불편을 초래할 수 있을 것이라 생각한다.

    그렇다면 컨트롤러 진입 이전에 인증 및 인가 부분을 체크하는 것이 좋은데 이를 어떻게 진행하는 것이 좋을까?

    Spring Security는 필터를 기반으로 동작하게 되며, "SecurityContext" 라는 것을 이용하여 인증 정보를 관리하게 된다.

    간단하게 설명하자면, 컨트롤러 진입 이전에 필터에서 요청으로 들어온 jwt토큰을 디코딩하여 어떤 유저인지 파악을 하고, 유저에 해당하는 Authentication 객체를 생성하여 이를 이용해서 간편하게 사용할 수 있다.

    또한, 스프링에서 컨트롤러에 진입하기 이전에 거치는 filter 단계에서 판별을 진행할 것인데, 스프링 자체적으로 내가 발급한 토큰을 해석하고 Authentication 객체를 등록해주는 부분은 없기 때문에, 별도의 커스텀 필터를 생성해야 한다.

     

    SecurityConfig 및 Custom Jwt Filter 적용


    먼저, 우리가 거르고 싶은 부분은 회원만 이용할 수 있는 서비스의 경우만 거르고 싶은 것이기 때문에 SecurityConfig에서 먼저 설정을 진행해준다.

    위의 코드에서 확인할 수 있듯이, 비회원이 접근할 수 있는 엔드포인트(메인 페이지, 회원가입, 로그인 등)와 스웨거 설정에 필요한 부분들을 하나의 String배열로 만들고 permitAll() 을 해준다. 그 이외의 엔드포인트는 인증을 필요로 하기 때문에 authenticated()로 설정해주었다.

    SecurityConfig 설정을 마쳤다면, Custom Jwt 필터를 생성해준다.

    필터의 경우 OncePerRequestFilter 을 상속받았다. GenericFilterBean을 상속받아 사용할 수도 있지만, GenericFilterBean은 모든 요청에 대해서 필터를 거치게 된다. 즉, reidrect같은 경우에서, 실질적인 request는 1번이지만, 필터는 2번을 타게 되는 단점이 존재한다. 따라서 한 번의 request에 한 번의 filter만 통과하는 OncePerRequestFilter가 더 적합하다.

     

    Custom Jwt filter의 역할은 2개로 구분할 수 있다.

    1. 토큰이 필요하지 않은 서비스의 경우(비회원 서비스 등) 별도의 체크를 진행하지 않고 넘긴다(SecurityConfig와 같이)
    2. 토큰이 필요한 서비스의 경우, request에 있는 토큰을 파악하고 이에 따라 Authentication 객체를 생성한다.

    먼저 1번 기능을 구현하기 위해서 request의 URI를 파악하고, 이에 따라서 인증 과정을 무시해도 되는지 아닌지를 판별한다.

    만약 인증 과정이 필요하지 않은 경우 다음 필터로 과정을 넘기고 return; 을 통해서 마친다.(ignoreTokenRequest() 메소드의 경우 상황에 맞춰서 적절히 설정해주자)

     

    2번 기능을 구현하기 위해서는 먼저, request의 헤더와 쿠키에서 accessToken과 refreshToken을 알아낸다.

    그 후, 필요한 검증 과정을 마친 후 CustomUserDetails을 통해서 security 객체를 생성하고, AuthenticationUser로 등록한다. 그런 후에 다음 필터로 진행을 시킨다.

     

    jwt필터의 위치는 UsernamePasswordAuthenticationFilter 이전에 위치시켰다. 이유는 다음과 같다.

    UsernamePasswordAuthenticationFilter 의 경우 Authentication객체가 존재하면 사용되지 않는다. -> 우리는 토큰을 이용해서 커스텀필터에서 authentication객체를 생성할 것이다.

    아래와 같이 JwtConfig 파일을 만든 후에

    이전 설정해준 SecurityConfig 파일에서 다음 부분을 추가해 주었다.

    또한 에러 발생 시의 응답을 처리하기 위해서

    두 가지 핸들러를 추가해주었다.(위 SecurityConfig에서는 이미 등록되어 있음)

     

    이로서 전체적인 설정에 대한 부분은 끝나게 되었고, 이후에는 실제로 토큰 발행과 인증 등을 어떻게 진행하였는지에 대해 작성해보려 한다.

    댓글