본문 바로가기
스프링

[스프링 시큐리티] custom한 authentication받기

by 근즈리얼 2023. 1. 25.
728x90

오늘은 jwt 토큰을 이용해서 로그인/로그아웃을 구현하던 중 만난 어려움에 대해서 포스팅 해보려고 합니다.

 

어려움을 만날 때까지의 과정

- 구글링을 통해서 로그인 기능 구현

- 토큰 값 없이 API요청을 할 경우 401에러와 권한이 없다는 메세지 출력

 

이후 로그아웃 기능을 구현하기 위해 controller로 넘어온 데이터를 확인하던 중 내가 커스텀한 데이터로 Authentication이 왔으면 좋겠다는 생각이 들었습니다.

 

수 많은 구글링을 해본 결과

방법은 3가지가 있습니다.

 

1.  Authentication의 .getName()을 해서 데이터베이스에서 데이터를 조회하는 것입니다.

@GetMapping("test")
public ResponseEntity<String> test(
        Authentication authentication
) {
    String name = authentication.getName();
    return ResponseEntity.ok("test");
}

위의 코드를 사용하면 처음 Token의 넣어졌던 Username을 가져올 수 있습니다.

저는 Username에 제가 만든 Users엔티티의 이메일 값을 넣었습니다.

따라서, String name = authentication.getName()으로 Users의 이메일을 알 수 있고 그 값으로 데이터를 조회할 수 있습니다.

 

2. UserDetails를 상속 받는 CustomDetails를 만들고 @AuthenticationPrincipal을 이용하는 것입니다.

@GetMapping("test")
public ResponseEntity<String> test(
        @AuthenticationPrincipal CustomDetails customDetails
) {

    return ResponseEntity.ok("test");
}


------------------------------------------------------------


@Getter
@NoArgsConstructor
public class CustomDetails implements UserDetails {

private Users users;

public static CustomDetails of(Users users) {
    CustomDetails instance = new CustomDetails();
    instance.users = users;

    return instance;
}

위에가 @AuthenticationPrincipal을 이용한 controller 부분입니다.

밑에는 UserDetails를 상속받은 custom객체입니다.

 

저는 이 두번째 방법이면 충분히 만족스러웠고 두번째 방법으로 진행했습니다.

하지만 이 방법을 진행하면서 너무 많은 시행착오를 겪게 되었습니다... ㅠㅠ

 

천천히 알아보겠습니다.

    1. 우선 정말 말 그래도 상속받은 객체를 만들고 controller에 @AuthenticationPrincipal을 붙여서 사용했습니다.. 당연히      결과는 실패...

 

    2. loadUserByUsername의 return 값을 수정해봤습니다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UsersRepository usersRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return usersRepository.findByEmail(username)
                .map(CustomDetails::of)
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }
}

기존에는 userDetails을 반환하던 메소드를 내부 로직에서 CustomDetails를 리턴할 수 있도록 수정했습니다.

이젠 확실히 정답에 가까워진거 같은데... 이번에도 제가 원하는 결과에는 도달하지 못했습니다...

이후로 무한 디버깅... 진짜 별에 별거를 다 찍어봤는데요.,.. ㅠㅠ

 

디버깅을 찍던 중 Authentication을 리턴해주는 로직을 발견했습니다. 이 로직에서는 여전히 스프링 시큐리티가 제공하는 User를 파라미터로 넘겨주고 있었습니다.

저는 여기서 User를 만들어서 파라미터로 넘겨주는 대신

    3. loadUserByUsername 메소드를 호출해서 customDetails를 만들어 낸 뒤 파라미터로 넘겨줬습니다.

결과는 성공!!

public Authentication getAuthentication(String accessToken) {
        // 토큰 복호화
        Claims claims = parseClaims(accessToken);

        if(claims.get(AUTHORITIES_KEY) == null) {
            throw new RuntimeException("권한 정보가 없는 토근값입니다.");
        }

        // 클레임에서 권한 정보 가져오기
        Collection<? extends GrantedAuthority> authorities = Arrays.stream(
                        claims.get(AUTHORITIES_KEY).toString().split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        // 기존 코드
        //UserDetails principal = new User(claims.getSubject(), "", authorities);
        //return new UsernamePasswordAuthenticationToken(principal, "", authorities);

        //수정한 코드
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject());

        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
    }

정말 수많은 삽질 끝에

제가 원하는 커스텀 데이터가 찍히는 것을 확인할 수 있었습니다.

 

3. UserAdapter 방식입니다.

 

제가 직접 해보지 않아서 보여드릴 코드는 존재하지 않습니다만...

Users를 직접 받는 것이 아닌 어댑터 패턴을 이용해서 받는다고 합니다...

나중에 제가 직접 만들어보고 이어서 포스팅 해보겠습니다~

728x90

댓글