Spring Security는 Java 기반의 애플리케이션에서 보안 인증과 권한 부여를 처리하기 위한 프레임워크이다. 스프링 시큐리티는 다양한 인증 메커니즘을 지원하고, 다양한 방식으로 사용자 인증을 구현할 수 있다. 이번 블로그를 통해 스프링 시큐리티의 동작원리에 대해 이해하고, 대표적인 특징에 대해 알아보려고 한다.
1. 스프링 시큐리티의 구조
1. Http Request 수신
클라이언트로부터 로그인정보와 함께 인증 요청을 하게 되면 AuthenticationFilter에서 인증 및 권한 부여의 목적으로 일련의 필터를 거치게 된다.
로그인이 이미 되어있다면!!
이미 Authentication 객체가 Http Session에 저장되어 있기 때문에 AuthenticationManager의 authenticate() 메서드를 호출하지 않고 SecurityContextHolder에서 Authenticatin 객체를 가져와 인증 후 곧바로 클라이언트의 요청을 수행한다.
▶ 시큐리티에서 제공하는 필터(유저 인증 관련)
- UsernamePasswordAuthenticationFilter : 사용자의 이름과 비밀번호를 사용한 기본 폼 기반 인증을 처리하는 필터로, 사용자가 로그인을 하게 되면 이 필터가 사용자를 인증하고 세션에 사용자 정보를 저장한다.
- DefaultLoginPageGeneratingFilter : 기본 로그인 페이지를 생성하고 제공하는 역할
- DefaultLogoutPageGeneratingFilter : 기본 로그아웃 페이지를 생성하고 제공하는 역할
- RememberMeAuthenticationFilter : 사용자가 이전에 로그인한 정보를 기반으로 자동 로그인을 지원하는 역할
- RequestCacheAwareFilter : 인증되지 않은 사용자가 접근하려는 리소스에 대한 요청을 캐시 하고, 사용자가 로그인한 후 이전 요청으로 돌아갈 수 있게끔 도와주는 필터
- OAuth2LoginAuthenticationFilter : OAuth2.0을 통한 로그인을 처리하는 필터. 외부 로그인 서비스(카카오톡, 네이버, 구글 등)를 지원한다.
- JwtAuthenticationFilter : JWT(JSON Web Token)를 사용한 인증을 처리하는 필터. 클라이언트가 JWT 토큰을 제공하면 필터가 해당 토큰으로 사용자를 인증한다.
- CasAuthenticationFilter : Central Authentication Service(CAS)를 통해 인증하는 필터로, 싱글 로그인(SSO, Single Sign-On) 환경에서 사용된다. (SSO : 여러개의 사이트에서 한 번의 로그인으로 여러 가지 다른 사이트들을 자동적으로 접속하여 이용할 수 있도록 하는 서비스)
2. 사용자 자격증명을 기반으로 한 AuthenticationToken 생성
유저의 인증 요청에서 AuthenticationFilter에 의해 유저의 이름과 패스워드를 추출한다. 추출된 정보를 기반으로 인증 토큰(UsernamePasswordAuthenticationToken)이 생성된다. 생성된 토큰은 AuthenticationManager의 authenticate() 메서드를 통해 ProviderManager로 넘어가게 된다.
3. AuthenticationManager에게 인증 토큰 위임
UsernamePasswordAuthenticationToken 개체 생성 후 AuthenticationManger의 인증 메서드를 호출하는 데 사용된다. AuthenticationManager 인터페이스이고 실제 구현은 ProviderManager이기에 해당 객체에 인증 토큰을 전달한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// ...
private List<AuthenticationProvider> providers = Collections.emptyList();
// ...
@Override
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 (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
}
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
}
}
}
}
위 클래스는 AuthenticationMananger의 구현체인 ProviderManager 클래스이다. 클래스의 변수를 보면 AuthenticationProvider를 리스트로 관리하고 있다. 그리고 authenticate() 메서드에서 for문으로 리스트를 순환하며 provider 클래스의 supports() 메서드를 호출하며 인증 메커니즘을 수행하도록 한다.
4. Authentication Provider 목록으로 사용자 인증 처리
- DaoAuthenticationProvider : 유저 이름과 비밀번호를 사용한 인증 처리
- JwtAuthenticationProvider : JWT를 사용한 인증 처리
AuthenticationProvider(인증 제공자)의 구현체는 특정 인증 메커니즘에 대한 인증을 수행하며(다중 인증 메커니즘 지원), 사용자의 자격 증명을 확인하고, 사용자를 인증한다. 인증에 성공하면 인증된 Authentication 객체를 반환한다.
※ 개발자가 커스텀하여 AuthenticationProvider를 구현하여 고유한 인증로직을 구현할 수도 있음.
5. UserDetailsService 에 Authentication 넘기기
UserDetailsService는 유저의 정보를 가져오는 인터페이스로 해당 인터페이스를 구현하기 위해서는 DB로부터 유저의 정보를 불러와 UserDetails로 반환하는 loadUserByUsername 메서드를 구현해야 한다.
- 사용자 이름을 기반으로 사용자의 세부 정보를 검색할 수 있다.
메서드 | 리턴 타입 | 설명 |
loadUserByUsername | UserDetails | DB로 부터 유저의 정보를 불러와서 UserDetails로 리턴 |
6. UserDetailsService로부터 UserDetails 검색
UserDetails 인터페이스는 사용자 정보를 캡슐화하고 사용자의 인증 및 권한 정보를 Spring Security와 함께 사용할 수 있도록 설계되었다. DB에서 사용자의 정보를 토대로 검색하고, 해당 데이터를 UserDetails 객체에 매핑시켜 반환한다.
UserDetails의 주요 메서드 및 속성
메서드 | 리턴타입 | 설명 |
getAuthorities() | Collection<? extends GrantedAuthority> | 사용자에게 부여된 권한 목록을 반환 |
getPassword() | String | 사용자의 암호화된 비밀번호 반환 |
getUsername() | String | 사용자의 이름 반환 |
isAccountNonExpired() | boolean | 사용자의 계정이 만료되었는지의 여부 |
isAccountNonLocked() | boolean | 사용자의 계정이 잠겨있는지 여부 |
isCredentialsNonExpired() | boolean | 사용자의 자격증명(비밀번호)이 만료되었는지 여부 |
isEnabled() | boolean | 사용자의 계정이 활성화되었는지 여부 |
7. AuthenticationProviders에게 UserDetails 객체 전달
AuthenticationProviders는 UserDetails를 받아 유저 정보를 비교한다.
8. 성공 시 인증개체 반환 OR 실패 시 인증 예외 발생
인증 완료 시 Principal(주체), Credentials(자격 증명), Authorities(권한), Authenticated(인증 여부)를 설정한 Authentication 객체를 반환하고, 인증에 실패하면 상황에 맞는 예외를 발생시킨다.
Authentication 내부 구조
▶ Principal(주체) : 사용자 아이디 혹은 User 객체
- getPrincipal() 메서드를 통해 사용자를 나타내는 주체(Principal)를 얻을 수 있다.
- 주체는 보통 사용자의 식별정보(사용자 이름, ID 등)를 나타내며, UserDetails 객체로 나타낼 수 있다.
▶ Credentials(자격 증명) : 사용자 비밀번호
- getCredentials() 메서드를 통해 사용자의 자격 증명(비밀번호, 토큰)을 얻을 수 있다.
- 자격 증명은 사용자를 인증하기 위한 정보이다.
▶ Authorities(권한) : 인증된 사용자의 권한 목록
- getAuthorities() 메서드를 통해 사용자에게 부여된 권한목록을 얻을 수 있다.
- 권한은 사용자가 수행할 수 있는 작업이나 접근할 수 있는 리소스를 나타낸다.
▶ Authenticated(인증여부)
- isAuthenticated() 메서드는 사용자가 인증되었는지 여부를 나타내며, true, 또는 false 값을 반환한다.
- 사용자가 인증되면 true, 그렇지 않으면 false를 반환한다.
9. 인증 완료
AuthenticationManager는 Authentication 객체를 관련 AuthenticationFilter로 반환한다.
10. SecurityContext에 Authentication 객체 저장
SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다.
Security Context
- Authentication 객체가 직접 저장되는 저장소.
- 필요시 언제든 SecurityContext에서 Authentication 객체를 가져올 수 있다.
- 동일 스레드 내에서 전역적으로 접근 가능하도록 설계(SecurityContextHolder Strategy에 따라 다름 )
정리
- 클라이언트가 로그인 정보(username, password)와 함께 인증 요청
- AuthenticationFilter(ex. UsernamePasswordAuthenticationFilter)에서 AuthenticationManager에 의해 attemptAuthentication() 메서드 호출
- UsernamePasswordAuthenticationToken 생성 후 인증 메커니즘 진행
- 인증 성공
- Authentication 객체에 Principal, Credentials, Authorities, Details, Authenticated 값을 초기화 후 반환
- SecuritySession을 만들고 SecurityContext에 저장
- 이후의 인증과정은 SecurityContextHolder 내부의 SecurityContext에 접근하여 확인
- Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
참고하면 좋은 사이트
'Spring' 카테고리의 다른 글
스프링 FeignClient (0) | 2023.05.09 |
---|---|
[Spring Batch] Batch 이해하기 (0) | 2023.03.07 |
[Spring Boot JWT Tutorial - 인프런] 스프링 JWT 적용하기 part4. 회원가입 API 구현 및 권한 검증 확인 (0) | 2022.09.03 |
[Spring Boot JWT Tutorial - 인프런] 스프링 JWT 적용하기 part3. Repository, 로그인 API 구현 (0) | 2022.09.03 |
[Spring Boot JWT Tutorial - 인프런] 스프링 JWT 적용하기 part2. JWT 관련 설정하기 (0) | 2022.09.03 |