Spring Security는 우리가 스프링 관련 프로젝트를 진행할 때, 로그인 기능을 구현하다 보면 자연스럽게 접할수 있다. 모두의 마당, 식구하자 프로젝트를 진행하면서 시큐리티를 다뤘지만, 구현에 집중하느라 Security 내부 동작에 대해선 이해가 부족한 채로 진행했다. 이번 기회를 통해 내부 동작 원리에 대해서 공부해보자 !
스프링 시큐리티 (Spring Security)는 스프링 기반 어플리케이션의 보안(인증과 권한, 인가)을 담당하는 스프링 하위 프레임워크.
보안과 관련해서 체계적으로 많은 옵션들을 제공해주기 때문에 개발자의 입장에서는 하나하나 보안 관련 로직을 작성하지 않아도 된다는 장점이 있다.
API에 권한 기능이 없으면, 아무나 회원 정보를 조회하고 수정하고 삭제할 수 있다. 따라서 이를 막기 위해 인증된 유저만 API를 사용할 수 있도록 해야하는데, 이때 사용할 수 있는 해결 책 중 하나가 Spring Security다.
인증, 인가 개념에 대해서 집중적으로 이해해보자!
본인인지
확인하는 절차자원에 접근 가능한지
결정하는 절차Spring Security는 기본적으로 인증 절차를 거친 후 인가 절차를 진행하게 되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지를 확인한다. 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 인증 방식을 사용한다.
spring security 에서는 기본적으로 세션 - 쿠키 방식을 사용하고 있다.
스프링 시큐리티 구조의 처리 과정을 간단히 설명하면 다음과 같습니다다.
public interface Authentication extends Principal, Serializable {
// 현재 사용자의 권한 목록을 가져옴
Collection<? extends GrantedAuthority> getAuthorities();
// credentials(주로 비밀번호)을 가져옴
Object getCredentials();
Object getDetails();
// Principal 객체를 가져옴
Object getPrincipal();
// 인증 여부를 가져옴
boolean isAuthenticated();
// 인증 여부를 설정함
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
} public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { \
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// 주로 사용자의 ID에 해당
private final Object principal;
// 주로 사용자의 PW에 해당
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;
super.setAuthenticated(true);
// must use super, as we override
}
}
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
// for문으로 모든 provider를 순회하여 처리하고 result가 나올때까지 반복한다.
for (AuthenticationProvider provider : getProviders()) { ... }
}
}
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder.getContext().getAuthentication(authentication);