[첫번째 프로젝트] 각종구현체 - UserDetailsService (Authentication)

노력을 즐겼던 사람·2020년 4월 2일
4

참고문서

https://docs.spring.io/spring-security/site/docs/5.1.10.BUILD-SNAPSHOT/reference/htmlsingle/#jc-authentication

Authentication

SpringSecurity가 제공하는 basic form login 말고도 Authentication에는 몇가지 방법이 더 있다.

  • In-Memory Authentication
  • JDBC Authentication
  • LDAP Authentication
  • AuthenticationProvider
  • UserDetailsService

UserDetailsService

UserDetailsServiceoverride함으로 Authentication을 custom할 수 있다.

SecurityContextHolder, SecurityContext, Authentication

문서를 읽어보아도 내가 근본적으로 가진 의문이 풀리지 않았다.

로그인한 유저의 정보를 가져오는 가장 좋은 방법은 무엇일까?

UserDetails user = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
내가 사용한 방법은 그래도 봐줄만한 방법인가?이다. 그래서 내가 사용한 SecurityContextHolder에 대해 알아보기로 했다.

SecurityContextHolder란?

SecurityContextHolder는 가장 기본이 되는 Object라고 한다. 여기에는 우리의 Spring Application에 정의한 UserDetails를 저장한다. 그리고 이 UserDetails는 현재 사용중인, 그러니까 지금 우리의 서버에 접속한 사람의 pricipal을 포함한 UserDetails가 저장된다.
기본적으로 SecurityContextHolderUserDetails를 저장할 때 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()을 통해 가져온 principalUserDetails로 cast해버린다. UserDetailsSpringSecurity의 core interface이다. UserDetails를 adapter 정도로 생각하자. Spring SecuritySecurityContextHolder와 우리가 정의한 데이터베이스에게 필요한 어댑터. (실제로 이번 프로젝트에서 UserDetailsSecurityContextHolder에도 저장되고 데이터베이스에서 유저의 정보를 사용하여 조회할 때 사용되었다.)
그러면 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하는 UserDetailsSecurityContextHolder에 저장한다.

GrantedAuthority

loadUserByUsername의 return값을 보면 authorities에 해당하는 부분이 있다. 이곳이 권한을 부여하는 부분이다.

profile
노력하는 자는 즐기는 자를 이길 수 없다 를 알면서도 게으름에 지는 중

0개의 댓글