스프링 시큐리티 - 기본 인증 프로세스

반응형

스프링 시큐리티는 스프링 기반 애플리케이션에서 인증과 인가를 구현하기 위한 강력한 보안 프레임워크입니다.

애플리케이션의 보안 요구사항을 쉽게 구현할 수 있도록 다양한 설정 옵션과 확장 가능한 구조를 제공합니다.

스프링 시큐리티를 이해하기 위해서는 아래의 핵심 개념들을 이해해야 하는데요.

 

1. Authentication (인증) `당신이 누구인가?`

사용자가 누구인지 확인하는 과정입니다.

즉, 사용자가 주장하는 신원이 실제로 그 사용자와 일치하는지 검증하는 절차입니다.

사용자가 시스템에 접근할 때, 시스템이 사용자의 신원을 확인하여 해당 사용자가 누구인지 파악하기 위한 목적입니다.

예를 들어, 사용자가 아이디와 비밀번호를 입력하면, 이를 검증하여 사용자가 누구인지 확인합니다.

 

2. Authorization (인가) `당신이 어떤 권한을 가지고 있는가?`

인증된 사용자가 특정 리소스에 접근하거나 작업을 수행할 권한이 있는지 확인하는 과정입니다.

사용자가 접근할 수 있는 자원이나 기능을 제한하고, 권한이 없는 사용자가 민감한 데이터나 기능에 접근하지 못하게 하기 위한 목적입니다.

예를 들어, 관리자인 경우에만 관리자 페이지에 접근할 수 있도록 설정하는 것입니다.

 

3. SecurityContext

인증 정보(Principal)와 관련된 보안 컨텍스트를 저장하는 객체입니다.

`SecurityContextHolder`를 통해 애플리케이션 전반에서 인증 정보를 참조할 수 있습니다.

 

4. Filter Chain

스프링 시큐리티는 여러 필터로 구성된 필터 체인을 통해 요청을 처리합니다.

인증, 권한 검사, CORS 설정 등이 필터를 통해 이루어집니다.


스프링 시큐리티를 의존성에 추가해 놓게 되면 스프링 시큐리티가 클라이언트가 보낸 요청을 가로채서 시큐리티 필터를 통해 검증을 시작합니다.

해당 경로의 접근이 누구에게 열려있는지, 로그인이 완료된 사용자인지, 해당되는 role을 갖고 있는지 등을 시큐리티의 체인 구조에 따라 순차적으로 검증을 해나갑니다.

스프링 시큐리티의 구조

https://kasunprageethdissanayake.medium.com/spring-security-the-security-filter-chain-2e399a1cb8e3

 

스프링 시큐리티의 구조를 간략화해서 보면 위에 그림과 같은데요.

 

우선 인증 요청 흐름을 살펴보고 각각의 부분에 대해서 좀 더 자세하게 다루겠습니다.

 

스프링 시큐리티 인증 요청 흐름

1) 클라이언트 요청

클라이언트가 애플리케이션 서버로 요청을 보냅니다.

예를 들어, 사용자가 `/user/profile`에 접근하려고 하면 요청이 먼저 스프링 시큐리티의 필터 체인을 통과해야 합니다.

 

2) Security Filter Chain

모든 요청은 스프링 시큐리티가 등록한 여러 필터를 거치게 되는데요.

필터는 특정 보안 작업을 수행하며, 그중 인증 작업을 처리하는 필터들이 있습니다.

예를 들어, `UsernamePasswordAuthenticationFilter`는 로그인 요청(`/login`)에서 사용자 인증을 처리합니다.

`JwtAuthenticationFilter`는 요청 헤더에 JWT 토큰이 있는지 확인하고 이를 인증합니다.

 

3) `AuthenticationManager` 호출

필터에서 자체적으로 처리 가능한 부분은 자체적으로 처리하지만, 그게 안 되는 인증이 필요한 로직은 사용자의 정보가 담긴 `Authentication` 객체를 `AuthenticationManager`에게 보내서 검증을 요청합니다.

 

4) `AuthenticationProvider` 호출

`AuthenticationManager`는 인터페이스이기 때문에, 실질적인 인증 로직은 `AuthenticationProvier` 구현체에 넘겨져서 수행됩니다.

 

5) `UserDetailsService` 호출

`DaoAuthenticationProvider`처럼 DB에 접근해서 인증 절차를 진행해야 하는 Provider는 `UserDetailService`를 호출해서 사용자 정보를 조회합니다.

`UserDetailService`는 사용자 이름(Username)을 기반으로 `UserDetails`객체를 반환합니다.

 

6) `UserDetails` 반환

`UserDetailsService`는 사용자 정보를 데이터베이스 등에 조회하여 `UserDetails` 객체를 생성해 반환합니다.

반환된` UserDetails`는 `AuthenticationProvider`가 사용자의 자격 증명(예: 비밀번호)을 검증하는 데 사용됩니다.

 

7) 인증 결과

위에서 `AuthenticationManger`와 `AuthenticationProvider`에 의해 인증이 끝나면 해당 결과를 가지고, 성공 시에는 인증된 사용자 정보가 `SecurityContextHolder`에 저장되고, 실패 시에는 예외를 발생시키거나 로그인 페이지로 리다이렉트 시킵니다.

 

8) 인가(Authorization)

인증에 성공하여 `SecurityContext`에 정보가 저장된 사용자는 애플리케이션 내의 특정 리소스에 접근할 때마다 스프링 시큐리티의 인가 검사를 받습니다.

 

9) 요청 처리

인증과 인가가 성공하면 요청은 해당 컨트롤러로 전달되어 정상적으로 처리됩니다.


우선 Security Filter Chain에 대해서 알아보겠습니다.

 

스프링 시큐리티가 적용된 애플리케이션으로 들어오는 모든 HTTP 요청은 시큐리티의 필터 체인을 통과하게 되는데요.

필터는 상당히 많은 종류가 있지만 주요한 필터는 아래와 같습니다.

UsernamePasswordAuthenticationFilter 폼 기반 인증 처리
BasicAuthenticationFilter HTTP Basic 인증 처리
JwtAuthenticationFilter JWT 토큰 인증 처리 (커스텀 구현 가능)
ExceptionTranslationFilter 인증 및 권한 예외 처리
FilterSecurityInterceptor 최종적으로 인가 여부를 확인

 

이러한 시큐리티 필터에서 일부는 자체적으로 요청 처리가 가능하지만, 인증 요청을 처리하기 위해 `AuthenticationManager`를 호출해야 하는 상황도 있습니다.

필터 자체적으로 처리가 가능한 경우는 주로 인증이나 인가와 직접 관련이 없거나, 이미 인증된 사용자의 정보를 확인하는 경우입니다.
아래는 자체적 처리가 가능한 필터들 중 일부 예시입니다.

필터 이름 주요 역할 및 자체 처리 가능한 경우
AnonymousAuthenticationFilter 인증이 없는 경우 익명 사용자를 처리
SecurityContextPersistenceFilter SecurityContext를 관리하며 인증 정보가 이미 있으면 추가 작업 없이 통과
CorsFilter 요청 출처를 검증하고 허용된 요청은 통과
LogoutFilter 로그아웃 요청을 자체 처리

 

즉, `AuthenticationManager`는 요청마다 호출되는 것이 아니라, 필터에서 인증이 필요한 경우에만 호출이 됩니다.

 

주요 필터들이 `AuthenticationManager`를 호출하는 상황들은 아래와 같은데요.

 

1) `UsernamePasswordAuthenticationFilter`

  • 로그인 요청 (`/login`)에서 사용자의 아이디와 비밀번호를 처리할 때 호출됩니다.
  • `AuthenticationManager`는 전달받은 `UsernamePasswordAuthenticationToken`을 검증합니다.

2) `BasicAuthenticationFilter`

  • HTTP Basic 인증 요청에서 헤더 정보를 기반으로 인증을 처리할 때 호출됩니다.

3) `JwtAuthenticationFilter`

  • JWT 토큰이 포함된 요청의 인증을 처리할 때 호출됩니다.
  • 토큰을 파싱 하여 사용자 정보를 검증한 후,  `AuthenticationManager`로 넘길 수 있습니다.

`AuthenticationManager`는 인증을 처리하는 핵심 클래스입니다.(정확히는 인터페이스에 가까움)

사용자의 인증 정보(Authentication)를 받아 유효성을 검증하는데, 이때 기본적으로 하나 이상의 `AuthenticationProvider`를 통해 실질적인 인증 절차를 진행합니다.

그리고 성공하면 인증된 `Authentication` 객체를 반환해 줍니다.

 

`AuthenticationManger`와 `AuthenticationProvider`의 관계

1. `AuthenticationManager`

public interface AuthenticationManager {
   Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
  • 인터페이스로 정의되어 있습니다.
  • 기본 구현체는 `ProviderManager`로, 여러 `AuthenticationProvider`를 관리하고 요청을 적절한 `AuthenticationProvider`로 전달합니다. 쉽게 말해서 이 구현체(`ProviderManager`)의 역할은 적합한 인증 제공자를 찾아주는 것입니다.

2. `AuthenticationProvider`

  • `AuthenticationManager`가 호출하는 실제 인증 로직을 구현하는 인터페이스입니다.
  • 여러 구현체를 등록할 수 있으며, 각각의 `AuthenticationProvider`는 자신이 처리할 수 있는 인증 방식을 판단합니다.

`UserDetailService`와 `UserDetails`

`AuthenticationProvider` 중에 데이터베이스, 메모리, 외부 API 등의 소스에서 사용자 정보를 가져와서 사용해야 하는 Provider들은 `UserDetailService`를 사용해서 사용자 정보를 가져와야 합니다.

 

`UserDetailService`는 `UserDetails` 객체를 생성하고 반환하는 역할을 담당하는 인터페이스입니다.

사용자 이름(username)을 기반으로 `UserDetails` 객체를 조회합니다.

 

'UserDetails`는 사용자 정보를 담는 인터페이스로, 스프링 시큐리티가 인증 및 인가에 필요한 정보를 제공하기 위해 사용됩니다.

일반적으로 `username`, `password`, `authorities`(권한 목록) 등을 포함합니다.

 

`UserDetailsService`를 주로 호출하는 상황들은 아래와 같습니다.

 

1. `DaoAuthenticationProvider`

`DaoAuthenticationProvider`는 `UserDetailsService`를 사용하여 사용자 정보를 데이터베이스(DAO, Data Access Object)에서 조회하고 인증을 처리하는 기본 제공 `AuthenticationProvider`입니다.

 

2. Custom `AuthenticationProvider`

`UserDetailsService`를 호출하여 사용자 정보를 가져오고, 추가 인증 로직을 처리하는 커스텀 `AuthenticationProvider`를 만들 수도 있습니다.

 

3. JWT 기반 인증

JWT 인증에서는 일반적으로 데이터베이스를 조회하지 않고 토큰에 포함된 정보를 검증하지만, 경우에 따라 추가적인 데이터베이스 검증을 위해 `UserDetailsService`를 사용할 수 있습니다.

 

4. 권한 확인 및 인가 (`Authorization`)

인증 후, 사용자의 권한 정보를 추가적으로 확인하거나 로깅하려는 경우 `UserDetailsService`를 호출할 수 있습니다.


`SecurityContextHolder`

`SecurityContextHolder`는 스프링 시큐리티가 애플리케이션 전역에서 보안 정보를 관리하는 핵심 클래스입니다.

인증에 성공하면 `SecurityContextHolder`에 인증된 `Authentication` 객체가 저장됩니다.

 

인증된 정보는 아래와 같이 꺼내서 사용할 수 있습니다.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName(); // 사용자 이름
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 권한 목록

 

또는 아래와 같이 컨트롤러에서 바로 주입받아 사용할 수도 있습니다.

@GetMapping("/profile")
public String getUserProfile(@AuthenticationPrincipal UserDetails userDetails) {
    String username = userDetails.getUsername();
    return "Profile of " + username;
}

 

유저가 서버에 접속할 때 하나의 쓰레드가 배정되고 여기에 하나의 `SecurityContext`가 생성됩니다.
이후 사용자가 서버에서 빠져나가면(응답이 나가면) `SecurityContext`는 초기화됩니다.

세션을 사용하든, JWT 방식과 같이 Stateless로 동작하든 `SecurityContext`는 존재합니다.

세션 방식(Stateful)의 경우 사용자가 서버에 접근하면 서버 세션으로부터 `SecurityContext`에 값을 주입하고, 로그인이 수행되어 `SecurityContext`에 값이 추가될 때 세션에도 값을 넣게 됩니다.
이후 인증 상태는 세션에 유지되면, 서버는 세션에서 인증 정보를 읽어와 `SecurityContext`를 채웁니다.

반면에 JWT 방식과 같은 Stateless의 경우, 서버는 요청마다 JWT를 해석하여 인증 정보를 확인하고 해당 요청 단위마다 `SecurityContext`가 생성됩니다.
서버에서 상태를 관리하지 않으므로 서버 간 동기화가 필요 없어서 확장성이 증가합니다.
(자세한 내용은 링크 참고)
만약 로그인 사용자가 많다면 세션에 많은 로그인된 사용자들 정보가 있을 수 있습니다.
그렇다면 특정한 매개변수 없이도 어떻게 해당하는 특정 사용자의 인증 정보를 갖고 올 수 있을까요?

정답은 쿠키의 JSESSIONID 값 덕분인데요.
서버 세션에 있는 유저의 인증 정보 값은 브라우저가 들고 오는 쿠키의 JSESSIONID 값에 매핑되어 사용자에게 배정되는 쓰레드의 `SecurityContext`에 배정되게 됩니다.

이러한 인증 절차를 성공적으로 마무리하고 `SecurityContext`에 사용자 정보가 담기면, 이후에 사용자가 애플리케이션 내의 특정 리소스(페이지, API 엔드포인트 등)에 접근할 때마다 스프링 시큐리티는 인가를 검사하는데요.

 

인가(Authorization)의 검사 흐름

1. 인증 완료 후 `SecurityContext`

  • 인증이 성공하면, 사용자의 `Authentication` 객체가 SecurityContext에 저장됩니다.

2.  요청 시 인가 검사

  • 사용자가 인증된 상태로 특정 리소스에 접근하면, 스프링 시큐리티는 다음 기준에 따라 인가 검사를 수행합니다.
    1. 해당 요청을 처리하려는 컨트롤러 메서드나 URL이 어떤 권한(ROLE_USER, ROLE_ADMIN 등)을 요구하는지 확인
    2. 요청한 사용자의 `Authentication` 객체에 포함된 권한 정보(`GrantedAuthority`)와 비교

3. 인가 실패 시

  • 사용자가 요청한 리소스에 접근 권한이 없는 경우, 스프링 시큐리티는 `403 Forbidden` 상태 코드를 반환합니다.

4. 인가 성공 시

  • 사용자가 요청한 리소스에 대한 권한이 있으면 요청을 정상적으로 처리합니다.

 

URL별 권한 설정을 하는 방식은 `SecurityConfig`에 URL별 설정을 하는 방법과 컨트롤러에서 메서드별 설정을 하는 방법이 있습니다.

// SecurityConfig에서 URL별 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private static final String[] PERMIT_URL_ARRAY = {
        "/auth", "/error", "/authNonMember"
    };

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
        		.requestMatchers(PERMIT_URL_ARRAY).permitAll() // 위에 설정된 URL은 권한 필요없음
                .antMatchers("/admin/**").hasRole("ADMIN") // /admin/**는 ADMIN 권한 필요
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN") // /user/**는 USER 또는 ADMIN 권한 필요
                .anyRequest().authenticated() // 나머지 요청은 인증된 사용자만 접근 가능
            )
            .formLogin(form -> form
                .loginPage("/login") // 커스텀 로그인 페이지
                .permitAll()
            )
            .logout(logout -> logout
                .permitAll()
            );
        return http.build();
    }
}
// 컨트롤러에서 메서드 기반 인가

@RestController
@RequestMapping("/api")
public class ExampleController {

    @PreAuthorize("hasRole('ADMIN')")  // ADMIN 권한만 접근 가능
    @GetMapping("/admin")
    public String adminAccess() {
        return "Admin access granted!";
    }

    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")  // USER 또는 ADMIN 권한 접근 가능
    @GetMapping("/user")
    public String userAccess() {
        return "User access granted!";
    }
}

 

이 외에도 커스텀 인가 로직이 필요할 경우에는 `AccessDecisionManager` 또는 `PermissionEvaluator`를 구현하여 사용할 수 있습니다.

 

권한 관련해서 DB에 저장을 하다 보면 `ROLE_권한명` 형태로 들어가는 것을 볼 수 있는데요.

`ROLE_`은 스프링 시큐리티에서 필수적으로 사용되는 접두사이기 때문입니다.

 

`SecurityConfig` 설정 시에는 권한명만 명시하면 자동으로 생성되며, DB 기반 인증 시 `ROLE_`이라는 접두사를 필수적으로 붙여야 하기 때문에 DB 저장 시 접두사를 붙여 저장합니다.

DB에서 붙이지 않고 UserDetails에서 처리를 진행해도 되지만 편의상 DB에 접두사를 함께 저장합니다.

 

만약에 `ROLE_` 접두사를 변경하고 싶으면 아래의 메서드를 `@Bean`으로 등록해서 변경하면 됩니다.

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("MYPREFIX_");
}

 

혹은 DB에 이미 `ROLE_` 접두사가 없이 권한이 저장되어 있다면 아래와 같이 권한을 불러올 때 접두사를 붙여주는 식으로 처리할 수 있습니다.

public Collection<? extends GrantedAuthority> getAuthorities() {

    Collection<GrantedAuthority> collection = new ArrayList<>();

    collection.add(new GrantedAuthority () {

        @Override
        public String getAuthority() {
            return "ROLE_" +  userEntity.getRole();
        }
    });

 


이렇게 스프링 시큐리티의 기본적인 인증 절차에 대해서 정리해 보았습니다.

실제적인 설정과 구현 코드는 다음 글에서 계속 정리해 보겠습니다.

반응형

'방구석 컴퓨터 > 방구석 스프링' 카테고리의 다른 글

Getter도 Setter도 쓰지 말라고??  (0) 2024.11.26
Lombok 생성자와 빌더  (0) 2024.11.25
JPA, JDBC  (4) 2024.11.20
@RestController  (0) 2024.11.19
구글 플레이 인앱 결제와 영수증 처리 프로세스  (2) 2024.10.30
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유