Spring Security란?
Spring Security는 Spring 기반 애플리케이션의 인증, 인가 및 보안을 담당하는 스프링 하위 프레임워크입니다.
Spring Security는 인증(Authentication)과 권한(Authorization)에 대한 부분을 Filter 기반으로 동작하고 Spring MVC와 분리되어 관리 및 동작하며, 데이터 보호 기능을 포함하여 웹 개발 과정에서 필수적인 사용자 관리 기능을 구현하는데 도움을 줍니다.
Spring Security 3.2부터는 XML을 설정하지 않고 Java Bean 설정으로 간단하게 설정할 수 있도록 지원해줍니다.
Spring Security를 사용하는 이유
Spring Security를 사용하는 이유는 Spring의 생태계에서 보안에 필요한 기능들을 제공하기 때문에, 개발자 입장에서 일일이 보안 관련 로직을 작성하지 않아도 된다는 장점이 있습니다. Spring Security는 Spring이라는 프레임워크에서 활용하기 적절한 구조로, 보안 기능을 추가할 때 활용하기 좋습니다. 프레임워크를 사용하지 않고 코드를 직접 작성할 경우 Spring에서 추구하는 IoC/DI 패턴과 같은 확장 패턴을 고려하여 인증/인가 부분을 직접 개발하기 쉽지 않지만, Spring Security에서는 이와 같은 기능들을 제공해 주기 때문에 개발 작업 효율을 높일 수 있습니다. 그러나, Spring Security를 사용하지 않는다면 직접 Session을 체크하고 redirect 등을 해야만 합니다.
Spring Security Architecture와 처리 과정
- 사용자가 로그인 정보와 함께 인증 요청을 합니다.
- AuthenticationFilter가 요청을 가로채서 UsernamePasswordAuthenticationToken의 인증용 객체를 생성합니다.
- AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달합니다.
- AuthenticationManager는 등록된 AuthenticationProvider들을 조회하여 인증을 요구합니다.
- 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨줍니다.
- 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듭니다.
- AuthenticationProvider들은 UserDetails를 넘겨받고 사용자 정보를 비교합니다.
- 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.
- 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.
- Authentication 객체를 SecurityContext에 저장합니다.
- SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장합니다.
인증(Authentication)과 인가(Authorization)
- 인증(Authentication) : 해당 사용자가 본인이 맞는지 확인합니다.
- 인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지를 결정합니다.
Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인합니다. Spring Security는 위 플로우에서 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용합니다.
- Principal(접근 주체) : 보호받는 Resource에 접근하는 대상입니다.
- Credential(비밀번호) : Resource에 접근하는 대상의 비밀번호입니다.
Spring Security 모듈
Spring Security의 주요 모듈은 아래와 같이 구성되어 있습니다.
SecurityContextHolder
SecurityContextHolder는 보안 주체의 세부 정보를 포함하여 응용 프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장됩니다. SecurityContextHolder는 기본적으로 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 방법과 SecurityContextHolder.MODE_THREADLOCAL 방법을 제공합니다.
SecurityContext
Authentication을 보관하는 역할을 하며, SecurityContext를 통해 Authentication 객체를 꺼내올 수 있습니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder.getContext().getAuthentication(authentication);
Authentication
Authentication은 현재 접근하는 주체의 정보와 권한을 담는 인터페이스입니다. Authentication 객체는 Security Context에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근하고, SecurityContext를 통해 Authentication에 접근할 수 있습니다.
public interface Authentication extends Principal, Serialzable {
// 현재 사용자의 권한 목록을 가져옴
Collection<? extends GrantedAuthority> getAuthorities();
// Principal 객체
Object getPrincipal();
// credentials 객체
Object getCredentials();
Object getDetails();
// 인증 여부
boolean isAuthenticated();
// 인증 여부를 설정
void setAuthenticated(boolean isAuthenticated) thrwos IllegalArgumentException;
}
UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken은 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, User의 Id가 Principal 역할을 하고, Password가 Credential의 역할을 합니다. UsernamePasswordAuthenticationToken의 첫 번째 생성자는 인증 전의 객체를 생성하고, 두 번째 생성자는 인증이 완료된 객체를 생성합니다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// 주로 사용자의 Id
private final Object principal;
// 주로 사용자의 Password
private Object credentials;
// 인증 완료 전의 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
// 인증 완료 후의 객체 생성
public UsernamePasswordAuthenticationToken(
Object principal,
Object credentials,
Collection<? extends GrantedAuthority> authorities
) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(true);
}
}
public abstract class AbstractAuthentiacationToken implements Authentication, CredentialsContainer {
}
AuthenticationManager
인증에 대한 부분은 Spring Security의 AuthenticationManager를 통해서 처리하게 되는데, 실질적으로 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리됩니다. 인증이 성공하면 두 번째 생성자를 이용해 인증이 성공한(isAuthenticated = true) 객체를 생성하여 Security Context에 저장합니다. 그리고 인증 상태를 유지하기 위해 세션에 보관하며, 인증이 실패한 경우에는 AuthenticationException를 발생시킵니다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationProvider
AuthenticationProvider에서는 실제 인증에 대한 부분을 처리하는데, 인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 합니다. 아래와 같은 AuthenticatonProvider 인터페이스를 구현해서 Custom 한 AuthenticationProvider을 작성해서 AuthenticationManager에 등록하면 됩니다.
public interface AuthenticationProvider {
// 인증 전의 Authentication 객체를 받아서 인증된 Authentication 객체를 반환
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
ProviderManager
AuthenticationManager를 implements 한 ProviderManager는 실제 인증 과정에 대한 로직을 가지고 있는 AuthenticationProvider를 List로 가지고 있으며, ProviderManager는 반복문을 통해 모든 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();
//for문으로 모든 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;
}
}
UserDetails
인증에 성공하여 생성된 UserDetails 객체를 Authentication 객체를 구현한 UsernamePasswordAuthenticationToken을 생성하기 위해 사용됩니다. UserDetails 인터페이스를 살펴보면 아래와 같이 정보를 반환하는 메서드를 가지고 있습니다. UserDetails 인터페이스의 경우 직접 개발한 UserVO 모델에 UserDetails를 implements 하여 이를 처리하거나 UserDetailsVO에 UserDetails를 implements 하여 처리할 수 있습니다.
public interface UserDetails extends Serialzable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpried();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetailsService
UserDetailService 인터페이스는 UserDetails 객체를 반환하는 단 하나의 메서드를 가지고 있는데, 일반적으로 이를 구현한 클래스의 내부에 UserRepository를 주입받아 DB와 연결하여 처리합니다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
Password Encoding
AuthenticationManagerBuilder.userDetailsService().passwordEncoder()를 통해 패스워드 암호화에 사용될 PasswordEncoder 구현체를 지정할 수 있습니다.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.userDetailsService(userDetialsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
GrantedAuthority
GrantedAuthority는 현재 사용자(principal)가 가지고 있는 권한을 의미합니다. ROLE_ADMIN난 ROLE_USER와 같이 ROLE_*의 형태로 사용합니다. GrantedAuthority 객체는 UserDetailsService에 의해 불러올 수 있고, 특정 자원에 대한 권한이 있는지를 검사하여 접근 허용 여부를 결정합니다.