Spring Security를 이용한 회원 인증 (feat jwt, Rest api) (1/2)

YeonCloud·2024년 8월 13일
0

Spring

목록 보기
3/3

팀 프로젝트를 하면서 시간이 조금 남아 spring security와 jwt를 적용해 보려고 하였다. 하지만 로그인 인증, 보안등을 중간에 추가하기에는 힘들고 이러한 것은 시스템 설계 과정에서 생각하고 적용해야한다는 것을 알았다. 다음에는 이러한 실수를 하지 않기 위해 내용을 정리해 보려고 합니다.

먼저, Spring Security를 왜 사용하는가? 모든 어플리케이션은 보안도 중요한 요소이다. 보안에는 인증 및 인가 같은 복잡한 요구사항이 있지만 Spring Security는 이러한 복잡한 요구사항을 쉽게 구현할 수 있도록 해주는 프레임워크이기 때문에 많이 사용한다.
그러면 Spring Security를 사용하면 어플리케이션이 어떻게 동작 과정을 살펴보겠다.


Spring MVC의 기본 구조에서는 DispatcherServlet이 모든 요청(request)을 가로채서 해당 컨트롤러(controller)로 전달하는 흐름을 따릅니다. 하지만 Spring Security를 적용하면, DispatcherServlet이 요청을 받기 전에 Spring Security가 먼저 이를 가로채고, 필터 체인(filter chain)을 통해 보안을 강화하는 역할을 수행합니다.

요청(request) -> Spring Security -> DispatcherServlet -> Controller

그렇다면 이 필터 체인(filter chain) 에서는 어떤 일이 일어날까? Spring Security는 받은 요청에 대해 다음과 같은 작업을 수행합니다.

  1. Authentication (인증): 요청을 보낸 사용자가 타당한 사용자인지를 확인합니다.
  2. Authorization (인가): 사용자가 해당 요청에 대해 올바른 접근 권한을 가지고 있는지를 검증합니다.
  3. 기타 보안 필터: CORS 필터, CSRF 필터 등 추가적인 보안 요소들을 제공합니다.
    또한, Spring Security는 기본 로그인 페이지를 제공하며, HTTP 응답을 통해 발생하는 예외 처리도 가능합니다.

필터 체인 내에서 각 필터의 실행 순서는 매우 중요합니다. 일반적으로 필터는 다음 순서로 실행됩니다

  1. 기본 보안 체크 필터: CORS 필터와 CSRF 필터와 같은 기본 보안 필터들이 먼저 실행됩니다.
  2. 인증 필터 (Authentication Filter): 사용자의 자격 증명을 확인합니다.
  3. 인가 필터 (Authorization Filter): 사용자의 접근 권한을 확인합니다.

이와 같은 순서로 필터가 실행되며, 이를 통해 요청이 적절하게 보호되고, 애플리케이션의 보안이 강화됩니다.


spring security 적용

Gradle 파일에 다음과 같은 의존성만 추가하면, 해당 서버로 들어오는 모든 리소스가 자동으로 보호됩니다.

implementation 'org.springframework.boot:spring-boot-starter-security'

예를 들어, 존재하지 않는 리소스에 접근하려 해도 로그인을 요구합니다. http://www.yeonCloud.com/adfasdfasdf처럼 도메인 뒤에 무작위 문자열을 추가해도, 서버는 로그인 요청 페이지로 리디렉션합니다. Spring Security의 기본 설정 덕분에 서버로 들어오는 모든 요청은 인증을 필요로 하게 됩니다.

Spring Security에서는 기본적으로 제공되는 로그인 페이지에서 사용자 이름(username)과 비밀번호(password)를 설정할 수 있으며, 이 로그인 기능을 비활성화하고 싶다면 요청 헤더에 Base64로 인코딩된 사용자 이름과 비밀번호를 보낼 수도 있습니다.

위 예시에서 Spring Security의 사용자 이름과 비밀번호를 설정한 후, 이를 요청에 포함시켜 보내 보겠습니다.

포스트맨(Postman)을 이용한 확인 ⬇️

요청 결과 Status: 200 OK가 표시되는 것을 확인할 수 있습니다. 이는 인증이 성공적으로 이루어졌음을 의미합니다.

이 기본 인증 방식 대신, JWT(JSON Web Token)를 사용할 수도 있습니다. JWT를 사용하면 더욱 안전하고 유연한 인증 시스템을 구축할 수 있습니다.


Spring Security 커스텀하기

먼저 기본 Spring Security filter chain을 살펴보자(Ctrl + N -> SpringBootWebSecurityConfiguration 검색 후 확인)

@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {

   @Bean
   @Order(SecurityProperties.BASIC_AUTH_ORDER)
   SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
      http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
      http.formLogin(withDefaults());
      http.httpBasic(withDefaults());
      return http.build();
   }

}

위 코드는 기본 설정된 Spring Security filter chain이다.
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); 코드는 모든 요청이 인증되어야 한다는 코드이다.

그리고 나는 session을 통한 인증이 아니기 때문에 session 정책을 stateless로 변경한다.

http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

무상태(stateless) 인증 방식을 적용하게 될 때 이는 보안 측면에서 다음과 같은 이점이 있다.

  1. 세션 상태 관리의 부재:
    • 이 설정을 통해 애플리케이션은 클라이언트와 서버 간의 상태를 유지하지 않습니다. 즉, 서버가 클라이언트의 세션 정보를 저장하지 않기 때문에 서버의 상태를 유지할 필요가 없습니다.
    • 요청마다 독립적으로 인증이 이루어지며, 요청 간에 세션이 공유되지 않기 때문에 애플리케이션의 확장성(Scalability)이 향상됩니다.
  2. 보안성 향상:
    • 세션 하이재킹(session hijacking)과 같은 공격에 대한 위험이 줄어듭니다. 세션 쿠키를 사용하지 않기 때문에 클라이언트와 서버 간의 인증이 더 안전하게 이루어질 수 있습니다.
    • JWT(JSON Web Token) 같은 토큰 기반 인증 방식과 잘 어울립니다. JWT는 클라이언트 측에서 유지되며, 서버는 요청이 들어올 때마다 JWT를 검증하여 인증을 수행합니다.
  3. RESTful 서비스와의 호환성:
    • RESTful API 디자인에서 권장되는 무상태(Stateless) 원칙을 따르기 때문에, REST API 서버에 적합한 설정입니다. 이는 서버가 각 요청을 독립적으로 처리하고, 클라이언트의 상태를 기억하지 않아야 한다는 REST의 원칙과 일치합니다.

그리고 CSRF 보안 설정을 제거한다.

http.csrf(csrf -> csrf.disable());

장점

  1. RESTful API 및 무상태 애플리케이션에 적합
    • RESTful API는 보통 무상태(stateless)로 설계되며, 클라이언트는 매 요청마다 인증 정보를 제공해야 합니다. 이런 환경에서는 서버가 세션이나 상태를 유지하지 않기 때문에 CSRF 공격의 위험이 상대적으로 낮습니다.
    • 이러한 상황에서 CSRF 보호는 불필요할 수 있으며, 비활성화하면 성능 최적화와 설정 간소화에 도움이 됩니다.
  2. 폼 기반 인증을 사용하지 않는 애플리케이션에 유리
    • CSRF 공격은 주로 사용자가 인증된 세션을 유지하고 있을 때 발생합니다. 특히, HTML 폼을 통한 POST 요청이 공격에 취약합니다.
    • 그러나 API 기반 애플리케이션에서 주로 사용하는 인증 방식(예: JWT 토큰)은 클라이언트가 매 요청마다 인증 정보를 포함하기 때문에 CSRF 공격이 발생할 가능성이 적습니다. 이런 경우 CSRF 보호를 비활성화해도 보안 위험이 크지 않을 수 있습니다.
  3. 개발 편의성
    • 개발 초기 단계나 내부 애플리케이션 개발 중일 때는 CSRF 보호를 비활성화하면 인증 및 요청 처리를 테스트하는 데 있어서 편리합니다.
    • 특히 프론트엔드와 백엔드가 분리된 애플리케이션에서 프론트엔드 개발자가 요청을 쉽게 테스트할 수 있도록 CSRF 보호를 임시로 비활성화할 수 있습니다.

CORS (Cross-Origin Resource Sharing)

브라우저는 보안상의 이유로, 기본적으로 다른 도메인에서 제공되는 외부 리소스에 대한 API 호출을 허용하지 않습니다. 이러한 제한을 해결하기 위해 CORS(Cross-Origin Resource Sharing)라는 사양이 존재합니다. CORS를 통해 어떤 도메인 간 요청이 허용되는지 구성할 수 있습니다.

Spring MVC에서는 CORS 설정을 통해 특정 도메인 간 요청을 허용할 수 있으며, 이를 글로벌 또는 로컬 수준에서 구성할 수 있습니다.

  1. Global Configuration
    글로벌 설정은 애플리케이션 전체에 적용됩니다. 아래 예제는 모든 경로에 대해 모든 HTTP 메서드를 허용하고, 특정 도메인(http://localhost:3000)에서 오는 요청을 허용하는 설정입니다.
public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**")
						.allowedMethods("*")
						.allowedOrigins("http://localhost:3000");
}
  1. Local Configuration
    로컬 설정은 특정 컨트롤러 또는 메서드에만 적용됩니다. 예를 들어, 아래와 같이 @CrossOrigin 애너테이션을 사용하여 특정 도메인(https://www.naver.com)에서 오는 요청을 허용하거나, 별도의 설정 없이 기본 CORS 설정을 사용할 수 있습니다.
@CrossOrigin(origins = "https://www.naver.com") 
// or
@CrossOrigin 

사용자 신원 정보 저장

Spring Security에서 사용자의 신원 정보를 저장하고 관리하기 위해 UserDetailsService를 구현할 수 있습니다. 아래 예제에서는 InMemoryUserDetailsManager를 사용하여 사용자 정보를 메모리에 저장하는 방법을 보여줍니다.

@Bean
public UserDetailsService userDetailsService(){

    var user = User.withUsername("yeon")
            .password("1234")
            .roles("USER")
            .build();

    var admin = User.withUsername("admin")
            .password("1234")
            .roles("ADMIN")
            .build();

    return new InMemoryUserDetailsManager(user, admin);
}

위 코드에서는 두 명의 사용자를 생성합니다:

yeon: 일반 사용자 역할(USER)을 가진 사용자
admin: 관리자 역할(ADMIN)을 가진 사용자
이 두 사용자는 InMemoryUserDetailsManager를 통해 메모리에 저장되며, 애플리케이션 실행 중 인증에 사용됩니다. 비밀번호는 {noop}을 사용하여 인코딩 없이 평문으로 저장되지만, 실제 애플리케이션에서는 반드시 안전한 방식으로 비밀번호를 인코딩해야 합니다.

이 방식으로 사용자를 메모리에 저장하면, 별도로 application.properties나 application.yml 파일에 사용자 정보를 저장할 필요가 없습니다. 다만, 이 방법은 주로 테스트나 프로토타입 개발 시에 사용되며, 실제 운영 환경에서는 데이터베이스나 다른 외부 저장소를 사용하여 사용자 정보를 관리하는 것이 일반적입니다.

0개의 댓글