Spring Security 구조
아래는 Authentication (인증) 요청부터 Spring security 가 어떤 흐름으로 처리하는지 정리한 것이다.
위 그림의 번호와는 상관없다.
- 사용자가 인증을 요청한다
- SecurityConfig 설정 클래스에서 설정한 인증이 필요한 경로에 요청이 왔을때 포함
- 스프링 시큐리티가 요청을 intercept 해서 사용자가 인증됐는지 확인한다. 인증이 안됐다면 로그인 페이지로 리다이렉트 시킨다.
- 유저는 Credential (설정에 따라, 대부분의 경우 username, password) 을 입력한다
- AuthenticaionFilter (credential 이 username,password 라면 UsernamePasswordAuthenticationFilter) 가 입력한 credential 을 확인하고 credential 이 담겨있는 UsernamePasswordAuthenticationToken 을 생성한다.
- UsernamePasswordAuthenticationToken 은 AuthenticationManager 을 구현하는 ProviderManager 로 전달되고, ProviderManager 는 하나 혹은 그 이상의 AuthenticationProvider 에게 인증 과정을 처리하도록 명령한다
- AuthenticationProvider 는 인증을 처리하는데 UserDetailsService를 사용해 사용자의 정보를 가져와서 인증을 처리한다.
- AuthenticationProvider 는 인증을 성공하면 Authentication 객체를 리턴한다.
- Authentication 객체에는 사용자의 인증정보와 authorities (roles) 가 담겨있다.
- UserDetailsService 인터페이스는 AuthenticationProvider 가 인증 처리를 위해 사용한다.
- UserDetails 는 유저 정보가 담겨있다.
- UserDetailsService 내부에 loadByUsername() 메소드가 UserDetails 를 리턴하도록 구현한다.
- 인증이 완료되면 Authentication 을 구현한 UsernamePasswordAuthenticationToken 객체가 생성되고 SecurityContext 에 저장된다.
AuthenticationFilter
스프링의 DispathcerServlet 으로 요청이 가기전에 AuthenticationFilter 에 요청이 도달한다.
여기서 모든 인증과정을 거치고 나서 DispatcherServlet 에 http 요청이 전달되는 것이다.
UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 은 Authentication 인터페이스를 구현한 클래스다.
Authentication 은 javax의 Principal 을 상속 받는다.
Principal (javax) <- Authentication (spring security) <- UsernamePasswordAuthenticationToken (spring security)
username, password 를 캡슐화해서 사용자에 대한 인증 요청을 수행하기 편하도록 만든 클래스라고 보면 된다.
AuthenticationManager
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Authentication (인증) 을 담당하는 중추 역할을 한다.
이 인터페이스에는 authenticate() 메소드 단 하나만 존재하고 다음중 3가지를 리턴한다.
- 전달받은 authentication 이 valid principal ( 즉 적절한 혹은 인증된 사용자라면) Authentication 을 리턴한다.
- invalid principal 이라면 AuthenticationException 을 리턴한다
- 판단 불가능하다면 null 을 리턴한다
실제로는 아래 나올 여러개의 AuthenticationProvider 들이 AuthenticationManager 에 등록되고 AuthenticationProvider 들이 인증을 처리한다.
ProviderManager
ProviderManager 는 AuthenticationManager 를 구현한다.
ProviderManager 에서는 등록된 AuthenticationProvider 들을 순회하며 처리한다.
AuthenticationProvider
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
AuthenticationProvider는 AuthenticationManager와 비슷하지만 호출자가 지정된 인증 유형을 지원하는지 여부를 쿼리할 수 있는 추가 메서드 supports 가 있다.
AuthenticationProvider 를 구현하는 클래스를 작성해서 ProviderManager 에 등록하면 ProviderManager 가 등록된 모든 AuthenticationProvider 를 기반으로 인증을 처리한다.
AuthenticationProvider 는 credential 들을 확인하고 인증이 된다면 Authentication 객체를 리턴한다.
UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 를 구현하는 클래스는 아래처럼 구현한다
/**
* SecurityConfig 에 설정한 loginProcessingUrl 에 요청이 오면
* 자동으로 UserDetailsService 빈의 loadUserByUsername() 실행 됨
*/
@Service
public class PrincipalDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Autowired
public PrincipalDetailsService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/**
* security session <- Authentication <- UserDetails <- 사용자 정보
* @param username
* @return : UserDetails 타입을 반환하고 UserDetails 는 Authentication 내부에 보관된다.
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<Member> byUsername = memberRepository.findByUsername(username);
Member member = byUsername.orElse(null);
if (member != null) {
return new PrincipalDetails(member);
} else {
return null;
}
}
}
오버라이드한 loadUserByUsername() 에서는 DB 에서 User 객체를 username 기반으로 가져오고 존재한다면 UserDetails 타입으로 반환한다.
UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
user 의 정보를 담는 인터페이스다.
UserDetails 를 구현한 클래스는 아래처럼 구현한다
/**
* security 가 SecurityConfig 에 설정한 loginProcessingUrl 에 요청이 오면 필터로 낚아채서 로그인 진행시켜줌
* 로그인 진행 완료되면 security session 을 만들어 Security ContextHolder 에 보관한다
* 그런데 보관되는 오브젝트의 타입이 Authentication 타입이다
* 그리고 Authentication 내부에 User 의 정보가 보관된다
* User 의 타입은 UserDetails 타입으로 저장된다
*/
public class PrincipalDetails implements UserDetails {
private Member member;
public PrincipalDetails(Member member) {
this.member = member;
}
// 해당 User 의 권한을 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getRole();
}
});
return collect;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
// 계정 만료 여부
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정 잠금 여부
@Override
public boolean isAccountNonLocked() {
return true;
}
// 계정 비밀번호 만료 여부
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 활성화 여부
@Override
public boolean isEnabled() {
return true;
}
}
참고
https://spring.io/guides/topicals/spring-security-architecture/
https://mangkyu.tistory.com/76