Spring

[Security] 현재 로그인한 사용자 정보 가져오기

voider 2021. 1. 23. 15:26

문제

회원가입 시 인증 메일을 전송하고, 인증 키를 데이터베이스에 보관한다.

인증 API는 URL은 /member/auth/{key}다. {key}와 회원 DB에 저장된 key를 비교하는 절차를 거쳐야 한다.

회원이 인증 API에 요청했을 때 서버는 어떤 회원의 요청인지 알아야 한다.

알아본 방법은 총 세 가지다.

첫 번째 방법 Principal

보다시피 java.security가 제공하는 Pincipal은 로그인한 회원의 아이디(getName())정도만 받아올 수 있다. 회원 아이디로 데이터베이스에서 회원 정보를 불러와서 회원이 가진 인증 키를 사용할 수 있다. 제법 쓸만해 보이지만 한 번 더 셀렉트 해야 한다는 단점을 가지고 있다.

두 번째 방법 @AuthenticationPrincipal

이 애노테이션을 사용하면 UserDetailsService가 반환하는 User객체를 인수로 받아서 사용할 수 있다고 한다. @AuthenticationPrincipal은 파라미터 레벨에서 사용한다.

Controller

    @GetMapping("/member/auth/{key}")
    public void authenticationKeyCheck(@PathVariable String key, @AuthenticationPrincipal User user) {
        log.info("user : " + user);
    }

위와 같은 핸들러를 만들고 값이 어떻게 들어오는 지 확인해봤다.

이로써 시큐리티에서 제공하는 UserDetails의 구현체 User객체를 받을 수 있다. 하지만 여전히 내가 원하는 회원 객체의 인증 키를 참조하기 위해서는 한 번 더 데이터베이스에 질의해야 한다. 그렇다면 Principal이나 @Authentication이나 임마가 점마고 점마가 임마다.

세 번째 방법 Adapter Class

이제 정말로 @AuthenticationPrincipal을 그대로 이용하면서 회원 엔티티를 사용할 수 있다. 위에서 @AuthenticationPrincipal UserDetailsService가 반환하는 User객체를 인수로 받을 수 있다고 말했다. 따라서 UserDetailsService 즉, loadByUsername()메소드가 반환하는 객체를 변경하면, 내가 원하는 객체를 인수로 받을 수 있다.

UserDetailsService.java

 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

loadByUsername()의 선언부다. UserDetails를 반환해야 하기 때문에 UserDetails를 직접 구현할 수도 있지만 구현체인 org.springframework.security.core.userdetails.User를 상속하는 게 더 편한 방법인 것 같다.

 

User.java

    public User(String username, String password,
            Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }

시큐리티가 제공하는 User클래스의 생성자다. 아이디, 비밀번호, 권한을 인수로 받는다. 내가 정의한 Member엔티티가 User를 상속하도록 만들 수도 있겠지만, 도메인 객체는 특정 기술에 종속되지 않는 것이 베스트 프랙티스라고 한다.

그래서 회원 도메인 객체를 UserDetails를 잇는 일종의 어댑터 클래스를 MemberAdapter클래스를 만든다.

MemberAdapter

@Getter
public class MemberAdapter extends User implements Serializable {
    private Member member;

    public MemberAdapter(Member member) {
        super(member.getEmail(), member.getPassword(), List.of(new SimpleGrantedAuthority(member.getRoleKey())));
        this.member = member;
    }
}

이제 loadByUser()에서 MemberAdapter를 반환하면 @AuthenticationPrincipal애노테이션으로 회원 도메인 객체(Member)를 참조할 수 있다.

UserDetailsServiceImpl

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member loginMember =
                memberRepository.findByEmail(email).orElseThrow(()->new UsernameNotFoundException("존재하지 않는 계정입니다."));

        return new MemberAdapter(loginMember);
    }

Test

    @GetMapping("/member/auth/{key}")
    public void authenticationKeyCheck(@PathVariable String key, @AuthenticationPrincipal MemberAdapter memberAdapter) {
        log.info("user : " + memberAdapter);
        log.info("key : " + memberAdapter.getMember().getAuthenticationKey());
    }

결과

2021-01-23 14:53:36.260  INFO 21070 --- [nio-8081-exec-5] com.dunk.django.member.MemberController  : user : com.dunk.django.member.MemberAccount@633c7471: Username: o@o.com; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER
2021-01-23 14:53:36.261  INFO 21070 --- [nio-8081-exec-5] com.dunk.django.member.MemberController  : key : c39c4c5f-c687-4bdf-8267-94104b08f89b

원했던 결과인 회원 도메인 객체를 참조하여 인증 키 정보를 가져올 수 있다.

 

참고 : ncucu.me/137