Web/Spring Security

Spring Security 구조

tose33 2023. 12. 11. 20:46

 

 

 

 

아래는 Authentication (인증) 요청부터 Spring security 가 어떤 흐름으로 처리하는지 정리한 것이다.

위 그림의 번호와는 상관없다.

 

  1. 사용자가 인증을 요청한다 
    • SecurityConfig 설정 클래스에서 설정한 인증이 필요한 경로에 요청이 왔을때 포함
  2. 스프링 시큐리티가 요청을 intercept 해서 사용자가 인증됐는지 확인한다. 인증이 안됐다면 로그인 페이지로 리다이렉트 시킨다.
  3. 유저는 Credential (설정에 따라, 대부분의 경우 username, password) 을 입력한다
  4. AuthenticaionFilter (credential 이 username,password 라면 UsernamePasswordAuthenticationFilter) 가 입력한 credential 을 확인하고 credential 이 담겨있는 UsernamePasswordAuthenticationToken 을 생성한다. 
  5. UsernamePasswordAuthenticationToken  AuthenticationManager 을 구현하는 ProviderManager 로 전달되고, ProviderManager 는 하나 혹은 그 이상의 AuthenticationProvider 에게 인증 과정을 처리하도록 명령한다 
  6. AuthenticationProvider 는 인증을 처리하는데 UserDetailsService를 사용해 사용자의 정보를 가져와서 인증을 처리한다.
    • AuthenticationProvider 는 인증을 성공하면 Authentication 객체를 리턴한다.
    • Authentication 객체에는 사용자의 인증정보와 authorities (roles) 가 담겨있다.
  7. UserDetailsService 인터페이스는 AuthenticationProvider 가 인증 처리를 위해 사용한다.
  8. UserDetails 는 유저 정보가 담겨있다.
    • UserDetailsService  내부에 loadByUsername() 메소드가 UserDetails 를 리턴하도록 구현한다.
  9. 인증이 완료되면 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