SpringSecurity
가 제공하는 basic form login
말고도 Authentication에는 몇가지 방법이 더 있다.
UserDetailsService
를 override
함으로 Authentication을 custom할 수 있다.
문서를 읽어보아도 내가 근본적으로 가진 의문이 풀리지 않았다.
로그인한 유저의 정보를 가져오는 가장 좋은 방법은 무엇일까?
UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
내가 사용한 방법은 그래도 봐줄만한 방법인가?이다. 그래서 내가 사용한 SecurityContextHolder
에 대해 알아보기로 했다.
SecurityContextHolder란?
SecurityContextHolder
는 가장 기본이 되는 Object라고 한다. 여기에는 우리의 Spring Application에 정의한 UserDetails
를 저장한다. 그리고 이 UserDetails
는 현재 사용중인, 그러니까 지금 우리의 서버에 접속한 사람의 pricipal
을 포함한 UserDetails
가 저장된다.
기본적으로 SecurityContextHolder
는 UserDetails
를 저장할 때 ThreadLocal
을 사용한다. 이것이 의미하는 것은 같은 Thread에서 실행되고 있다면 언제든지 SecurityContextHolder
의 method에 접근이 가능하다는 말이다. SecurityContextHolder
와 함께 ThreadLocal
을 사용하는 방법은 quite safe
하니까 걱정하지 말라고 한다. Spring Security
가 자동으로 제공해줄꺼니까 걱정하지 말자.
그렇지만 Thread와 함께 동작하는 Application들은 ThreadLocal
을 사용하는 것이 적합하지 않을 수 있다. 예를들어 Swing client
는 동일한 security context를 사용하기 위해서 JVM의 모든 Thread를 사용한다.
SecurityContextHolder
는 최초 실행시에 어떻게 저장될 것인지에 대한 전략을 설정할 수 있다. 만약 standalone application으로 사용하려면 SecurityContextHolder.MODE_GLOBAL
전략을 사용하자. 다른 application들은 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
을 사용하다. 참고로 default는 SecurityContextHolder.MODE_THREADLOCAL
이다. 그리고 이것을 두 가지 방법으로 바꿀 수 있다.
하나는 system property를 변경하는 것이고 두번째는 static method SecurityContextHolder
를 호출하는 것이다. 대부분의 application은 default에서 변경할 필요는 없다. 그래도 바꿔야한다면 JavaDoc for SecurityContextHolder
를 참고하자.
Obtaining information about the current user
아까 언급한 것 처럼 SecurityContextHolder
내부에는 현재 상호작용 중인(currently interacting) 녀석의 details가 저장되어 있다. Spring Security
는 이 녀석이 제공해주는 정보를 활용하여 Authentication
을 진행한다. 지금 접속한 녀석의 userName
을 가져와보자
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
이건 공식 문서에서 제공하는 코드이다. 내가 작성한 코드와 비교해보자.
if (!SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal().equals("anonymousUser")) {
UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
} else {
String user = "anonymousUser";
}
척 보기에도 더러운 코드이다. 코드가 이렇게 된 이유는 이렇다.
로그인 하지 않은 사용자의 SecurityContextHolder.getContext().getAuthentication().getPrincipal()
return값은 자료형이 String
value가 anonymousUser
이다.
반면에 로그인한 사용자는 UserDetails
자료형의 어떠한 값이다. 그렇기 때문에 무턱대고 UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
와 같은 코드를 남발하면 에러를 발생시키며 서버가 다운된다. 그에 대한 임시 방편으로 if문을 사용했다.
지금 되돌아보니 나의 코드는 아주 비효율적이다. 왜냐하면 사용자가 로그인을 했던지 안했던지 SecurityContextHolder.~~.getPrincipal()
을 두번이나 해야하기 때문이다. 앞으로는 위의 코드를 사용하자. 그리고 위의 코드는 application의 어디서나 사용할 수 있다고 한다.
UserDetailsService
위의 코드를 보면 getPrincipal()
을 통해 가져온 principal
을 UserDetails
로 cast해버린다. UserDetails
는 SpringSecurity
의 core interface이다. UserDetails
를 adapter 정도로 생각하자. Spring Security
의 SecurityContextHolder
와 우리가 정의한 데이터베이스에게 필요한 어댑터. (실제로 이번 프로젝트에서 UserDetails
는 SecurityContextHolder
에도 저장되고 데이터베이스에서 유저의 정보를 사용하여 조회할 때 사용되었다.)
그러면 UserDetails
는 어떻게 사용할까? UserDetailsService
라는 interface를 implements하면된다. 이 친구는 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
의 형태를 가진 method만 가지고 있다. 이 친구의 역할은 Authentication이다. 기본적으로는 username
을 통해 user를 load하지만 나의 이번 프로젝트에서는 email
을 통해 load를 해야 했다. 그래서 아래와 같이 Override해서 사용했다.
@Override
@Transactional
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Optional<Member> memberEntityWrapper = loadByEmail(email);
Member memberEntity = memberEntityWrapper.get();
return new com.hnu.pioneer.domain.UserDetails(memberEntity.getEmail(), memberEntity.getPassword(), authorities(memberEntity),
memberEntity.getName(), memberEntity.getStudentNumber());
}
매개변수가 email로 바뀌었다. 매개변수 email은 Form Login을 통해 입력된 email이 자동으로 들어온다. loadByEmail()
은 직접 작성한 method로서 일반적인 Repository
에서 findByEmail()
을 수행한다. 이러한 일련의 과정을 거쳐서 Authentication에 성공하면 loadUserByUsername
이 return하는 UserDetails
를 SecurityContextHolder
에 저장한다.
GrantedAuthority
loadUserByUsername
의 return값을 보면 authorities
에 해당하는 부분이 있다. 이곳이 권한을 부여하는 부분이다.