이번 yapp 어플리케이션 백엔드 프로젝트에서는 유저 인증 시 token 기반 인증을 구현하기로 했다.

token기반의 인증은 stateless api를 활용할 수 있다는 장점이 있다.

또한, 토큰이 탈취된다 하더라도, 유저의 기본 정보가 노출되지 않는다는 점이 장점이라고 할 수 있다.

뿐만 아니라, 프론트가 안드로이드 애플리케이션인데, 모바일 애플리케이션에서 jwt 기반 인증을 사용하는 이유는 다음과 같다.

모바일 트렌드 중 하나는 로그아웃을 하지 않는 이상 로그인을 유지하는 것입니다.
일반적으로는 손쉽게 Session을 이용해서 클라이언트와 서버 통신 중 Stateless의 단점을 보완할 수 있었지만, 모바일의 특성상 자주 끊길 소지가 있습니다.

세션과 비슷한 역할을 하되, 계속해서 유지될 수 있는 기술을 찾다 보니 Token을 이용한 방식이 있었고, 그중 JWT를 사용하게 되었습니다.

 

 

암튼 게시판을 구현하다가, 게시판 기능에서 요청 헤더에서 토큰을 가져오고,

이 토큰에 들어있는 user_id를 검증 해야하는 모든 controller에 공통된 로직이 있었다.

 

 

 

Interceptor

Interceptor란 컨트롤러에 들어오는 요청 HttpRequest와 컨트롤러가 응답하는 HttpResponse를 가로채는 역할을 한다.

따라서 게시판 기능으로 들어오는 모든 request를 가로채서 토큰의 검증에 사용해 보려 한다.

 

spring security를 사용해서도 토큰 기반 인증을 구현할 수 있지만, interceptor를 사용해서 가볍게 구현해 보았다.

 

 

 

  • preHandle: 컨트롤러가 호출되기 전에 실행된다.
  • postHandle: 컨트롤러가 실행된 후에 호출된다.
  • afterComplete: 뷰에서 최종 결과가 생성하는 일을 포함한 모든 일이 완료 되었을 때 실행된다.

 

Interceptor를 구현하기 위해서는 먼저 HandlerInterceptorAdapter를 구현한 클래스를 만든다.

@Component
@NoArgsConstructor
public class AuthInterceptor extends HandlerInterceptorAdapter {

	private UserRepository userRepository;
	private Auth auth;

	@Autowired
	public AuthInterceptor(UserRepository userRepository, Auth auth) {
		this.userRepository = userRepository;
		this.auth = auth;
	}

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		String token = request.getHeader(HttpHeaders.AUTHORIZATION);
		verifyToken(token);

		Long userId = auth.parseUserIdFromToken(token);
		log.info("[Request User ID] " + userId);
		User user = findUserById(userId)
				.orElseThrow(() -> new UserNotFoundException("[INTERCEPTOR Exception] user not found"));

		checkUserStatus(user.getStatus());
		return super.preHandle(request, response, handler);
	}

	private void checkUserStatus(UserStatus userStatus) {
		if (userStatus == UserStatus.INACTIVE) {
			throw new InactiveUserException("inactive user");
		} else if (userStatus == UserStatus.SUSPENDED) {
			throw new SuspendedUserException("suspended user");
		}
	}

	private Optional<User> findUserById(Long userId) {
		return userRepository.findUserById(userId);
	}

	private void verifyToken(String token) {
		auth.verifyToken(token);
	}
}

prehandle에서는 auth 클래스에서 구현한 토큰 검증이 진행된다.

이후, claim에 들어있는 userId를 가져와서 데이터베이스에서 user를 조회 및 user status를 확인한다.

 

 

그 다음으로는 adapter를 애플리케이션에 붙이기 위해서 WebConfiguration에 붙여준다.

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

	private AuthInterceptor authInterceptor;

	@Autowired
	public WebConfiguration(AuthInterceptor authInterceptor) {
		this.authInterceptor = authInterceptor;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(authInterceptor)
				.addPathPatterns("/**");
	}
}

첨에는 authInterceptor를 autowired 안해줬어서 제대로 실행되지 않았었다ㅠㅠ

 

 

마지막으로 @ControllerAdvice를 사용해서 컨트롤러에서 발생하는 exception을 모두 잡도록 했다.

+ Recent posts