티스토리 뷰
Springboot JWT 로그인 - (3) UserDetails, UserDetailsService 이해
구름뭉치 2022. 6. 13. 15:50UserDetails, UserDetailsService를 왜 사용해야하는 것일까?
먼저 스프링 시큐리티의 동작을 이해해보자.
- 시큐리티는 "~/login" 주소로 요청이 오면 가로채서 로그인을 진행한다.
- 로그인 진행이 완료되면 시큐리티_session을 만들고 SecurityContextHolder에 저장한다.
( SecurityContextHolder = 시큐리티 인메모리 세션 저장소 ) - 시큐리티가 갖고있는 시큐리티_session에 들어갈 수 있는 Object는 정해져있다.
( Object == Athentication 타입 객체 ) - Authentication 안에 User 정보가 있어야 한다.
- User 객체 타입은 UserDetails 타입 객체 이다.
정리해보면
- Security Session에 객체를 저장해준다.
- -> 세션에 저장될 수 있는 객체는 Authentication 타입이다.
- -> Authentication 객체는 User 객체를 저장한다.
- -> User 객체는 UserDetails 타입이다.
- => 따라서 우리 서비스는 UserDetails 를 상속받는 User를 만들어야 한다.
Security Session => Authentication => UserDetails 관계를 갖는 것이므로 차례로 접근해서 꺼내면 된다.
시큐리티_Session에 접근해서 Authentication을 꺼내고 거기서 UserDetails 타입 User를 꺼내면 우리가 원하는 유저 객체를 꺼낼 수 있게 되는 것이다.
정리
- 스프링 시큐리티는 오는 모든 접근 주체에 대해 Authentication를 생성한다.
- Authentication은 SecurityContext에 저장된다.
- SecurityContext는 SecurityContextHolder가 관리한다.
- 따라서, 시큐리티 세션들을 SecurityContextHolder 메모리 저장소에 저장하고, 꺼내서 사용하게 되는 것이다.
Authentication / SecurityContext / SecurityContextHolder에 대해 더 자세히 알아보자.
Authentication (인증)
접근자의 존재를 증명하는 것
Authentication authentication =
SecurityContexHolder.getContext().getAuthentication();
- 이런식으로 Authentication 객체를 SecurityContextHolder 저장소에서 꺼낼 수 있다.
Authentication 내부 구조
- principal: 사용자 아이디 혹은 User객체를 저장
- credentials: 사용자 비밀번호
- authorities: 인증된 사용자의 권한 목록
- details: 인증 부가 정보
- Authenticated: 인증 여부(Boolean)
요청 Flow
참고로 필자는 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();
}
- 외부에서 로그인을 위해 [username, password]를 가지고 요청
- UsernamePasswordAuthenticationFilter에서AuthenticationManager에 의해서 attemptAuthentication() 메소드를 통해 인증처리를 수행한다.
- 인증 성공 후 Authentication 객체를 생성 후 principal, credentials, authorities, details, authenticated 값들을 채워넣는다.
- 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토큰까지 발급받아 보자.
해당 부분은 다음 포스팅에서 이어서 정리하겠다.
'프로젝트 > JWT 방식 인증&인가 시리즈' 카테고리의 다른 글
Springboot JWT 로그인 - (6) RefreshToken 방식을 사용하도록 변경 & 코드 정리 (5) | 2022.06.24 |
---|---|
Springboot JWT 로그인 - (5) 클라이언트 요청 시 BasicAuthenticationFilter를 이용한 JWT 검증 (6) | 2022.06.20 |
Springboot JWT 로그인 - (4) UsernamePasswordAuthenticationFilter를 통한 로그인 로직 구현 (0) | 2022.06.16 |
Springboot JWT 로그인 - (2) Filter 적용 테스트 (0) | 2022.06.09 |
Springboot JWT 로그인 - (1) Filter에 대한 이해 (0) | 2022.06.06 |
- Total
- Today
- Yesterday