스프링 시큐리티의 인증
스프링 시큐리티의 인증 과정
스프링 시큐리티가 제공하는 사용자 인증 기능에 대해 알아본다. 먼저 간략하게 전체 구조를 훑어본다.
- SecurityContextHolder로부터 현재 인증된 Authentication 객체를 받을 수 있다.
- Authentication은 사용자의 정보를 담고 있는 객체다.
- SecurityContextHolder에 Authentication 객체가 들어가면 login, 제거하면 logout이다.
- SecurityContextHolder에 드가는 Authentication 객체는 인증된 사용자라고 생각하면 되지만, 인증의 과정을 거치지 않은 쌩 객체도 드갈 수는 있다.
- DB에 접근해서 사용자 인증에 필요한 정보를 받아온다.
- 가장 많이 사용하는 username/password 인증을 위해 DB에서 패스워드 정보를 얻어와야 한다.
- 그때 사용하는 서비스 객체가 UserDetailsService이다.
- UserDetailsService는 username을 통해 얻는 정보를 UserDetails라는 객체에 담아 전달한다.
- UserDetails는 내 서비스 내의 유저 객체와 SpringSecurity에서 사용하는 유저 정보 객체 사이의 어답터이다.
- AuthenticationManager를 통해 Authentication 객체를 검증한다.
- AuthenticationManager의 역할은 아직 인증되지 않은 Authentication 객체를 검증하고, 인증 여부를 판단하는 것이다.
- 객체를 검증하기 위해서 필요한 정보를 UserDetailsService로 부터 UserDetails의 형태로 받아온다.
- 물론 DB 정보가 필요할 때만이다. - 인증에 성공한다면, Authentication 객체는 SecurityContextHolder에 들어가게 된다.
- ProviderManager는 가장 많이 쓰이는 AuthenticationManager의 구현체이다.
- ProviderManager는 여러 개의 AuthenticationProvider를 갖고 있다.
- ProviderManager는 AuthenticationProvider의 리스트를 순차 탐색하면서, 인증하려는 Authentication에 맞는 프로바이더를 찾아서 걔로 인증 과정을 수행한다.
SecurityContextHolder
- SecurityContext로의 접근을 제공하는 헬퍼 객체다.
- SecurityContext의 SecurityContextHolderStrategy를 설정할 수 있다.
- MODE_THREADLOCAL
- 같은 쓰레드 내에서 SecurityContext의 정보를 유지한다.
- MODE_INHERITABLETHREADLOCAL
- 같은 쓰레드 + 하위 쓰레드까지도 SecurityContext의 정보를 유지한다.
- MODE_GLOBAL
- 같은 어플리케이션 내에서 계속 SecurityContext의 정보를 유지한다.
- MODE_THREADLOCAL
- SecurityContextHolderStrategy 설정하기
- JVM 옵션으로 어플리케이션 구동 시점에 spring.security.strategy 속성을 설정한다.
- 어플리케이션 자바 설정 파일로 설정한다.
- SecurityContextHolder.setStrategyName(strategyName);
ThreadLocal
- 자바에서 제공하는 쓰레드 범위의 변수
- 같은 쓰레드 내에서 같은 상태를 유지하는 변수의 사용이 가능하다.
- 1번 쓰레드에는 “babo”를 저장해두고,
- 2번 쓰레드에서 get() 해보면 null만 나온다.
- 2번 쓰레드에서 “babobabo”를 set()한다.
- 1번 쓰레드 내에서 get() 해보면 “babo”가 나온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MySecurityContextHolder {
private final ThreadLocal<MySecurityContext> context = new ThreadLocal<>();
public MySecurityContext getContext() {
return context.get();
}
public void setContext(MySecurityContext context) {
this.context.set(context);
}
public MySecurityContext createEmptyContext() {
return new MySecurityContext();
}
}
이런 식으로 ThreadLocal을 사용해서 SecurityContextHolder를 통해 SecurityContext에 접근할 수 있다.
SecurityContext
- SecurityContext는 SecurityContextHolder를 통해 얻을 수 있다.
- 얘한테 Authentication 객체를 담아 홀더에 넣는게 현재 인증된 사용자를 등록하는 방법이다.
인증 완료된 사용자 정보 저장
1
2
3
4
5
6
7
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
- SecurityContextHolder를 통해 빈 SecurityContext 객체를 생성한다.
- SecurityContextHolder.getContext()로 받아와서 사용하면 멀티 스레드 환경에서 충돌 문제가 발생할 수 있기 때문에 피해야 한다.
- 인증 과정이 완료된 Authentication을 SecurityContext에 set 해준다.
- 예제 코드에선 인증 과정 생략
- 인증된 Authentication 객체가 담긴 SecurityContext 객체를 SecurityContextHolder에 set 해준다.
- 이거 가끔씩 까먹어서 생쑈를 많이 했음
인증된 사용자 정보 조회
1
2
3
4
5
6
7
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
- SecurityContextHolder로부터 SecurityContext 객체를 받아온다.
- SecurityContext 객체로부터 Authentication 객체를 받아온다.
- Authentication 객체에 인증된 사용자의 정보가 담겨있다.
인증된 사용자가 한둘이 아닐텐데.. Map처럼 키를 주고 받아오는 것도 아니고, 어떻게 그냥 주세요! 해서 받아오나요
SecurityContextHolderStrategy의 디폴트 설정인 쓰레드 로컬을 사용해서 그렇다.
쓰레드 내에선 인증된 사용자가 하나밖에 없으니까 그냥 주세요! 하면 된다.
Authentication
- 사용자 인증 정보를 담는 객체이다.
- 얘는 두 가지 역할을 한다.
- 인증 하려는 유저의 정보를 담아 AuthenticationManager의 입력으로 사용된다.(isAuthenticated() = false)
- 인증된 사용자의 정보를 제공할 때 여기에 담아서 사용한다. (isAuthenticated() = true)
- Authentication은 세 가지 정보를 담고 있다.
- principal
- 인증 주체로 사용자의 식별자이다.
- 많이 쓰이는 username 같은 거.
- credential
- 인증 주체의 자격 증명이다.
- 많이 쓰이는 password 같은 거.
- 인증이 끝나면 보안 상의 이유로 얘는 지워진다.
- authorities
- 부여된 권한
- 서비스 접근 권한이 다른 사용자의 권한을 나타낸다.
- 어드민 유저랑 일반 유저의 구분 등
- principal
GrantedAuthority
- 사용자에 부여된 권한을 나타내는 객체이다.
AuthenticationManager
- SecurityFilter가 어떻게 사용자 인증을 수행할지 정의해둔 인증 객체이다.
- 보통 인증되지 않은 Authenticaion 객체를 받아서,
- 여기서 Authentication은 isAuthenticated() = false
- authorities는 비어있음.
- 정의된 방식으로 인증 과정을 거치고,
- 인증이 성공했다면, 인증 처리한 Authenticaion 객체를 필터에 반환한다.
- 여기서 반환하는 Authentication은 isAuthenticated() = true
- authorities를 채워서 줌.
- 인증이 끝났기 때문에 보안을 위해 credential은 지워버림
- 인증이 실패했다면,
- 인증 처리 되지 않은 Authentication 객체를 반환한다. isAuthenticated() = false
ProviderManager
- 대표적인 AuthenticationManager의 구현체이다.
- 여러 개의 AuthenticationProvider를 들고 있다.
- providers 리스트를 순차적을 돌면서 인증을 위해 받아온 적절한 provider를 찾아 인증 과정을 수행한다.
- AuthenticaionProvider의 supports(Class<?> auth) 메서드를 통해서 해당 provider가 요청으로 들어온 Authentication 객체를 인증을 수행할 수 있는지 확인한다.
- providers를 전부 돌았음에도 적절한 provider가 없었다면,
- ProviderNotFoundException을 날리며 인증 실패를 알린다.
UserDetailsService
- 스프링 시큐리티에서 유저 정보를 load하는 핵심 인터페이스이다.
- 스프링 시큐리티 전체에서 유저의 dao로 사용된다.
- 메소드는 UserDetails loadUserByUsername(String username) 하나 있다.
- dao로부터 유저 정보를 가져오고, UserDetails 객체로 변환하여 리턴하는 역할.
- DaoAuthenticationProvider가 인증을 수행할 때 사용하는 기본 전략이다.
UserDetails
- 유저 정보를 담아서 제공하는 객체이다.
- 인증 같은 보안 로직에서 사용되지는 않는다.
- 목적은 그저 사용자의 유저 정보 객체와 시큐리티의 유저 정보 객체의 어답터 역할이다.
여기까지 다 읽은 뒤에 맨위의 그림을 보면 좀 더 이해가 쉽지 않을까.
This post is licensed under CC BY 4.0 by the author.