취뽀몽

[Spring] Spring Security란 본문

spring

[Spring] Spring Security란

허몽구 2023. 8. 26. 15:20

대부분의 시스템은 회원 관리가 존재하다 보니, 인증과 인가에 대한 처리를 해줘야 하는 경우가 발생한다.

그렇기에 Spring에서는 스프링 기반의 보안(인증, 인가, 권한)을 담당하는 프레임워크인 Spring Security를 제공한다.

 

여기서 인증과 인가란,

- 인증(Authentication) : 사용자 본인이 맞는지 확인함

- 인가(Authorization) : 인증된 사용자가 요청한 리소스로 접근 가능한지 결정함

 

Spring Security는 기본적으로 인증 절차를 거친 후 인가 절차를 진행하고, 인가 과정에서 해당 리소스에 접근 권한이 존재하는지 확인한다.

 

사실 Spring Security에 대한 많은 글을 찾아보면, 처음 공부하는 사람은 이해가 잘 되지 않을 수 있다. (본인 포함...)

Spring Security에 관련된 Filter가 굉장히 많고 복잡하기 때문이다. 

사실 프레임워크는 사용자가 이용하기 편하도록 제공되어야 한다고 생각하는데 Spring Security 프레임워크가 복잡하다 보니 잘 사용하지 않았었다.

하지만 한 번 이해하기 시작하면 본인이 원하는 대로 권한을 설정하고 다룰 수 있기 때문에 굉장히 편할 것이다!

 

Spring Security의 특징은 다음과 같다.

1. 보안과 관련하여 체계적으로 많은 옵션을 제공하기 때문에 편리하게 사용할 수 있다.

2. Filter 기반으로 동작하기 때문에 MVC와 분리하여 관리하고 동작한다.

3. 어노테이션을 통해 설정을 간단하게 할 수 있다.

4. 기본적으로 세션과 쿠키 방식으로 인증을 한다.

5. 인증 관리자(Authentication Manager)와 접근 결정 관리자(Access Decision Manager)를 통해 사용자의 리소스 접근을 관리한다.

6. 인증 관리자는 UserNamePasswordAuthenticationFilter, 접근 관리자는 FilterSecurityInterceptor가 수행한다.

 

처음 보면 이해가 잘 되지 않겠지만, 최대한 쉽게 구조를 설명해보도록 하겠다.

Spring Security의 인증 구조

 

1. 사용자가 Form을 통해 로그인 정보를 입력하고 인증 요청을 보낸다.

 

2. AuthenticationFilter(구현체 - UserNamePasswordAuthentecationFilter)가 HttpServletRequest에서 사용자가 보낸 Id, Password를 인터셉트, 즉 가로챈다. 안전을 위해 사용자가 보낸 Id와 Password의 유효성을 검사하는 것이다.

HttpServletRequest에서 꺼내온 Id와 Password를 인증을 담당할 AuthenticationManager 인터페이스(구현체 - ProviderManager)에게 인증용 객체(UsernamePasswordAuthenticationToken)로 만들어서 위임한다.

 

3. AuthenticationFilter에게 위에서 만든 인증용 객체(UsernamePasswordAuthenticationToken)를 전달받는다.

 

4. 실제 인증을 담당하는 AuthenticationProvider에게 Authentication 객체(UsernamePasswordAuthenticationToken)를 전달한다.

 

5. 데이터베이스에서 사용자의 인증 정보를 가져올 UserDetailsService 객체에게 사용자의 Id를 넘겨주고, 데이터베이스에서 인증에 사용할 사용자의 정보를 UserDetails 객체로 전달받는다. 인증용 객체와 도메인 객체를 분리하지 않기 위해 실제 사용되는 도메인 객체에 UserDetails를 상속하는 것이 좋다.

 

6. AuthenticationProvider는 UserDetails 객체를 전달받은 이후, 실제 사용자의 입력정보와 UserDetails 객체를 가지고 인증을 시도한다.

 

7. 인증이 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후, AuthenticationSuccessHandler를 실행한다. (실패시 AuthenticationFailureHandler를 실행한다.)

 

글만 읽어서는 감이 잘 잡히지 않을 것이다. 그럼, 각 모듈이 어떤 역할을 하는지 자세히 알아보도록 하자.

Spring Security의 주요 모듈은 다음과 같다.

 

1. SecurityContextHolder

현재 사용자의 보안 컨텍스트를 제공하는 클래스이다. 이를 통해 현재 사용자의 인증 정보와 권한 정보에 접근할 수 있다.

 

2. SecurityContext

현재 사용자의 보안 정보를 저장하는 인터페이스이다. SecurityContext Authentication 객체와 사용자의 권한 정보를 포함하고 있다.

 

3. Authentication

사용자의 인증 정보를 나타내는 인터페이스이다. Authentication 객체는 Security Context에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고 SecurityContext를 통해 Authentication에 접근할 수 있다.

 

4. UserNamePasswordAuthenticationToken

사용자의 인증 정보를 나타내는 구체적인 구현 클래스이다. Authentication 인터페이스를 구현하며, 사용자의 아이디와 비밀번호를 사용하여 인증하는데 사용된다.

UserNamePasswordAuthenticationToken은 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, User의 ID가 Principal 역할을 하고, Password가 Credential의 역할을 한다. 

여기서 Principal은 접근 주체, 즉 보호받는 리소스에 접근하는 대상을 의미하고 Credential은 비밀번호, 즉 리소스에 접근하는 대상의 비밀번호를 의미한다. 

UserNamePasswordAuthenticationToken의 첫 번째 생성자는 인증 전의 객체를 생성하고 두 번째 생성자는 인증이 완료된 객체를 생성한다. 

 

5. AuthenticationProvider

인증 처리를 담당하는 핵심 인터페이스로 실제로 사용자의 인증을 처리하고 검증하는 역할을 수행한다.

public interface AuthenticationProvider {
	// 인증 전의 Authenticaion 객체를 받아서 인증된 Authentication 객체를 반환
    Authentication authenticate(Authentication var1) throws AuthenticationException;

    boolean supports(Class<?> var1);
}

위의 코드처럼 AuthenticationProvider 인터페이스를 구현하여 AuthenticaticationManager에 등록하면 인증 전의 Authentication 객체를 받아 인증이 완료된 객체를 반환한다.

 

6. AuthenticationManager

인증에 대한 부분은 AuthenticationManager을 통해 처리되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다. 인증이 성공하면 두 번째 생성자를 통해 인증이 성공한 객체를 생성하여 Security Context에 저장한다. 인증 상태를 유지하기 위해 세션이 보관하고, 인증이 실패한 경우에는 AuthenticationException을 발생시킨다. 

AuthenticationManager를 implements한 ProviderManager는 실제 인증 과정을 갖고 있는  AuthenticaionProvider를 List로 가지고 있으며, ProividerManager는 for문을 통해 모든 provider를 조회하면서 authenticate 처리를 한다.

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
    public List<AuthenticationProvider> getProviders() {
		return providers;
	}
    
    public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();
        
      		 //모든 provider를 돌며 처리하고 result가 나올 때까지 반복
		for (AuthenticationProvider provider : getProviders()) {
            ....
			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
           
			catch (AccountStatusException e) {
				prepareException(e, authentication);

				throw e;
			}
            ....
		}
		throw lastException;
	}
}

 

7. UserDetails

인증에 성공하여 생성된 UserDetails 객체는 Authentication 객체를 구현한 UserNamePasswordAuthenticationToken을 생성하기 위해 사용된다. 

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
    
}

UserDetails는 위와 같이 정보를 반환하는 메소드를 가지고 있는데, 나는 주로 엔티티에서 implements UserDetails를 사용하여 처리한다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "User")
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false, unique = true)
    private String password;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Builder
    public User(String email, String password) {
        this.email = email;
        this.password = password;
    }

    public User(String email, String password, Collection<? extends GrantedAuthority> authorities) {
    }

    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

8. UserDetailsService

UserDetailsService 인터페이스는 UserDetails 객체를 반환하는 메소드를 가지고 있는데, 이를 구현한 클래스 내부에 UserRepository를 주입받아 데이터베이스와 연결하여 처리한다.

public interface UserDetailsService {

    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;

}

 

9. PasswordEncoding

비밀번호 암호화에 사용될 PasswordEncoder 구현체를 지정한다. 

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig{

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    }

}

Security Config에 등록하여 사용할 수 있다. 비밀번호는 암호화시키는 것이 중요하기 때문에 사용법을 꼭! 알아둬야 한다.

 

10. GrantedAuthority

현재 사용자가 가지고 있는 권한을 나타낸다. ROLE_*의 형태로 사용하며 보통 "roles"라고 한다.

GrantedAuthority 객체는 UserDetailsService에 의해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정한다.

 

 

Servlet Authentication Architecture :: Spring Security

ProviderManager is the most commonly used implementation of AuthenticationManager. ProviderManager delegates to a List of AuthenticationProvider instances. Each AuthenticationProvider has an opportunity to indicate that authentication should be successful,

docs.spring.io

더 궁금한 점이 있다면 위의 공식문서를 통해 공부하는 걸 추천한다.

 

Spring Security를 공부하면 이 복잡한 걸 도대체 왜 사용하는 걸까? 라는 의문이 들 수 있다. (본인 얘기...)

찾아보니 Spring Security 가이드에서 8가지 이유를 적어놨다.

 

1.  모든 URL에 대해 인증을 요구한다.

2. 로그인 폼을 생성한다.

3. 기초적인 폼에 대해 사용자 이름과 비밀번호를 요구한다.

4. 로그아웃 기능이 있다.

5. CSRF 공격을 방어한다. 

6. Sesseion Fixsation을 방어한다.

7. 요청 헤드 보안을 강화한다.

8. Servlet API를 제공한다.

 

많은 이유가 있다. 물론 직접 구현할 수 있는 것이지만, 스프링에서 제공해주는 것을 사용하는 것도 좋을 것 같다!

 

Security를 많이 쓰는 추세이기에 이제는 필수가 되어가는 것 같다.. 복잡하긴 해도 많은 장점이 있으니 더 깊게 공부해야겠다.

다음에는 어떤 식으로 코드에 적용할 수 있는지 간단한 예제로 포스팅을 하도록 하겠다.