나의 스프링 시큐리티/JWT 해방일지

백여우·2022년 9월 23일
53
post-thumbnail

제가 처음 스프링 공부를 시작할 때는
소위 말하는 '야생형' 스타일, '백문이 불여일타' 스타일을 추구하며
클론 코딩 서적을 보고 따라 쳐보는 공부를 많이 했어요.

원리 이해보다 프로젝트 완성에 초점을 두다 보니 서적의 챕터 하나마다 '이게 뭐지'를 한 3만 번 외친 것 같은데,

그 중에서도 도저히도저히 도오저히 이해할 수 없는 챕터가 있었으니 그게 바로 '스프링 시큐리티를 이용한 인증/인가' 챕터였어요.

스프링 프레임워크에서 제공하는 여러 라이브러리 중에서도 유독 배우기 어려운 주제가 바로 스프링 시큐리티이기 때문이에요.

서론: 왜 스프링 시큐리티는 어려울까?

(바쁘시면 해당 소주제는 건너뛰세욥!)
위 질문에 대해 생각해 보기 전에 질문의 관점을 약간 바꾸어,
'그렇다면 시큐리티를 제외한 스프링 라이브러리는 쉬울까?'
그리고 '쉽다는 것은 무슨 뜻일까?' 를 한 번 생각해 보면 좋을 것 같아요.


스프링을 처음 배우기 시작하면 맨날 보는 컨트롤러 메서드에 대해 한 번 생각해 봅시다!

Spring initializr에서 딱 Spring Web 의존성만 추가하여 자바 스프링 부트 프로젝트를 만들고, 메인 메소드 클래스가 있는 패키지 아래에 다음과 같은 초간단 컨트롤러를 하나 만들어 보아요. (따라하지 마십시오!)

package com.example.demo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
    @GetMapping("/hello")
    public @ResponseBody String hello() {
        return "hello";
    }
}

해당 프로젝트에서 실행한 서버 주소(http://localhost:8080)로 들어오는 모든 요청은 HttpServlet을 상속한 DispatcherServlet에서 처리합니다. (DispatcherServlet는 아주 중요한 개념이므로, 처음 들어보시거나 무엇인지 잘 모르신다면 김영한님의 스프링 mvc 강의를 한 번 쭉 보시면서 개념을 익혀두시는 것을 추천드려요!)

이 DispatcherServlet이 동작하는 원리를 디버깅 과정을 통해 살펴봅시다. (자세히 보지 마십시오.)

  1. DispatcherServlet 설정
    1-1. http://localhost:8080/hello를 호출하여 DispatcherServlet이 처음 사용되었습니다. DispatcherServlet의 초기화를 위해 onRefresh(ApplicationContext context)가 호출됩니다.
    1-2. onRefresh 메소드 내부적으로 initStrategies(ApplicationContext context)가 호출됩니다.
    1-3. initStrategies 메소드 내부적으로 initMultipartResolver, initLocaleResolver, initThemeResolver, initHandlerMappings, initHandlerAdapters, initHandlerExceptionResolvers, initRequestToViewNameTranslator, initViewResolvers, initFlashMapManager 메서드가 호출되며 DispatcherServlet의 필드들을 ApplicationContext 설정값에 알맞게 초기화합니다.
  2. DispatcherServlet 실행
    2-1. DispatcherServlet의 doService(HttpServlet의 메소드)가 실행됩니다. 내부적으로 doDispatch(HttpServletRequest request, HttpServletResponse response)가 실행됩니다.
    2-2. doDispatch 메소드 내부적으로 getHandler(HttpServletRequest request)가 호출됩니다. getHandler 메소드에서는 1-3에서 등록된 HandlerMapping 리스트를 foreach문으로 호출하며 요청 정보(/hello, GET)에 알맞는 핸들러(우리가 만든 HelloController!)를 찾아, 필요한 인터셉터를 추가해 HandlerExecutionChain을 생성한 후 반환합니다.
    2-3. 2-2에 이어서 getHandlerAdapter(Object handler)를 호출합니다. 1-3에서 DispatcherServlet의 필드에 저장한 HandlerAdapter 리스트 중, 2-2에서 찾은 HelloController 핸들러를 처리할 수 있는 어댑터를 찾아 반환합니다. 이번 요청의 경우 RequestMappingHandlerAdapter를 찾아냅니다.
    2-4. 2-3에서 찾은 adapter의 handle(HttpServletRequest request, HttpServletResponse response, Object handler) 메서드를 호출합니다. 해당 메서드를 호출한 클래스는 AbstractHandlerMethodAdapter인데, 내부적으로 구현체 어댑터 클래스(=2-3에서 찾은 어댑터 클래스)의 handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) 메소드를 다시 호출해 handle 로직을 위임합니다.
    2-5. handleInternal 메소드 내부에서 invokeHandlerMethod, 그 내부에서 invokeAndHandle, 그 내부에서 invokeForRequest, 그 내부에서 doInvoke, 그 내부에서 invoke, 그 내부에서 invoke, 그 내부에서 invoke, 그 내부에서 invoke 한 후에 드디어 HelloController.hello() 메소드가 실행됩니다.
    2-6. hello()메소드에서 hello를 리턴받은 ServletInvocableHandlerMethod.invokeAndHandle 메소드 내부에서 handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest)를 호출합니다. 컨트롤러에 붙은 어노테이션과 리턴 타입을 바탕으로 리턴값을 처리하기에 알맞은 HandlerMethodReturnValueHandler를 찾는데, 우리의 경우 @ResponseBody 어노테이션에 String 타입의 값을 반환하므로 RequestResponseBodyMethodProcessor가 알맞는 핸들러입니다.
    2-7. RequestResponseBodyMethodProcessor의 handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) 메소드가 호출됩니다. 적절한 request, response 객체를 만든 후 내부적으로 writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) 메소드를 호출합니다.
    2-8. 우리 컨트롤러의 리턴값(="Hello")를 화면에 출력하기 위해 적절한 MessageConverter를 찾습니다. 우리의 경우 String을 반환했으므로 StringHttpMessageConverter가 할당되고, 해당 컨버터의 write 메소드가 호출되어 http://localhost:8080/hello화면에 hello가 출력됩니다.
    2-9. 여러 후속 처리 작업을 실행한 후 DispatcherServlet의 doService 메소드가 역할을 다합니다.


우리가 무슨 부귀영화를 누리겠다는 것도 아니고, 화면에 겨우 hello 하나 적겠다는데 이렇게 고단한 과정이 필요하네요! 。゚(゚´ω`゚)゚。

단순한 컨트롤러 로직 하나도 이렇게나 배배 꼬여있는데, 우리가 흔히 사용하는 JPA나 validation(값 검증)과 관련된 동작 원리들도 어마어마하게 복잡할 거예요.

그렇다면 스프링에서 제공하는 컨트롤러를 결코 '쉽다'라고 말하기는 어려울 것 같습니다.

 
하지만 우리가 스프링에서 컨트롤러, JPA 등을 사용하면서 그 내부 동작원리까지 세세하게 알고 있어야만 사용할 수 있던가요?

앞서 설명되어 있듯, 우리가 한 거라곤 스프링 부트 프로젝트 하나 생성해서 아래의 간단한 메소드 하나 만든 게 전부인 걸요!

@Controller
public class HelloController {
    @GetMapping("/hello")
    public @ResponseBody String hello() {
        return "hello";
    }
}

적절한 위치에 적절한 어노테이션를 준수하여 메소드를 만들어 두기만 하면, 요청 주소를 비교해 우리가 만든 헬로컨트롤러를 찾고, 반환값을 분석해 화면에 hello를 출력해 주는 등의 복잡한 작업들은 스프링 프레임워크에서 자동으로 처리해 줍니다.

사용자는 필요한 규칙을 지키며 사용하기만 하면 내부 동작 원리는 보이지 않는 곳에서 대신 처리해 주니 사용자는 내부 동작 원리를 몰라도 상관이 없는 것!

같은 표현을 쪼금 어렵게 하면 '사용자에게는 동작에 필요한 최소한의 인터페이스만 제공하고, 내부 동작 원리는 캡슐화되어 사용자에게 노출되지 않는 것'.

우리가 지금까지 사용해 온 스프링의 라이브러리들은 위 철학을 아주 잘 지켜주고 있었기 때문에, 인터페이스만 사용하는 우리의 입장에서는 스프링이 사용하기에 쉽게 느껴진 거에요!


 
왜 뜬금없는 컨트롤러 얘기까지 꺼내며 빙빙 둘러 이야기를 하느냐,

위에서 설명드린 다른 스프링 라이브러리들에 비해, 스프링 시큐리티를 사용할 때는 인터페이스만 알고 있어서는 정상적으로 사용하기가 너무 힘들다는 것을 설명하기 위함이에요.

 
우리가 어떤 프로젝트를 만들든 로그인/회원가입 등의 인증과 인가(권한에 따른 접근) 기능을 도입할 때 사용자 정보를 표현하는 사용자 객체를 만들게 되는데, 어떤 프로젝트냐에 따라 그 사용자 객체가 갖추어야 하는 내부 구성이 다양하기 때문에 대부분 우리가 사용자 객체를 직접 만들어 사용하게 돼요.

문제는 우리가 직접 만든 사용자 객체의 경우에는
이 객체를 가지고 로그인을 진행하는 절차,
로그인이 완료된 후 인증 완료 정보를 만들고 저장하는 절차,
클라이언트에 인증 정보를 저장하게 하고 이후에 다른 요청이 들어올 때 인증 정보를 함께 보내게 하는 절차,
요청 시 함께 들어온 인증 정보로 로그인 여부를 검사하는 절차 등등 또한
직접 만들고 적절한 위치에 직접 배치해야 합니다.

(자세히 보지 마십시오.)그럼 아이디와 비밀번호를 사용해 로그인할 때 사용되는 필터인 UsernamePasswordAuthenticationFilter를 상속하여 JwtAuthenticationFilter의 attemptAuthentication 메소드를 오버라이딩하는데 내부적으로 authenticationManager의 authenticate 로직을 수행하는데 이 때 UserDetailsService의 loadUserByUsername을 호출하므로 로그인을 진행하는 UserDetailsService를 상속한 CustomDetailsService의 loadUserByUsername을 오버라이딩하여 커스텀한 로그인을 진행하는데 이 메소드는 UserDetails를 반환하므로 우리가 직접 만든 사용자 객체를 필드로 갖고 있으면서 UserDetails를 상속하는 CustomDetails를 만들어 반환하게 해 로그인 로직을 완성, 로그인 성공 시 수행되는 successfulAuthentication 메소드도 오버라이딩하고 내부적으로 JWT토큰을 만들어 리스폰스 Authorization 헤더에 넣게 설정, 해당 필터가 동작하려면 설정 클래스에서 SecurityFilterChain을 빈으로 등록하면서 HttpSecurity에 JwtAuthenticationFilter를 addFilter 메소드로 등록하고 ··· ··· ···

보셨듯 인증/인가 절차를 직접 구현하면서 적절한 클래스를 상속하고 적절한 메소드를 오버라이딩, 적절한 위치에 서비스나 필터를 등록하는 작업을 정말 오지게 많이 하게 돼요.

이 작업을 원활하게 하려면 스프링 시큐리티가 내부적으로 어떤 동작 원리를 가지는 지 구체적으로 숙지하고 있어야 하고, 바로 이 점이 스프링의 다른 라이브러리와 비교하여 학습 난이도를 확 끌어올리는 주범입니다!

그래서 저는 스프링 시큐리티를 원활하게 공부하기 위해서는
스프링 시큐리티의 구체적인 구조와 원리를 분석하고 이해하는 과정을 꼭 거친 후에
실제 프로젝트에 적용하는 다양한 방법들을 반복해 훈련하면서,
이론적으로 알고 있는 지식들을 훈련을 통해 체화하는 것이 가장 좋은 공부방법이라고 생각을 해요.

제가 위 방식대로 공부하면서 도움을 받았던 강의/서적들을 소개해드리려고 글을 작성했는데
서론이 진짜 무진장 기네요. (;ω;`)
드디어 본론으로 들어가, 제가 어떤 자료를 보면서 독학했는지 회고해 보겠습니다!


원리 이해 : 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security (정수원)

강의 링크

쪼금 단적으로 얘기하면 스프링 시큐리티의 동작 원리를 배우는 데에 이 강의보다 좋은 강의는 없을 것 같아요!

강의의 볼륨 자체도 20시간으로 어마어마하지만,
이 강의의 큰 차별점은 디버그 포인트를 뽁뽁 찍어서 내부 동작원리들을 디버깅 방식으로 직접 보여주면서 설명해주는 것이라 생각해요!

단순히 말로 설명하거나 ppt화면으로만 설명하는 강의들은 내용을 순식간에 까먹어버리는데,
이 강의의 경우 강사님이 찍어두신 디버깅 포인트를 따라 찍고 따라가다보면 시간은 오래 걸려도 이해 능력과 기억이 쑥쑥 올라가요.

구체적으로 어떤 클래스의 어떤 메소드가 호출되는 지 보는 것도 중요하지만,
스프링에서 작성한 코드는 어떤 디자인 패턴을 사용하는지와 객체지향적인 코드를 위해 어떻게 서술되어 있는지도 직접 보면서 공부할 수 있는 좋은 기회이기도 했어요!

저는 공부 당시에 디버깅을 사용하는 것이 익숙하지 않아서, 디버그 포인트에서 일어나는 일들을 메모장에 뽀짝뽀짝 기록해가면서 공부했는데 이것도 큰 도움이 되었습니다.

필기 내용에서 유추할 수 있듯, 호출되는 메소드 단위로 엄청나게 자세하게 알려주시고,
당장 내용을 이해하기 어렵더라도 강의가 진행되고 챕터가 바뀌면
같은 메소드를 다양한 관점에서 반복해서 분석해 설명해주시기 때문에 강의를 완강할 때 즈음이면 전반적인 동작 원리를 한 5번 정도 반복학습 한 상태가 돼요.

강의 중후반부부터는 요구사항에 맞추어 커스텀한 필터와 메소드를 만들고 등록, 테스트하는 과정을 거치기 때문에 공부한 내용을 응용하는 훈련도 할 수 있었어요! 여러가지로 정말 도움되는 강의였네요 :-)

주의할 점이 있다면
스프링 시큐리티 5.7.0-M2 버전부터는 시큐리티 관련 설정에 사용하는 WebSecurityConfigurerAdapter를 더이상 사용하지 않으면서, 시큐리티 설정 방법이 강의와 꽤 차이가 나게 되었어요.
새롭게 바뀐 설정 방법 문서를 참고하시면 무리없이 강의를 따라가실 수 있는데
정상 작동이 안 되는 설정도 있으니 많이 어려우시면 그냥 deprecated된 클래스를 사용하셔도 괜찮을 것 같아욥.


응용 1 : 스프링부트 시큐리티 & JWT 강의 (최주호)

강의 링크

이론 공부가 되었다면 실전 응용을 해 봐야죠! (ว˙∇˙)ง (ง˙∇˙)ว
실전 강의 첫 번째로 유튜브에도 무료로 공개되어 있는 메타코딩(최주호)님의 강의를 추천합니다.

스프링 시큐리티를 기반으로 하되
소셜 로그인에 사용하는 Oauth2.0를 사용한 인증/인가와 JWT를 사용한 인증/인가 방식을 구현하는 2개의 간단한 프로젝트를 만드는 강의에요.

그런데 프로젝트에서 우리가 작성하는 코드가 어떤 이유로 작성하는 것인지,
어떤 원리를 구현하고자 하는 것인지를 삼성 노트에 손수 필기하며 설명을 해주시기 때문에
시큐리티의 동작 원리를 또 한번 반복학습하고 이해도를 한 층 더 높일 수 있습니다!

특히 JWT의 개념, 사용하게 된 배경과 장단점을 자세히 알려주시기 때문에 보안과 관련한 지식을 가볍게 학습하는 데에도 큰 도움이 됩니다.

해당 강의는 위에서 주의사항으로 말씀드린 변경된 버전의 설정코드를 반영해서 설명이 되어있기 때문에 새 버전의 스프링 시큐리티로도 무리없이 학습하실 수 있어요!


응용 2 : 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 (이동욱)

서적 링크

이 서적은 스프링 시큐리티만 떼서 배우는 책이 아니라 스프링 부트를 이용해 처음부터 배포까지 쭉 해보는 클론코딩 서적입니다.

사실 이 책에서 스프링 시큐리티는 전체 중 한 챕터일 뿐인데도 이 서적을 추천하는 이유는
스프링 시큐리티와 스프링의 테스트 코드들을 알맞게 동기화하는 내용이 포함되어 있기 때문입니다.

여러분의 프로젝트에 처음부터 스프링 시큐리티를 넣지 않았다면
아마 프로젝트에서 API를 설계, 구현하고, 테스트코드를 작성한 후
나중에 스프링 시큐리티를 추가하여 인증/인가 작업을 시작하게 되실텐데

스프링 부트 프로젝트에 스프링 시큐리티 라이브러리가 추가되는 순간
클라이언트로부터의 요청/응답을 테스트(보통 MockMvc를 사용합니다)하는 테스트코드들이 모조리 터져나가는 불꽃축제를 보게 됩니다.

MockMvc로 보내는 요청에는 스프링 시큐리티가 요구하는 인증 정보가 들어가있지 않아 인증 오류가 발생하는 것인데,
이 서적에서 그 테스트 코드가 다시 정상 작동되려면 어떤 조치를 취해야 하는 지 자세히 설명되어 있기 때문에
나중에 토이프로젝트 등을 만들 때 중요하게 쓰일 지식을 익힐 수 있어요!

Oauth 2.0을 이용해 소셜 로그인을 하는 방식으로 프로젝트를 구현하니 스프링 시큐리티를 소셜 로그인에 응용하는 훈련을 할 수 있고,

'보라색 책'으로 유명한 책인 만큼 실습하면서 생기는 오류를 구글 검색하면 해결 방법이 많이 나와있으니 수월하게 공부하실 수 있어요!


응용 3 : React.js, 스프링 부트, AWS로 배우는 웹 개발 101 (김다정)

서적 링크

지금까지 소개한 강의와 서적은 서버에서 html을 완성한 후 화면에 표현하는 방식(서버-사이드 렌더링이라고 부릅니다.)으로 프로젝트를 진행합니다.
그래서 화면을 구성하는 데에 사용하는 thymeleaf, mustache 등의 템플릿 엔진에서 제공하는 다양한 기능들 중 스프링 시큐리티와 연동한 유용한 기능들을 사용할 수 있었는데요.

이게 편리하긴 하지만 좀 더 실무환경에 가까운 프로젝트를 만들고자 할 때에는 아쉬울 수가 있어요.

규모가 있는 회사에서 실제로 사용하는 프로젝트들은 뷰 템플릿을 활용한 서버사이드 렌더링보다는
화면을 구성하는 데에 집중하는 클라이언트(클라이언트-사이드 렌더링이라고 부릅니다!)와 데이터를 다루는 서버를 분리하고 둘 사이에서 JSON 같은 형태로 데이터를 주고받는 경우가 많기 때문에
뷰 템플릿으로만 프로젝트를 만들어 보셨다면 클라이언트-사이드 렌더링 환경의 실무환경에 대비하기엔 2% 부족할 수 있겠다 생각을 해요.

이 책의 경우에는 서버는 스프링 부트로, 클라이언트는 리액트를 사용해 프로젝트를 구성하고,
이렇게 분리된 두 프로젝트 사이에서 JSON을 이용해 소통하면서 스프링 시큐리티를 어떻게 적용할 것인가를 학습하실 수 있어요!

(아니 나는 백엔드 지망생이라 리액트 하기 싫은데! 하더라도 걱정하실 필요 없어요! 요구사항이 매우 단순한 프로젝트이기 때문에 리액트가 정 하기 싫다면 이 부분은 그냥 소스코드를 복붙해도 잘 돌아갑니다 :-) )

클라이언트에서는 인증된 사용자와 인증되지 않은 사용자를 어떻게 구분하여 대처할 것인지,
서버에서는 인증된 사용자와 인증되지 않은 사용자를 어떻게 구분하여 대처하고
인증 요청이 들어왔을 때 인증 성공과 실패 여부에 따라 클라이언트에 어떻게 응답하고,
클라이언트는 그 응답의 내용에 따라 또 어떻게 대처해야 하는지,
인증 정보는 클라이언트의 어디에 저장하고 요청시에 어떻게 넣어 보내야 하는지 등

스프링 시큐리티를 사용해
인증/인가가 이루어지는 과정을 프로젝트의 전체적인 흐름으로 쭉 이어보는 정말 중요한 경험을 할 수 있기 때문에
이 책은 처음부터 끝까지 쭉 따라가면서 실습해보셨음 좋겠어요.

인증/인가 방식으로 JWT를 사용하기 때문에 JWT에 대한 개념공부도 할 수 있고
이후에 서버에 HTTPS를 구축하면서 JWT 토큰의 보안을 강화하는 과정도 실습할 수 있으니
개인적으로 정말 추천하는 서적입니다! :-)

(이 책도 스프링 시큐리티 설정 변경사항이 적용되지 않아 설정에 애를 먹으실 수도 있는데
2022년 10월 1일에 변경 사항을 반영한 개정판이 출간될 예정이라고 하니 편하게 학습하실 수 있을 거에요!
개정판에서는 JWT뿐만 아니라 Oauth2.0을 사용한 인증/인가도 추가된다고 하니 기대가 됩니다.)


이렇게 제가 스프링 시큐리티를 공부할 때 도움이 되었던 자료들을 정리해 소개해 보았습니다.
지금은 스프링 시큐리티 인 액션이라는 책이 나왔길래 주문해서 도착하기를 기다리고 있습니다.

어쩌구저쩌구 인 액션 시리즈가 보통 좀 어렵다고 해서 조금 쫄리긴 하지만
열심히 공부해보고 시간이 되면 해당 글에 후기를 추가해 보겠습니다.

스프링 시큐리티는 제가 공부할 때 제일 힘들어하고 하기 싫어했던 스프링 라이브러리이지만
익숙해지고 나니 제일 재미있어하는 라이브러리가 되었습니다.

이 글이 여러분의 백엔드 여정에 미약하게나마 참고가 된다면 기쁠 것 같아요.

읽어주셔서 고마워요. 안녕! :-)

profile
얼레벌레

3개의 댓글

comment-user-thumbnail
2022년 9월 27일

너무 재밌게 잘 적어주신 것 같아요 잘보고 갑니당~

답글 달기
comment-user-thumbnail
2022년 10월 1일

위 DispatcherServlet 동작 원리를 김영한님 강의에서 저렇게 상세히 설명해주시나요??
아니면 어떤 책에서 그런지 알 수 있을까요?

1개의 답글