Spring Security 로그인 후 사용자 정보얻기

DragonTiger·2021년 12월 13일
8

Spring Security 로그인 후 사용자 정보를 어떻게 가져올까?
Spring Boot와 Spring Security를 배우는 입장으로 장님 상태로 코드를 짜려니 막막해서 여기저기 기웃거려 보았다.찾아보니 3가지 방법이있다고 한다.

  1. Bean을 통해 사용자 정보를 가져오는 방법
  2. Controller 에서 사용자 정보를 얻는다.
  3. @AuthenticationPrincipal 을 사용한다. // 이방법을 주로 권장한다고 한다.

1. Bean을 통해 사용자 정보를 가져오는 방법.

SecurityContextHolder를 통해 가져오는 방법이다.

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 
UserDetails userDetails = (UserDetails)principal; 
String username = principal.getUsername(); 
String password = principal.getPassword();

2. Controller 에서 사용자 정보를 얻는다.

Principal 객체에 접근해 정보를 가져온다.

 @Controller 
public class SecurityController { 
    @GetMapping("/username") 
    @ResponseBody 
    public String currentUserName(Principal principal) { 
        return principal.getName(); 
    } 
}

본인의 경우는 Controller에서 Authentication 객체를 활용해서 getName()로 사용자 정보를 찾아서 서비스단에 넘겨서 DB에서 정보를 찾아서 인서트를 했었다.

	//Controller 클래스	
        @PostMapping("/form")
        public String formSubmit(@Validated @ModelAttribute BoardForm boardForm, BindingResult bindingResult,Authentication authentication){
		
        //...
        
        String memberEmail = authentication.getName();
        boardService.save(boardForm, memberEmail);
        
        //...
        //BoardService 클래스
        public Board save(BoardForm boardForm, String memberEmail) {

        Optional<Member> member = memberRepository.findByEmail(memberEmail);

        return boardRepository.save(Board.builder()
        		//...
                
                .member(member.get()).build());
    	}

3. @AuthenticationPrincipal 을 사용한다.

Spring Security 3.2 부터는 annotation을 이용하여 현재 로그인한 사용자 객체를 인자에 주입할 수 있습니다.

@AuthenticationPrincipal
로그인한 사용자의 정보를 파라미터로 받고 싶을때 기존에는 2번과 같이 Principal 객체로 받아서 사용한다.

@Controller 
public class SecurityController { 
    @GetMapping("/username") 
    @ResponseBody 
    public String currentUserName(Principal principal) { 
        return principal.getName(); 
    } 
}

하지만 이 객체는 SecurityContextHolder의 Principal과는 다른 객체이다.

이 객체는 JAVA 표준 Principal 객체이며 우리가 참조할수 있는 정보는 getName() 정보 밖에 없다.

@AuthenticationPrincipal 애노테이션을 사용하면 UserDetailsService에서 Return한 객체 를 파라메터로 직접 받아 사용할 수 있다.

하지만 이방법도 우리가 원하는 방법은 아니다.
현재 로그인한 사용자의 정보를 참조하고 싶을때 도메인의 User를 나타내는 객체 (Account)를 직접 사용하고 싶다.

Adapter 클래스

  • UserDetailsService에서 리턴하는 타입을 변경하면, Controller에서 @AuthenticationPrincipal로 받아올수 있는 객체가 변경된다.
  • 이때 사용할수 있는 방법은 두가지가 존재한다
  1. Account 객체를 직접 리턴하기
  2. Account 객체를 감싸는 Adapter 클래스 사용하기


Account객체를 직접 리턴하는 방법은 나쁜 방법은 아니지만, Account객체가 UserDetails를 구현해야한다.
도메인 객체는 특정 기술에 종속되지 않도록 개발하는것이 베스트 프렉티스이다.

Adapter 클래스를 사용하는 방법을 사용하도록 한다.

MemberAdapter

@Getter
public class MemberAdapter extends User {

    private Member member;


    public MemberAdapter(Member member) {
        super(member.getEmail(), member.getPassword(), List.of(new SimpleGrantedAuthority(member.getAuth())));

        this.member = member;
    }

}

MemberAdapter 클래스의 내부를 살펴보자

User클래스를 상속받는다.

MemberAdapter의 멤버는 오로지 Member 객체만이 존재한다.
생성자 내부에서 User 클래스의 생성자를 호출하여 username, password, role을 세팅한다.
User 클래스를 상속받는 이유 ?

UserDetailsService에서 Return하는 객체는 UserDetails 타입이여야 한다.
따라서 UserDetails를 구현하는 User 클래스를 상속 받는 방식으로 사용한다.

UserDetailsService 클래스를 보면 return타입이 UserDetails 인걸 확인 할 수있다.

또한 User 클래스 또한 UserDetails 상속 받고있다.

//UserDetailsService 클래스
public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
//User 클래스
public class User implements UserDetails, CredentialsContainer {
//...

변경된 MemberService (implements UserDetailsService) 는 다음과 같다.

@Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member member = memberRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException(email));

        return new MemberAdapter(member);
    }

MemberAdapter 사용하기

MemberService (implements UserDetailsService)에서 MemberAdapter 객체를 리턴하도록 변경하였으니 Controller에서도 변경되었는지 확인해보자.

우리가 기대한 대로 MemberAdapter 객체를 가져오며 내부에 Member 객체를 감싸고있어 로그인한 사용자에 대한 Member 객체를 참조하여 사용할 수 있다.

@GetMapping(//...)
    public String Test(@AuthenticationPrincipal MemberAdapter memberAdapter,//...){
    	//...
        log.info("사용자 정보 : " + memberAdapter.getMember());
        log.info("사용자 이메일 : " + memberAdapter.getMember().getEmail());
        //...
       
    }
    
 //result 
2021-12-14 01:49:24.133  INFO 6580 --- [nio-8017-exec-2] c.f.f.controller.DatanInfoController     : 사용자 정보 : Member(id=3, email=yoho9908@gmail.com, nickname=클래식, password=$2a$10$PurgZfsuZyHb3zl1E7kYQubIO1bYHX9dh74fz9r7kIz1Ata82YeLu, auth=ROLE_ADMIN, regdate=2021-11-19T15:28:22.248477)
2021-12-14 01:49:24.133  INFO 6580 --- [nio-8017-exec-2] c.f.f.controller.DatanInfoController     : 사용자 이메일 : yoho9908@gmail.com

하지만 이 방법은 한가지 문제가 있다.

Member 객체를 MemberAdapter객체가 감싸는 구조이기 때문에 Member객체를 직접 참조하여 사용하려면 getMember() 코드가 게속해서 중복된다.

위 문제를 해결하기위해선 Member 객체를 직접 사용하는 커스텀 어노테이션을 쓴다고 한다.

하지만 아직 난 초보이기때문에 코드의 흐름을 기억하기위해 일단은 MemberAdapter에서 getMember() 로 한다리 걸처서 쓰도록 하겠다.

참고한 사이트

profile
take the bull by the horns

0개의 댓글