티스토리 뷰

UserDetails, UserDetailsService를 왜 사용해야하는 것일까?

먼저 스프링 시큐리티의 동작을 이해해보자.

  1. 시큐리티는 "~/login" 주소로 요청이 오면 가로채서 로그인을 진행한다.
  2. 로그인 진행이 완료되면 시큐리티_session 만들고 SecurityContextHolder에 저장한다.
    ( SecurityContextHolder = 시큐리티 인메모리 세션 저장소 )
  3. 시큐리티가 갖고있는 시큐리티_session 들어갈 있는 Object 정해져있다.
    ( Object == Athentication 타입 객체 )
  4. Authentication 안에 User 정보가 있어야 한다.
  5. User 객체 타입은 UserDetails 타입 객체 이다.

정리해보면

  • Security Session에 객체를 저장해준다.
  • -> 세션에 저장될 수 있는 객체는 Authentication 타입이다.
  • -> Authentication 객체는 User 객체를 저장한다.
  • -> User 객체는 UserDetails 타입이다.
  • => 따라서 우리 서비스는 UserDetails 를 상속받는 User를 만들어야 한다.

UserDetails를 상속받는 User

Security Session => Authentication => UserDetails 관계를 갖는 것이므로 차례로 접근해서 꺼내면 된다.

시큐리티_Session에 접근해서 Authentication을 꺼내고 거기서 UserDetails 타입 User를 꺼내면 우리가 원하는 유저 객체를 꺼낼 수 있게 되는 것이다.

 

정리

  1. 스프링 시큐리티는 오는 모든 접근 주체에 대해 Authentication를 생성한다.
  2. AuthenticationSecurityContext에 저장된다.
  3. SecurityContextSecurityContextHolder가 관리한다.
  4. 따라서, 시큐리티 세션들을 SecurityContextHolder 메모리 저장소에 저장하고, 꺼내서 사용하게 되는 것이다.

Authentication / SecurityContext / SecurityContextHolder에 대해 더 자세히 알아보자.


Authentication (인증)

접근자의 존재를 증명하는 것

Authentication authentication = 
	SecurityContexHolder.getContext().getAuthentication();
  • 이런식으로 Authentication 객체를 SecurityContextHolder 저장소에서 꺼낼 수 있다.

Authentication 내부 구조

  1. principal: 사용자 아이디 혹은 User객체를 저장
  2. credentials: 사용자 비밀번호
  3. authorities: 인증된 사용자의 권한 목록
  4. details: 인증 부가 정보
  5. Authenticated: 인증 여부(Boolean)

요청 Flow

출처 : https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

참고로 필자는 JWT를 사용할 것이므로 SecurityConfig에서 LoginForm()을 막고,

UsernamePasswordAuthenticationFilter를 상속받는

JwtAuthenticationFilter를 생성했다. 해당 필터를 스프링_시큐리티_필터에 추가해서 Authentication 을 검증하도록 구현했다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(new MyFilter4(), BasicAuthenticationFilter.class);
    http.csrf().disable();
    http
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)    // 세션 사용 안함
            .and()
            .addFilter(corsFilter) // 인증(O), security Filter 에 등록 / @CrossOrigin (인증X)
            .formLogin().disable() // Form login 안함
            .httpBasic().disable()
            .addFilter(new JwtAuthenticationFilter(authenticationManager())) // 차단한 formLogin 대신 필터를 넣어준다. AuthenticationManager 가 필요
            .authorizeRequests()
            .antMatchers("/v1/api/member/**")
            .access("hasRole('USER') or hasRole('ADMIN')")
            .antMatchers("/v1/api/admin/**")
            .access("hasRole('ADMIN')")
            .anyRequest() // 모든 요청에 대해서 허용하라.
            .permitAll();
}
  1. 외부에서 로그인을 위해 [username, password]를 가지고 요청
  2. UsernamePasswordAuthenticationFilter에서AuthenticationManager에 의해서 attemptAuthentication() 메소드를 통해 인증처리를 수행한다.
  3. 인증 성공 후 Authentication 객체를 생성 후 principal, credentials, authorities, details, authenticated 값들을 채워넣는다.
  4. SecurityContextHolder 내부의 SecurityContext 안에 저장한다.

참고로 SecurityContextHolder는 전략에 따라 3가지 형태로 존재하는데 기본 전략은 ThreadLocal로 요청 시 할당받은 스레드 내에 저장된다. 따라서 각기 다른 요청들에 대해 스레드 별로 각각의 SecurityContextHolder를 가지고 처리하므로 개별적으로 인증 처리가 가능하다.

SecurityContext

  • Authenticaion 객체가 직접 저장되는 저장소로 필요 시 언제든지 Authenticaion 객체를 꺼내서 사용할 수 있도록 제공되는 클래스이다.
  • TheadLocal에 저장되어서 같은 스레드라면 전역적으로 어디서든 접근이 가능하도록 설계되어있다.
    (단, SecurityContextHolder Strategy에 따라 다를 수 있다)

SecurityContextHolder

Authenticaion을 저장하는 SecurityContext를 관리하는 객체이다.

SecurityContextHolder는 전략에 따라 SecurityContext를 갖는 범위가 다르다.

  • MODE_THREADLOCAL
    • (기본값) 요청을 처리하는 하나의 스레드에서만 SecurityContext를 공유한다.
    • 스레드당 SecurityContext 객체를 할당하게 된다.
    • 요청이 끝나면 스레드에서 지워줘야 한다.
    • 비동기-멀티스레드 처리시 다른 스레드에는 인증정보가 없을 수 있으니 주의해야한다.
  • MODE_INHERITABLETHREADLOCAL
    • 메인 스레드와 자식 스레드에 관하여 동일한 SecurityContext를 유지한다.
    • 따라서, Parent, Child 스레드는 동일한 SecurityContext를 갖는다.
  • MODE_GLOBAL
    • 응용 프로그램에서 단 하나의 SecurityContext를 저장한다.

실제 내부 코드가 아래와 같이 구현되어있다.

if (strategyName.equals(MODE_THREADLOCAL)) {
   strategy = new ThreadLocalSecurityContextHolderStrategy();
   return;
}
if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
   strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
   return;
}
if (strategyName.equals(MODE_GLOBAL)) {
   strategy = new GlobalSecurityContextHolderStrategy();
   return;
}

왜 UserDetails, UserDetailsService를 상속받는 클래스를 만들어야하는지 필요성에 대해 정리해봤다. 이제 해당 클래스를 생성해보자.

UserDetails를 상속받는 PrincipalDetais 클래스 생성

@Data
public class PrincipalDetails implements UserDetails {

	private final Member member;

	public PrincipalDetails(Member member) {
		this.member = member;
	}


	// 해당 유저의 권한을 리턴한다.
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		Collection<GrantedAuthority> authorities = new ArrayList<>();
		this.member.getRoleList().forEach(R -> {
			authorities.add(() -> R);
		});
		return authorities;
	}

	@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;
	}
}
  • Member는 필자 서비스에서 정의한 회원 엔티티이다. 각자 자신의 회원 엔티티를 맞게 넣어주면 되겠다.
  • getAuthorities()는 USER_ROLE, ADMIN_ROLE, ... 같은 권한 리스트를 가져오게된다.

AuthenticationManager.class가 authenticate()를 통해 호출하는 loadUserByUsername() 메소드를 갖고있는 클래스는 UserDetailsService 클래스이다. 이를 상속받는 PrincipalDetailsService 클래스를 생성한다.

UserDetailsService를 상속받는 PrincipalDetailsService 클래스 생성

@Slf4j
@Service
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {

	private final MemberRepository memberRepository;

	// 시큐리티 session -> Authentication -> UserDetails
	// 시큐리티 세션(내부 Authentication(내부 UserDetails(PrincipalDetails)))
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("PrincipalDetailService.loadUserByUsername");
		log.info("LOGIN");
		Member member = memberRepository.findByUsername(username)
				.orElseThrow(() -> new BussinessException(ExMessage.MEMBER_ERROR_NOT_FOUND));

		return new PrincipalDetails(member);
	}
}
  • UsernamePasswordAuthenticationFilter 클래스에서 인증을 위해 위 메서드를 호출하게 된다.

이렇게 필요한 객체 및 클래스를 생성했다. 또한 인증 과정에서 객체 타입이 어떠한 관계들로 되어있는지 정리가 되었으니 실제 로그인 요청을 받아서 Authentication을 발급받고 인증을 하고 JWT토큰까지 발급받아 보자.

해당 부분은 다음 포스팅에서 이어서 정리하겠다.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday