들어가며
어드민 페이지를 만들때 권한에 따라서 볼 수 있는 페이지와 볼 수 없는 페이지 또는 동작 등을 관리할 수 있다. 그렇기 때문에 현재 만들고 있는 어드민 페이지에 Spring Security를 이용하여 해당 기능을 적용해서 구현을 하기전 기본적인 동작원리와 간단하게 한 사이클을 돌 수 있는 demo page를 만든 것을 정리한다. 여기서는 따로 전체 코드를 기술하지는 않고 깃헙 링크로 대체하며, Spring Security의 기본 동적과정과 정의 등의 집중해서 정리한다. SpringSecurity를 딥하게 들어가면 워낙 양이 방대하므로 로그인 인증과정에 한하며 전체 사이클과 로직에 대해서 정리하도록 한다. 추가로 FilterChain에 속해있는 객체들의 역할에 대해서도 정리한다.
인증과 권한
스프링 시큐리티를 이해하기 위해서는 다음 두 단어를 이해하고 있어야 한다. 인증은 application의 접근한 대상에 대하여 어느 유저인지 그리고 작업을 수행해도 되는 주체인지를 확인하는 과정을 말하며 권한은 인증된 대상이 application에 동작을 수행할 수 있는 권한을 가졌는지에 대한 것이다. 즉 권한이 필요한 곳에 접근하기 위해서 인증과정을 통해 주체가 증명되어야 한다는 것이다.
이때 권한 부여 영역에는 웹 요청 권한과 메소드 호출 및 도메인 인스턴스에 대한 접근 권한으로 나뉜다.
인증하기 전에 인증이 필요한 상태에서의 요청
로그인에 관한 흐름을 알아보기 전에 아직 인증이 되지 않은 상황에서의 요청에 대해서 먼저 알고가자.
스프링시큐리티를 이용하게 되면 모든 요청은 Session을 발급 받는다. (스프링시큐리티는 세션과 쿠키를 이용한 기술임.) Session을 발급받으면 클라이언트의 쿠키에 JSESSIONID라는 키로 SessionID가 저장된다. 앞으로 아래의 설명하게 될 AuthenticationFilter는 해당 요청의 JSESSIONID를 확인하여 매핑되는 인증 정보가 SecurityContext에 있는지 판단 후 없다면 Login 페이지로 이동시킨다.
스프링 시큐리티 인증 아키텍쳐
자 이제 앞에 인증하기 전에 인증이 필요한 상태에서의 요청이 지났다는 가정하에 스프링 시큐리티 인증 아키텍쳐의 흐름을 통해 각 단계별로 어떤 일들이 일어나는지 알아본다. (Form 로그인에 대한 흐름이다. )
1. Http Request 수신 (로그인 양식으로 인증 요청)
- 스프링 시큐리티는 필터로 동작한다.
- 요청이 들어오면 인증과 권한을 위한 필터들을 통하게 된다.
- 예를들어 유저가 HTTP 기본 인증을 요청하면 BasicAuthenticationFilter를 통과한다. 만약 HTTP Digest 인증을 요청하면 DigestAuthenticationFilter를 통과한다. 이렇듯이 요청에 따라서 작동하는 필터들이 다르다.
+ 구현 후에 디버그를 통해 확인해보면 아래 사진과 같이 여러 필터들이 묶여있는데 이것을 필터체인이라고 표현한다.
2. 로그인 요청을 받으면 AuthenticationFilter가 HttpServletRequest에서 사용자가 보낸 아이디와 패스워드를 인터셉트한다. AuthenticationFilter는 기본적으로 로그인 폼으로부터 오는 데이터를 username과 password로 인식하고 있으므로 아래 폼 형식으로 해줘야 한다.
<form action="/login" method="post">
<input type="text" id="username" name="username"/>
<input type="password" id="password" name="password"/>
<button type="submit">Log in</button>
</form>
이렇게 넘어온 username과 password를 이용해 UsernamePasswordAuthenticationToken이라는 인증 객체를 만든다. 그리고 username과 password가 유요한 계정인지 판단하기 위해 AuthenticationManager에게 위임한다.
3. Filter를 통해 UsernamePasswordAuthenticationToken을 전달 받은 AuthenticationManager는 인터페이스로 정의되어 있으며 실제 구현은 ProviderManager에서 한다. 그리고 AuthenticationProcider들을 연쇄적으로 실행시킨다.
4. AuthenticationProvider의 구현체에서는 토큰에 있는 계정 정보가 유효한지 판단하는 로직을 구현해야 한다. (디비로부터 조회해오는) 아래 그림과 코드를 보면 이해가 빠를 것이다. 더불어 AuthenticationProvider가 인증을 위해 제공하는 것들에는 아래 것들이 존재한다.
- CasAuthenticationProvider
- JaasAuthenticationProvider
- DaoAuthenticationProvider
- OpenIDAuthenticationProvider
- RememberMeAuthenticationProvider
- LdapAuthenticationProvider
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
/**
* Spring Security 필수 메소드 구현
*
* @param email 이메일
* @return UserDetails
* @throws UsernameNotFoundException 유저가 없을 때 예외 발생
*/
@Override // 기본 반환타입은 UserDetails 이지만 User가 UserDatails를 상속받았으므로 자동으로 다운캐스팅
public User loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException((email)));
}
5. UserDetailsService는 username 기반의 user details를 검색한다. 이때 AuthenticationProvider에서 제공하는 DaoAuthenticationProvider를 사용한다. UserDetailsService를 상속받은 서비스 객체는 loadUserByUsername을 재정의하여 디비에 계정 정보를 확인하는 로직을 구현하고 디비 정보가 유효하다면 유저의 상세 정보를 이용해 새로운 UserPasswordAuthenticationToken을 발급한다.
public interface UserDetailsService
{
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
6. User 객체의 정보들을 UserDetails가 UserDetailsService에 전달한다. 이렇게 하기 위해 다음과 같이 User 객체를 정의해주면 된다. 결론적으로 UserDetailsService가 권한 정보를 알 수 있도록 User라는 객체를 통해 정보를 받을 수 있어야 한다는 것이다.
public class User implements UserDetails {
@Id
@Column(name = "code")
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long code;
@Column(name = "email", unique = true)
private String email;
@Column(name = "password")
private String password;
@Column(name = "auth")
private String auth;
...
}
7. AuthenticationProvider는 UserDetails 객체를 전달 받은 이후 실제 사용자의 입력정보와 UserDetails 객체를 가지고 인증을 시도한다.
8. 유저의 인증이 성공하면 전체 인증정보를 리턴하고 실패한다면 AuthenticationException을 던진다. 인증에 성공한 객체의 정보에는 authenticated - true, grant authorities list - 권한 정보, user credentials : username으로 인증된 사항 (username only) 가 들어있다.
9. AuthenticationManager는 완전한 인증 객체를 AuthenticationFilter에게 반환한다.
10. AuthenticationFilter는 인증 객체를 SecurityContext에 저장한다.
SecurityContextHolder.getContext().setAuthentication(authentication);
이렇게 로그인 관련 인증 부분에 대한 전체 사이클이 끝났다. 마지막으로 중간에 나왔던 개념인 FilterChain에는 어떠한 것들이 있고 역할은 무엇인지 정리한다.
SecurityFilterChain
1. SecurityContextPersistenceFilter
요청(request)전에, SecurityContextRepository에서 받아온 정보를 SecurityContextHolder에 주입
2. LogoutFilter
주체(Principal)의 로그아웃을 진행한다. (주체는 보통 유저를 말함)
3. UsernamePasswordAuthenticationFilter
(로그인) 인증 과정을 진행.
- 인증 성공 시 얻은 인증 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 인증 실패 시 AuthenticationFailureHandler 실행
4. DefaultLoginPageGeneratingFilter
사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지를 처리.
5. BasicAuthenticationFilter
HTTP 요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장한다.
6. RememberMeAuthenticationFilter
SecurityContext에 인증(Authentication) 객체가 있는지 확인하고RememberMeServices를 구현한 객체의 요청이 있을 경우, Remember-Me(ex 사용자가 바로 로그인을 하기 위해서 저장 한 아이디와 패스워드)를 인증 토큰으로 컨텍스트에 주입한다.
7. SecurityContextHolderAwareRequestFilter
HttpServletRequestWrapper를 상속한SecurityContextHolderAwareRequestWapper 클래스로 HttpServletRequest 정보를 감싼다. SecurityContextHolderAwareRequestWrapper 클래스는 필터 체인상의 다음 필터들에게 부가정보를 제공한다.
8. AnonymousAuthenticationFilter
SecurityContextHolder에 인증(Authentication) 객체가 있는지 확인하고, 필요한 경우 Authentication 객체를 주입한다.
9. SessionManagementFilter
요청이 시작된 이 후 인증된 사용자 인지 확인하고, 인증된 사용자일 경우SessionAuthenticationStrategy를 호출하여 세션 고정 보호 메커니즘을 활성화하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행한다.
10. ExceptionTranslationFilter
필터 체인 내에서 발생(Throw)되는 모든 예외(AccessDeniedException, AuthenticationException)를 처리한다.
11. FilterSecurityInterceptor
HTTP 리소스의 보안 처리를 수행한다.
Spring Security demo code
https://github.com/sjparkk/spring-security-demo
'Spring' 카테고리의 다른 글
Spring - @ConfigurationPropertiesScan 어노테이션 (0) | 2022.01.19 |
---|---|
Spring - Transaction 정의와 Spring에서의 Transaction (0) | 2021.12.08 |
JPA Auditing 을 이용한 생성 시간과 수정 시간 자동화하기 (0) | 2021.10.08 |
Spring - Singleton 컨테이너 정리. (0) | 2021.08.02 |
Spring - JUnit5을 이용한 단위 테스트 (기본 어노테이션 및 AssertJ) (0) | 2021.08.01 |
댓글