이 글을 쓰게 된 배경
스프링부트 프로젝트에서 로그인/회원가입 기능을 만들면서 스프링 시큐리티를 학습하는 과정을 담은 글입니다. 스프링 시큐리티의 내용이 너무 많아서 오랜 기간동안 학습하면서 여러번에 걸쳐서 작성했습니다.
다룰 내용
- Spring Security란?
- Spring Security를 사용해야 하는 이유
- Spring Security 구조
- Spring Security 이외에 인증/인가를 대체할 방법
Spring Security란?
Spring security란 스프링의 Filter를 이용하여 스프링부트에서 제공하는 인증/인가를 지원하는 프레임워크 입니다.
Spring Security를 사용해야 하는 이유
Spring Security는 애플리케이션의 보안을 체계적으로 관리하고, 최신 보안 표준을 준수하며, 다양한 인증 및 권한 부여 기능을 유연하게 확장할 수 있는 강력한 도구입니다. 보안은 애플리케이션의 필수적인 부분이며, Spring Security를 사용하면 이를 쉽게 구현하면서도 강력한 보안을 제공할 수 있습니다.
라는 것이 형식적인 이유이지만 사실 대부분의 스프링부트를 공부하는 사람이 그렇듯 저도 그냥 프로젝트에 인증/인가가 필요한데 어떻게 구현하지? 하다가 스프링 시큐리티를 찾은 것이었고 다른 대체 방안도 몰랐기 때문에 사용했습니다. 블로그 마지막에는 대체할 수 있는 다른 방안도 찾아서 적어보겠습니다.
Spring Security 구조
1. 사용자에게 http 요청이 들어옵니다. ('GET', 'POST' 등)
2. Authentication Filter가 중간에 요청을 가로채서 사용자의 인증 정보를 추출합니다. ex) username, passowrd
인증 정보를 바탕으로 Authentication 객체 (UsernamePasswordAuthenticatonToken)을 생성합니다.
실제로는 수많은 Filter가 체인으로 연결되어 있습니다. 검증하고 싶은 내용을 Filter로 만들어서 필터체인에 넣는 것도 가능합니다.
필터 순서에 따라 인증 과정에서 예상한 응답과 다른 응답이 될 수도 있기 때문에 중요한 필터의 순서가 어느정도에 있는지 확인하고 의도한 순서에 넣어야 합니다.
아래는 필터체인에 커스텀한 필터를 넣는 방법입니다. 제가 만든 tokenAuthenticationFilter를 기존에 있는 필터인UsernamePasswordAuthenticationFilter의 이전 순서로 추가하겠다는 의미입니다.
.addFilterBefore(tokenAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
3. 필터에서 생성한 Authentication 객체를 AuthenticationManager로 전달합니다.
4. AuthenticationMager에서 요청을 처리할 AuthenticationProvider를 선택합니다. 여러 Provider를 사용할 수 있으며 각각 특정 상황의 요청을 처리합니다. AuthenticationProvider는 말 그대로 인증관련 처리를 해야할 때 만들면 됩니다. 저는 OAuth2.0을 사용해서 로그인을 하고있기 때문에 인증 관련 처리는 OAuth2.0에서 하고 있으니 따로 관련 인증 기능은 구현하지 않아도 되었습니다. 또한 Provider에서 할 인증 기능을 Filter에서 구현할 수도 있습니다. Provider는 Filter에서 생성한 authentication 객체를 가지고 인증을 합니다. Filter에서 authentication 객체를 만들기 전에 먼저 인증을 하는 것도 하나의 방법일 수 있습니다.
5. AuthenticationProvider는 사용자 정보를 검증하기 위해 UserDetailService를 호출합니다. UserDetailService는 사용자 이름을 기반으로 사용자 정보를 로드합니다. 저는 OAuth2.0을 사용했기 때문에 UserDetailService가 아닌 DefaultOAuth2UserService를 사용했습니다. UserDetailService는 데이터베이스의 사용자 정보를 바탕으로 사용자를 가져오고 DefaultOAuth2UserService는 OAuth2 서버로 부터 사용자를 가져오는 것입니다. 일반적으로는 두가지 중 한가지만 사용하겠지만 OAuth2 서버의 사용자 정보를 바탕으로 내 데이터베이스의 사용자 정보와 검증을 거쳐야 하는 경우에는 두가지 다 사용할 수도 있습니다.
UserDetailService | DefaultOAuth2UserService | |
사용 목적 | 기본적인 인증(Username/Password 기반) | OAuth2.0 인증 |
입력 값 | username | OAuth2UserRequest |
변환 객체 | UserDetails | OAuth2User |
UserDetails가 username을 기반으로 사용자를 검증하는 경우 사용자의 이름이 같을 수도 있는데 무조건 username으로 검증하는 것은 이상하다는 생각에 다른 식별자(이메일 or 닉네임 등)으로 할 수 있는 방법은 없는지 찾아보았습니다. 알고보니 username이라고 해서 사용자 이름이 아니라 스프링에서 제공하는 고유식별자 같은 것이었습니다.
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.map(user -> new CustomUserDetails(user)) // UserDetails 구현체 반환
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
}
이런식으로 loadUserByUsername이라고 했지만 이메일을 받아서 이메일을 통해서 검증할 수도 있는 것이었습니다. (username이라고 해서 당연히 사용자이름이라고 생각한 게 실수 였습니다..)
6. UserDetailService에 인증정보와 권한정보를 담고 있는 UserDetails를 반환합니다.
7. 반환된 UserDetails객체에서 사용자 정보를 추출해서 요청을 보낸 사용자의 정보와 비교합니다.
8. 성공하면 AuthenticationProvider는 검증된 Authentication 객체를 반환합니다
9. 8번과 동일
10. 인증된 Authentication 객체는 SecurityContextHolder에 저장됩니다.
SecuriyContext는 어플리케이션 전역에서 접근할 수 있고 이후에 여기에 저장되어 있는 인증정보를 활용해서 권한 검사를 수행합니다.
Spring Security 이외에 인증/인가를 구현할 방법
저는 항상 비슷한 다른 기술들을 찾아보는 것이 중요하다고 생각하는데, 각 기술마다 장단점이 있고 내가 처한 상황에 적합한 기술이 있기 때문입니다. 기술을 다 익혀놓지 않더라도 어떤 장단점이 있는지 대충 파악만 하고 있더라도 나중에 아 이런 기술이 있었는데 적용해볼까라는 생각이 들고 그게 아니더라도 내가 이 기술을 왜 사용하는지에 대한 확신이 생기기 때문입니다.
- Keycloak
- API Gateway를 활용한 인증/인가
- Firebase
- Interceptor
Keycloak
레드햇에서 제공하는 인증/인가 솔루션으로 다운로드 받아서 프로젝트에 적용시키면 관련 서비스를 제공해줍니다. admin계정으로 여러가지 세부적인 부분을 변경할 수 있습니다. jdk, 도커 이미지 등 여러가지 다운로드 방법을 제공합니다. 인증/인가까지 구현할 시간이 부족할 때 사용하면 좋을 것 같습니다.
Keycloak
Single-Sign On Users authenticate with Keycloak rather than individual applications. This means that your applications don't have to deal with login forms, authenticating users, and storing users. Once logged-in to Keycloak, users don't have to login again
www.keycloak.org
API Gateway를 활용한 인증/인가
스프링 시큐리티가 어플리케이션 레벨에서 인증/인가를 한다면 API Gateway는 네트워크 레벨에서 인증/인가를 하는 방법입니다.
API Gateway는 들어오는 요청에 토큰을 검증(Jwt, OAuth2 Access Token)하고 라우팅해주는 역할을 합니다. Gateway에만 관련 로직이 있기 때문에 유지보수하기에 용이하지만 인증/인가 로직의 실패 지점이 모두 Gateway이기 때문에 Gateway가 단일 실패지점이 될 수 있는 단점이 있습니다. 마이크로서비스 아키텍처에서 사용하면 정말 좋을 것 같습니다.
API Gateway는 csrf, 권한 등의 기능을 제공하지 않아서 관련 기능이 필요하다면 API Gateway와 Spring Security를 같이 사용하는 것도 좋은 방법이 될 수 있습니다. 대표적인 오픈소스로는 Kong이 있습니다.
GitHub - Kong/kong: 🦍 The Cloud-Native API Gateway and AI Gateway.
🦍 The Cloud-Native API Gateway and AI Gateway. Contribute to Kong/kong development by creating an account on GitHub.
github.com
Firebase
파이어베이스는 구글에서 지원하는 웹 앱 어플리케이션 개발 플랫폼으로 여러가지 개발 관련 솔루션을 제공하는데 역시 인증 관련 기능도 제공합니다. 이메일/비밀번호, 소셜 로그인, 전화번호 인증 등 다양한 방식을 지원합니다.
대부분의 기능이 만들어져 있기 때문에 빠르게 만들어야 하는 소규모 프로젝트에서 사용하면 좋을 것 같습니다. 하지만 사용할때마다 비용이 들기 때문에 트래픽이 많고 비용관리가 중요한 경우에는 사용하기 좋지 않을 것 같습니다. 권한 관리도 어렵지만 스프링 시큐리티와 같이 사용하면 해결할 수 있습니다.
아래와 같은 방법으로 적용할 수 있습니다. (gradle)
dependencies {
implementation 'com.google.firebase:firebase-admin:9.1.1'
}
https://firebase.google.com/docs/auth/where-to-start?hl=ko
Firebase 인증은 어디에서 시작하나요? | Firebase Authentication
사용 사례, 경험, 앱 아키텍처에 따라 앱에 적합한 인증 옵션을 선택합니다.
firebase.google.com
Interceptor
스프링 시큐리티는 필터를 기반으로 인증/인가를 구현합니다. 인터셉터는 필터와 비슷한 기능을 하지만 필터는 디스패처 서블릿 밖에서(이전에) 실행되고 인터셉터는 디스패처 서블릿 안에서(이후에) 실행됩니다.
즉, 필터는 모든 요청에 대해서 인증/인가 처리를 하지만 인터셉터는 Spring MVC 안에서의 요청에만 인증/인가를 처리합니다.
정적 리소스를 사용하여 페이지를 구현하고 있다면 (Tymeleaf, jsp 등 사용) 필터를 사용해야 합니다. 하지만 REST API를 사용한다면 인터셉터를 사용해도 무방합니다. 저는 REST API 기반의 프로젝트 였기 때문에 인터셉터를 사용하는 것이 성능 면에서 더 좋았지만 CORS부분들은 필터에서 검증해야 하기 때문에 스프링 시큐리티를 선택했습니다. 둘다 사용하는 것이 베스트이지만 시간 상 두 가지 기술 다 공부하는 것은 무리였고 추후에 인터셉터를 사용하는 방법도 적용해 보려고 합니다.
마무리
인증/인가에 대한 기본적인 개념 없이 무작정 스프링 시큐리티를 공부하는데 시간과 돈(강의)를 정말 많이 썼는데 생각보다 간단하게 프로젝트에 적용할 수 있는 방법이 많았습니다. 처음부터 스프링 시큐리티를 하지 말고 파이어베이스같은 제품을 사용하면서 인증 흐름을 파악했다면 더 쉽게 공부 할 수 있었을 거라는 생각이 드네요...
'Spring' 카테고리의 다른 글
서블릿 컨테이너의 이해 (0) | 2025.04.04 |
---|---|
엔티티 DTO 변환 위치 (1) | 2025.01.10 |
스프링 부트에서 예외와 처리 방법 (2) | 2024.12.30 |
Spring Security 필터에서 발생한 인증/인가 예외 처리하는 방법 (2) | 2024.12.30 |
커넥션 풀 vs 복잡한 로직 시간 비용 (2) | 2024.12.22 |