본문 바로가기
[Spring]/Spring Security

당신의 첫 프로젝트를 위한 스프링 시큐리티 톺아보기

by 팡펑퐁 2024. 7. 6.
728x90
 🙇🏻‍♂️ 안녕하세요. 저는 부트캠프를 나와서 개발자로 일하고 있는 사람입니다. 지금 생각해 보면 부트캠프 메인 프로젝트 기간 때 가장 골치 아프고 힘들었던 게 스프링 시큐리티였던 것 같아요. 당시에 막상 만들어 놓고 이해도 잘 못하고 팀원들에게 설명하기 어려워했던 기억이 납니다. 그래서 언젠가 프로젝트를 처음 하시는 분들을 위해 잘 몰라도 따라 할 수 있는 스프링 시큐리티 글을 써야겠다 생각을 했는데 그걸 이제야 하게 됐네요. 아무튼 잘 몰라도, 지나가는 할머니께 다짜고짜 설명해도 이해할 수 있게 쉽게 작성해 보겠습니다.

 

 여기서는 웹, 앱 애플리케이션에서의 로그인, 로그아웃 구현을 위해 필요한 스프링 시큐리티(Spring Security)의 동작 원리에 대한 짤막한 개념과 설명을 다룹니다. 다음 글부터는 하나하나 만들어보며 최종적으로는 redis를 이용한 access, refresh token 관리까지 설명해 보겠습니다.

 

 제 글만 보고 따라 해도 스프링 시큐리티 구현에 아무런 문제가 없도록 친절하게 설명해 드리겠습니다.

원리나 개념에 대해서는 저도 잘 몰라서 설명이 부실하거나, 틀릴 수 있다는 점 양해부탁드립니다.

 

🛠️ 스프링 시큐리티의 동작 아키텍처

<구글 이미지에서 검색하면 흔히 나오는 스프링 시큐리티 인증 아키텍처>

1. Http Request -> AuthenticationFilter

 일반적으로 웹이든 앱이든 데이터를 주고받을 때 Http(s)로 통신하잖아요. 그러니 여러분이 웹 페이지에서 로그인 화면을 구현을 하면 id와 Password를 유저로부터 입력을 받을 수 있을텐데 그걸 나타내고 있는 것입니다. 1번은 클라이언트에서 입력한 id와 password가 자바의 서블릿 필터로 들어가는 부분을 표현하고 있습니다.

 

💡 서블릿에 대한 간단한 요약 설명
서블릿이란 자바에서 웹 통신을 하기 위해 만들어진 기술을 말합니다.
서블릿 컨테이너는 이러한 서블릿을 관리하는 기능을 합니다.

 

클라이언트에서 서버로 요청을 보낼 때의 통과 순서

클라이언트 요청: 클라이언트(브라우저 등)가 서버로 HTTP 요청(ex. http body에 id, password를 싣음)을 보냅니다.

서블릿 컨테이너: 요청이 서버에 도착하면, 서블릿 컨테이너(Tomcat, Jetty 등)가 요청을 받습니다.

서블릿 필터: 요청이 서블릿에 도달하기 전에, 서블릿 필터가 요청을 가로챕니다. 서블릿 필터는 요청 및 응답을 수정하거나 추가 작업을 수행할 수 있습니다. 예를 들어, 인증, 로깅, 압축 등이 가능합니다.

디스패처 서블릿: 서블릿 필터를 통과한 요청은 디스패처 서블릿(DispatcherServlet)으로 전달됩니다. 디스패처 서블릿은 스프링 MVC의 프론트 컨트롤러로, 모든 요청을 중앙에서 처리합니다.

핸들러 매핑: 디스패처 서블릿은 요청 URL을 기반으로 어떤 컨트롤러가 요청을 처리할지 결정하기 위해 핸들러 매핑(Handler Mapping)을 사용합니다.

인터셉터: 핸들러 매핑이 완료되면, 요청은 인터셉터(Interceptor) 체인을 통과합니다. 인터셉터는 요청 처리 전후에 추가 작업을 수행할 수 있습니다. 예를 들어, 인증, 권한 부여 등이 가능합니다.

컨트롤러: 인터셉터를 통과한 요청은 실제로 컨트롤러(Controller)로 전달됩니다. 컨트롤러는 비즈니스 로직을 처리하고, 적절한 모델과 뷰를 반환합니다.

 

 위의 요청 통과 순서를 이해하시면 무리 없이 클라이언트로부터 받은 id, password가 AuthenticationFilter로 들어온 내용을 상상하실 수 있을 겁니다.(만약 이해가 안가신다면 Spring MVC의 동작 구조에 대해 구글링해보세요!) AuthenticationFilter가 바로 서블릿 필터이기 때문입니다!! 여기서 요청을 가로채어 해당 요청에 대한 로그인 인증 처리를 하는 것이죠! 이해 못 하셔도 됩니다 ㅎㅎ '아 인증은 실제 비즈니스 로직을 처리하는 부분 전에 필터를 거쳐 처리되는구나'라고 생각하시면 됩니다. 

 

  • 1번 내용을 정리하겠습니다. 사용자가 저희가 만든 웹, 앱의 로그인 폼에서 자신의 아이디와 비밀번호를 입력하면 그 데이터가 json 형식으로 서버로 넘어올 겁니다.
{
    "id" : "myid111",
    "passeord" : 123456
}
  • 이런식으로요. 이 때 서블릿 필터인 AuthenticationFilter가 해당 요청을 가로 챈 것입니다.

 

2. AuthenticationFilter -> UsernamePasswordAuthenticationToken

  • 그 다음은 2번, 가로 챈 요청으로 로그인 처리를 해야겠죠?
  • 요청 body에 담겨 있는 사용자 이름과 비밀번호를 UsernamePasswordAuthenticationToken라는 객체에 담아 인증 요청을 하기 위한 준비를 하는 단계입니다.
    • json 형태를 자바 객체로 변경하는 역직렬화(Deserialization) 작업을 해야합니다.
  • 이 객체에 담아야 스프링 시큐리티의 AuthenticationManager를 이용할 수 있습니다.
    • AuthenticationManager에서는 UsernamePasswordAuthenticationToken 안에 있는 사용자의 로그인 정보를 받아 처리하도록 설계되어 있습니다.
    • 이걸 왜 하냐구요? 스프링 시큐리티가 만들어 놓은 인증 체계를 저희가 따르기 위해서입니다. 이렇게 해야 스프링 시큐리티가 귀찮은 작업을 알아서 다 해주니까요.

 

3. UsernamePasswordAuthenticationToken -> AuthenticationManager(ProviderManager)

💡 인터페이스
인터페이스란 구현해야 하는 메서드의 목록을 정의하는 일종의 목차입니다.
인터페이스 자체는 실제 구현 내용(로직 코드)을 포함하지 않고, 메서드 시그니처(메서드 이름, 반환 타입, 매개변수 목록)만을 정의합니다. 저희가 책을 보면 목차가 있잖아요? 그 목차만 있는 겁니다.
'안에 내용은 네가 규칙(메서드 시그니처)에 맞춰 알아서 구현하세요.' 이 말입니다.

 

  • 이 부분은 UsernamePasswordAuthenticationToken의 로그인 정보로 로그인 인증을 처리하기 위해서는 AuthenticationManager의 구현체인 ProviderManager를 사용해야 함을 의미합니다.
    • 이걸 직접 만들거나, 이미 만들어져 있는 구현체를 사용하셔도 됩니다. 잘 모르셔도 일단 넘어가시고 이 부분을 구현할 때 자세히 설명해 드리겠습니다.
  • 지금은 간단히 ProviderManager가 로그인 인증 처리를 하기 위한 매니저로서의 역할을 부여받는 순간이라고 생각하시면 좋을 것 같습니다.

 

4. AuthenticationManager(ProviderManager) -> AuthenticationProvider

  •  위에서 언급했듯이 AuthenticationManager의 구현체인 ProviderManager가 매니저로서의 역할을 부여받습니다.
  •  그리고 UsernamePasswordAuthenticationToken에 들어있는 id와 password를, 인증을 처리해 주는 AuthenticationProvider(인증 처리 제공자)에게 'id와 password를 드릴 테니 이 정보로 저희 회원이 맞는지 확인해 주세요.'라고 오더를 내리는 부분입니다.
    • AuthenticationProvider는 매니저의 오더를 받고 사용자의 id와 password를 검증합니다.
  • 여기서부터는 이해를 돕기 위해 비유를 들겠습니다. 
  • 4번은 매니저(AuthenticationManager)가 직접 인증 처리를 하지 않고, 중간 직원(AuthenticationProvider)에게 인증 처리를 하라고 오더만 내리는 역할임을 이해하는 것이 포인트입니다.

 

5. AuthenticationProvider -> UserDetailsService

  • 매니저에게 오더를 받은 중간 직원(AuthenticationProvider)은 사용자의 id와 password를 검증할 때 자신이 직접하지 않습니다.
  • 이를 또 말단직원(UserDetailsService)에게 시킵니다.(하청의 하청의 하청..)
    • UserDetailsService는 인터페이스로서 loadUserByUsername라는 메서드가 정의되어 있습니다.
    • 감이 오시나요? loadUser(유저의 정보를 가지고 온다.), ByUsername(유저의 이름으로부터) 데이터베이스에 username을 넘기고 유저의 정보를 가지고 오는 메서드처럼 보입니다.
    • 이때 username은 무엇일까요? 사용자로부터 입력받은 id입니다. 사용자의 id를 주고 사용자 정보를 가지고 올 때 password도 포함되어 있을 테니 데이터베이스의 password와 사용자로부터 입력받은 password를 비교하여 인증 통과 or 실패 여부를 결정하는 것입니다.

 

6. UserDetailsService -> UserDetails

Member member = memberRepository.findByUsername(username);
  • 말단 직원(UserDetailsService)은 이제 데이터베이스에서 꺼낸 id, password를 UserDetails라는 틀에 맞춰 담습니다.
  • 그리고 클라이언트에게 받은 id, password 정보와, 데이터베이스 내의 id, password가 일치하는지 검증을 하고, 이 결과를 중간 직원(AuthenticationProvider)에게 넘기는 작업을 합니다.
    • 위에서 데이터베이스에 사용자 정보를 가지고 오면 엔티티 인스턴스에 정보가 담겨 있겠죠?
      • 아마 JPA를 쓰시는 분들은 Spring Data Jpa의 findByUsername와 같은 쿼리 메서드를 이용하실 거고, 아니면 sql문으로 직접 데이터를 가져오실 텐데요.
    • 뭐가 됐든 이 정보를 UserDetails라는 새로운 객체에 옮깁니다. 별도의 객체에 사용자의 정보를 담는 이유는 앞으로 있을 다른 인증이나 권한 처리 등 스프링 시큐리티에서의 여러 기능에 사용하기 위해서입니다. 그냥 entity 인스턴스를 쓰면 되지 않냐 하실 수 있는데 객체지향적으로 설계돼서 그렇습니다.(위부터 계속 하청의 하청이 지속되고, 모든 단계를 하나의 역할로 쪼개는 이유는 바로 스프링이 객체지향적인 설계가 기본으로 되어있기 때문입니다. SOLID 원칙..)
      • 참고로 UserDetails도 인터페이스로 구현체가 필요합니다.

 

7 ~ 10. SecurityContextHolder - Authentication

  • 인증 처리가 끝이 나면 역으로 그대로 돌아가 SecurityContextHolderAuthentication에 인증 정보가 실립니다. 이때의 인증 정보는 6번의 UserDetails를 포함합니다.
  • 위의 그림에는 나와 있지 않지만 이때 인증 성공/실패 여부에 따라 성공할 때 수행할 핸들러, 실패할 때 수행할 핸들러로 넘어가 각 상황에 맞는 메서드가 수행됩니다.
    • 성공했을 때는 성공했다는 응답을 보낸다든가, 실패했을 때는 각 에러에 해당하는 예외를 처리하고 응답을 보내는 식으로 각 핸들러를 동작시키기 위함입니다.

 

📜 정리

 1 ~ 10번까지를 짧게 요약하면 '사용자로부터 id와 password를 받아 데이터베이스에서 입력받은 id로 사용자의 정보를 찾고 입력받은 password와 저장된 password를 비교하여 인증 성공/실패 여부에 따라 SecurityContextHolder의 Authentication에 인증 정보나 인증 예외 정보를 싣는다. 이후 각 상황에 맞는 핸들러를 실행시킨다.'라고 볼 수 있겠네요.

 

 

동작 원리에 대한 설명을 최대한 쉽게 설명해 보았습니다. 다음 글부터는 직접 구현해 보면서 이해해 보겠습니다.

 

 

 

참고

뤼튼

https://velog.io/@kyungwoon/Spring-Security-Spring-Security-동작-원리

https://mangkyu.tistory.com/14

728x90