1️⃣ Spring Security Filters
Spring Security Filter들은 HTTP 요청을 처리하고 보안 관련 작업을 수행한다.
만약 인증이 필요하면 사용자 로그인으로 안내하거나, 인증받은 정보가 있다면 기존의 정보를 사용한다.
일반적으로 웹 애플리케이션에서 사용자의 로그인 요청을 처리하고 인증하는 데 UsernamePasswordAuthenticationFilter가 사용된다. 이는 HTTP 요청에서 username과 password를 추출해 Authentication 타입 객체를 준비한다.
Authentication은 인증된 사용자의 세부 정보를 저장하는 클래스이다.
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
실제 UsernamePasswordAuthenticationFilter와 메서드이다. 해당 메서드에서는 request가 POST 방식으로 넘어왔는지, username과 password의 null인지 등을 체크한다.
그 후 UsernamePasswordAuthenticationToken 객체를 생성한 후 authenticate 메서드를 통해 AuthenticationManager(인증 관리자)로 전달한다. 해당 토큰은 아직 인증되지 않은 Authentication이다.
Authentication과 UsernamePasswordAuthenticationFilter은 모두 사용자의 인증을 처리하는 데 관련된 요소이지만, Authentication은 개념적인 측면에서 사용자의 신원을 확인하는 과정을 나타내며, UsernamePasswordAuthenticationFilter는 이를 실제로 처리하는 Spring Security의 필터 중 하나이다. Authentication은 UsernamePasswordAuthenticationFilter를 포함하여 다양한 방법으로 수행될 수 있다.
❗️ 왜 Authentication이 아닌 UsernamePasswordAuthenticationToken을 넘길까?
상속도를 보면 UsernamePasswordAuthenticationToken 객체는 Authentication 인터페이스를 구현한 객체임을 알 수 있다.
2️⃣ Authentication Manager
Spring Security에는 사용자 아이디 / 비밀번호를 인증 및 처리하는 과정에 AuthenticationManager, AuthenticationProvider가 있다.
Manager은 공장 안에서 작업 처리를 지시하는 매니저라고 생각하고,
Provider은 Manager가 시킨 일을 직접 하는 작업자라고 생각하면 쉽게 이해할 수 있다.
Authentication Manager은 필터로부터 요청받은 사용자의 세부 정보의 유효성을 검사하기 위해 요청을 Authentication Provider로 위임한다. 애플리케이션 내에 여러 Provider가 있을 수 있으므로 Authentication Manager은 사용가능한 모든 Authentication Provider들을 관리하는 책임이 있다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager은 인터페이스이다.
ProvideManager는 AuthenticationManager 인터페이스를 구현한 클래스이다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// ...
// 사용 가능한 Provider 탐색
for (AuthenticationProvider provider : getProviders()) {
try {
// Provider 찾으면 일 시키기
result = provider.authenticate(authentication);
}
}
// ... 코드 생략
}
}
ProvideManager 내부의 autenticate 메서드에서 getProviders() 메서드로 사용 가능한 Provider를 탐색하여 provider의 authenticate 메서드를 호출한다.
3️⃣ Authentication Providers
Authentication Providers에는 사용자의 세부 정보의 유효성을 검사하는 모든 핵심 로직이 있다.
public Authentication authenticate(Athentication authentication)
4️⃣ UserDetailService
최하단 3개의 클래스는 Spring Security에서 기본적으로 제공하는 매니저이며, 개발자가 정의한 매니저를 사용할 수 있다.
🌱 UserDetailsService
UserDetailsService 인터페이스에는 loadUserByUsername() 메서드만 정의되어 있으며 클라이언트에게 받은 username을 검색한다. 이때 검색한 username으로 UserDetail을 반환한다.
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
🌱 UserDetailsManager
UserDetailsManager는 UserDetailsService를 상속받은 인터페이스이다.
해당 인터페이스는 User의 생성, 수정, 삭제 등의 기능을 제공한다.
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);
void updateUser(UserDetails user);
void deleteUser(String username);
void changePassword(String oldPassword, String newPassword);
boolean userExists(String username);
}
🌱 InMemoryUserDetailsManager
InMemoryUserDetailsManager는 UserDetailsManager를 상속받은 클래스이며 Spring Security가 기본적으로 제공하는 클래스 중 하나이다.
public class InMemoryUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = this.users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),
user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
}
public class User implements UserDetails, CredentialsContainer {
// ...
}
구현된 loadUserByUsername 메서드를 보면 username으로 검색한 뒤 User를 반환하는 것을 볼 수 있다. 이는 User가 UserDetails를 구현한 클래스이기 때문에 반환될 수 있는 것이다.
5️⃣ SecurityContext
요청이 인증되면 일반적으로 Autentication은 SecurityContextHolder가 관리하는 스레드 로컬 SecurityContext에 저장된다.
이는 동일한 사용자의 요청을 처리하는 데 도움이 된다.
❗️ SecurityContextHolder와 SecurityContext
🫧 SecurityContextHolder
SecurityContext를 감싸는 객체
실제 SecurityContext를 위한 ThreadLocal을 가지는 객체이다.
🫧 SecurityContext
필터를 거친 Authentication(인증된 객체)가 저장되는 저장소
일반적으로 ThreadLocal에 저장되기 때문에 전역적으로 SecurityContext에 접근이 가능하다.
❗️ Authentication과 UserDetails의 차이
SecurityContext에 저장되는 Authentication과 UserDetailsService에서 반환하는 UserDetails와의 차이점은 무엇일까?
Authentication 타입 객체는 Spring Security 전반에 사용되는 객체이며, 인증의 성공 여부를 확인하는 역할이다.
UserDetails 타입의 객체는 UserDetailsService와 UserDetailsManager에서 사용되는 객체이며, 데이터베이스나 해시맵 같은 인메모리 저장 공간에서 유저의 정보를 로드할 때 사용된다.
Spring Security 필터들을 거친 이후 유저 정보를 추출하여 Authentication을 구현한 토큰을 생성한다. 그다음 AuthenticationManager는 AuthenticationProvider들을 authenticate() 메서드로 호출한다.
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
...
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
...
}
...
}
...
return createSuccessAuthentication(principalToReturn, authentication, user);
}
여기 try-catch 문에서 retrieveUser 메서드를 호출하는데,
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
...
}
catch (InternalAuthenticationServiceException ex) {
...
}
catch (Exception ex) {
...
}
}
retrieveUser 메서드는 loadUserByUsername 메서드를 호출해 UserDetails 타입의 loadUser을 반환한다.
그 후, retrieveUser를 호출한 authenticate 메서드에서는 반환된 UserDetails 객체를 createSuccessAuthentication 메서드의 파라미터로 넣어 리턴한다.
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = UsernamePasswordAuthenticationToken.authenticated(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
createSuccessAuthentication 메서드는 Authentication, 즉 인증 객체를 반환한다.
Authentication 타입 객체는 Spring Security 전반에 사용되는 객체이며, 인증의 성공 여부를 확인하는 역할이다.
UserDetails 타입의 객체는 UserDetailsService와 UserDetailsManager에서 사용되는 객체이며, 데이터베이스나 해시맵 같은 인메모리 저장 공간에서 유저의 정보를 로드할 때 사용된다.
UserDetailsManager는 자신을 호출한 Provider에게 UserDetails를 넘겨주며, Provider는 별도의 메서드를 사용해 Authentication을 넘겨준다. 즉, 아이디나 비밀번호와 같은 유저의 정보는 리턴되지 않는다.
📚 참고
https://jaykaybaek.tistory.com/27
'Back-end' 카테고리의 다른 글
[JPA] JPA Entity에서의 equals(), hashCode() (0) | 2024.05.16 |
---|---|
Postman 환경변수 자동 세팅(토큰값 자동 세팅) (0) | 2024.05.06 |
Java Record로 DTO를 만들어봅시다 (0) | 2024.05.01 |
[Spring] Meta Annotation이란? (@Target, @Retention 등) (0) | 2024.04.29 |
[JPA] @SQLDelete와 영속성 컨텍스트 (0) | 2024.04.22 |