본문 바로가기

FrameWork/Spring Boot

SpringBoot - Interceptor와 JwtToken을 이용한 App API 요청 인증

interceptor, JWTTOken

Spring InterceptorJWT Token을이용한 App(모바일) 인증 구현에 대해 알아보겠습니다.

 

이번 포스팅에서는 전체적인 App의 인증 과정에대한 시나리오를 알아보고, 사용자가 회원가입시 토큰을 저장하고, 로그인시 저장된 토큰을 받는 부분까지 구현해보도록 하겠습니다.

 

 

1. 환경

IDE

  • WebStorm
  • Intellij

 

Build tool

  • gradle

 

FrameWork

  • Spring

 

Language

  • JAVA

 

 

 

 

2. 시나리오

전체 시나리오

  1. 사용자가 회원가입을 하면, auth0 JWT를 이용해 Token을 생성하여, 사용자의 데이터와 함께 저장합니다.


 

  1. 사용자가 ID, PW을 이용해 로그인을 하게되면, 서버에서 사용자의 Token을 반환해줍니다.


 

  1. Client측에서, Token을 저장한뒤, 검증이 필요한 Server의 서비스를 이용할 때, 요청의 HeaderToken을 포함시킵니다.

 

  1. Server에서 토큰을 검증 후, API 응답을 합니다.

 

 

서버의 토큰검증 시나리오

  1. 검증이 필요한(토큰이 필요한) API 이용시, Client의 요청 Header에 Token을 포함해 요청합니다.
  2. Spring의 Interceptor에 의해 요청이 Intercept 됩니다.
  3. 해당 사용자에게 제공되었던 Token과, 요청의 Header에 담긴 Token이 일치하는지 확인합니다.
  4. 마지막으로 auth0 JWT를 이용해 issuer, expire를 검증합니다.

 

 

 

 

3. 실습

3.1 프로젝트 생성

앞서 말씀드렷듯이 Intellij를 이용해서 Spring Boot 프로젝트를 생성합니다. build toolgradle을 이용하였고, java version1.8입니다.

 


Lombok을 체크합니다.

 


spring web을 체크합니다.

 


jpah2 db를 선택합니다.

 

 

 

3.2 프로젝트 구조


빨간색, 주황색, 초록색 별로 같은 Depth에 위치한 디렉토리입니다.

  • common : 어플리케이션에서 전체적으로 사용되는 기능들이 위치함
    • config : Spring 설정파일들이 위치함
  • domains : 도메인들이 위치함
    • user(도메인 이름) : 각각의 도메인
      • presentation : 사용자의 endpoint로 사용자와 소통하는 UI로직(Controller)들이 위치함
      • service : application(service) 계층에 해당하는 로직들이 위치함
      • domain : 도메인 계층에 해당하는 로직들이 위치함
      • infra : 위의 3계층이 공통적으로 사용할 기능과 외부 모듈등이 위치함

 

 

저는 DDD시 위와 같은 디렉토리형태로 구조를 잡고 진행을 하는데요, 첫째로 common과 domains를 분리함으로써, 도메인로직들은 domains 디렉토리 하위에 존재함을 직관적으로 볼수 있기때문에, 도메인로직에 좀더 집중할 수 있습니다.

 

또한 domains디렉토리에서 다시 어플리케이션에서 사용되는 domain들이 나뉘고, 그 안에 layerd architecture의 형태로 디렉토리 구조를 잡음으로써, ui 계층로직, service 계층로직, domain 계층 로직, infra 계층 로직을 다시 분리하여, 각각의 domain들의 도메인로직에 집중할 수 있기 때문에, 매우 효율적인 구조로 느껴졌기 때문입니다.

 

 

 

3.3 구현

 

domain 계층


위의 구조를 가지는 User Entity를 생성합니다. user는 login시 사용될 email, password와 api 요청시 사용될 token을 필드로 가집니다. (userEmail, userPassword, tokenCredential이라는 하나의 논리적인 Value 타입으로 변경할 수도 있지만, 그렇게 하지 않았습니다. User가 현재 Application에서는 Login을 하는 User라는 것 이외의 특별한 역할을 하는 domain은 아니기 때문입니다.)

 

 

UserRepository 는 Interface로 JpaRepository<User, Long>를 구현함으로써, User Entity의 영속성 관리를 하며 User와 관련된 CRUD메소드를 자동으로 생성합니다.

또한 Optional<User> 객체를 반환하는 findByUserEmail() 메소드를 가지는데, 이 메소드는 사용자가 로그인시 DB에 저장된 User의 정보를 가져오기 위해 사용합니다.

 

 

 

Service(Application) 계층


 

UserService클래스에는 Presentation계층의 UserController에서 사용할 signUp(), signIn()메소드가 존재합니다. Service 계층의 메소드들은 인자로, 자신들이 사용할 객체를 전달받습니다.

 

 

먼저 signUp()메소드는 SignUpRequest를 인자로 전달받는데요, 사용자가 회원가입시 요청한 email, password가 담겨 있습니다. 우선, 요청한 email을 인자로 받는 verifyDuplicatedUser() 메소드를 호출하여, 중복된 email이 존재하는지 찾습니다.

중복되지 않는다면, jwtUtilcreateToken() 메소드를 이용하여 token을 생성하여, 사용자가 요청한 정보와 함께 새로운 User Entity를 생성하고, UserRepository를 이용해 이를 영속화 합니다.

 

이번 포스팅에서는, 사용자가 로그인 후, 토큰을 전달받는 과정까지만 해볼 것이므로, 회원가입시 사용자에게 전달될 정보가 없습니다. 따라서 signup의 반환형은 void로 합니다.

 

 

signIn() 메소드는 SignInRequest를 인자로 전달받습니다. 우선, 아까 전 UserRepository에 정의한 findByUserEmail() 메소드를 이용해, 사용자의 요청에 담긴 Email에 해당하는 User가 서버에 존재하는지 확인합니다. 사용자가 존재한다면, 그 사용자의 Password와 비교하여 일치하는 경우, Token을 가지는 SignInResponse 객체를 반환합니다. (원래 사용자 정보 보호법에 의해, 사용자의 비밀번호를 서버에 저장할 시에는 암호화를 한뒤 저장을 해야합니다. 암호화 시에는 단방향 해시 알고리즘을 통해 관리자또한 비밀번호를 확인할 수 없도록 해야하며, 이때는, 사용자의 요청을 같은 단방향 해시 알고리즘으로 해싱하여 해싱된 값과 저장된 값을 비교하여 로그인 검증을 합니다.)

 

 

JwtUtil은 UserService에서 필요로하는 기능을 정의하고 있는 interface로, 구현체는 infra 계층을 나타내는 디렉토리에 존재합니다. 이는 DIP를 위한 설계로, Service계층에 해당하는 로직이 infra 계층 의 로직에 의존하지 않고, infra계층의 로직이 Service계층에 의존하도록 합니다. 이를 통해 infra 계층 코드의 변화로 부터 service 계층이 자유로울 수 있습니다.

 

 

 

Infra 계층




바로 위에서 말씀드린 JwtUtil의 구현체입니다. auth0 Jwt 라이브러리를 이용했습니다.

 

auth0 jwt를 사용하기 위해서는, build.gradle의 dependencies 블록에 등록을 해주어야 합니다.

 

 

 

Presentation(UI) 계층



UserController는 RestController로, 사용자의 요청에 따라 적절한 서비스를 실행한뒤, 응답을 반환합니다.

 

 

 

 

Interceptor

사용자가 Server에 요청시 특정 url을 제외한 거의 모든 기능은 Token을 필요로합니다. 따라서 이를 각각의 모든 handler에서 검증하는 것이 아니라, handler로 요청이 전달되기 직전인, Interceptor에서 이를 검증한다면 훨씬 효율적입니다.

 

우선 HandlerInterceptor를 구현합니다. 그 후, preHandle() 메소드를 override하고, 이곳에 token 검증로직을 추가하면 됩니다.

preHandle()이 반환하는 boolean 값에따라 요청이 제어되는데요, verifyToken() 메소드에서 token을 검증하고, 실패한 경우에는 exception을 발생시킵니다. 모든 검증이 완료된 후, true를 반환하게 되면, 이 요청이 원래의 목적 handler에게 전달됩니다.

 

 

Config

WebMvcConfigurer를 구현하여, WebMVC를 편하게 수정할 수 있도록 합니다. addInterceptors()를 override 하여 앞서 생성한 JwtAuthInterceptor()를 등록하고, Intercept할 url pattern을 지정하고, 마지막으로 제외할 url pattern을 등록하면 끝입니다.