스프링 시큐리티 - @AuthenticationPrincipal 와 Spel

이진우·2024년 4월 30일
0

스프링 학습

목록 보기
31/41

기존에 사용했던 방식

private Member getMember(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return memberRepository.findMemberByLoginId(authentication.getName()).orElseThrow(()->new NotFoundException(
                ErrorCode.MESSAGE_NOT_FOUND));
    }

위 코드처럼 직접 SecurityContextHolder 에서 인증객체를 직접 꺼내는 방식을 컨트롤러 단이나 서비스 단에서 수행했었다.

하지만 다른 방법도 시도해보자 (장단점이 있을까?)

대안

아래와 같이 @AuthenticatinoPrincipal 을 이용해서 이를 해결할 수 있다.

 public ResponseEntity<Void> savePost(@PathVariable("communityId") final Long communityId,@ModelAttribute @Valid final PostSaveRequestDto postSaveRequestDto,
@AuthenticationPrincipal final UserDetails userDetails){
        postService.savePost(postSaveRequestDto,communityId,userDetails.getUsername());
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

대안에 대한 문제점 발생 가능성

하지만 위 방법에서 만약 로그인된 사용자, 로그인되지 않은 사용자가 동시에 사용하기 때문에
인가 처리를 하지 않은 경우 혹은 로그인된 사용자만 사용하지만 인가처리를 실수로 못한 경우 등(Filter에서 거르지 못했을 때)
에는 UserDetailsNull 값이 들어갈 수 있다.

그리고 그 Null 값이 생긴다면 각 서비스 코드에서 Null 에 대한 처리를 해줘야 하는 번거로움이 생길 수 있다고 한다. 실제로 얼마나 번거로울지는 모르겠지만

그 원인

위 내용의 상황 같은 경우 Authentication 객체에는 Null 이 들어가지 않고 Authentication.getName() ,getPrincipal()
에는 anonymousUser 라는 값이 들어가있다.

따라서 @AuthenticationPrincipal 사용시 AuthenticationPrincipalArgumentResolver
resolveArgument 메서드의 아래 그림의 파란색 부분에서 null 이 반환되는 것이다.

해결책: 직접 resolveArgument 를 구현해서 이를 해결하기

위에 대한 문제 해결을 위해

https://wildeveloperetrain.tistory.com/324

https://velog.io/@hann1233/AuthenticationPrincipal-%EC%A0%81%EC%9A%A9

여러 블로그에서 따로 resolveArgument 를 구현한 것을 볼 수 있었다.

일단 중요한 것은 NPE 방지 이므로 reture NULL 대신 예외를 던지는 식으로 처리를 해준 것을 볼 수 있다.

해결책: SpEL

@AuthenticationPrincipal 을 상속한 커스텀 어노테이션을 만들어서 반환값이 UserDetails 대신 loginId를 반환하게 하겠다.

이 과정에서 userDetails가 NULL 이라면 loginId 를 null 로 세팅하여서 객체가 null 이 아니도록 할 수 있다.

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : #this.getUsername()")
public @interface CurrentMemberLoginId {

}
public ResponseEntity<Void> savePost(@PathVariable("communityId") final Long communityId,@ModelAttribute @Valid final PostSaveRequestDto postSaveRequestDto,
            @CurrentMemberLoginId final String loginId){
        postService.savePost(postSaveRequestDto,communityId,getMemberByLoginId(loginId));
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
private Member getMemberByLoginId(String loginId){
        return memberRepository.findMemberByLoginId(loginId).orElseThrow(()->new NotFoundException(
                ErrorCode.MESSAGE_NOT_FOUND));
    }

어차피 null 이 들어가봤자 loginId 에 대한 NULL 이라서 NPE 가 발생하지 않는다.

로그인된 객체를 바로 사용해보기

CustomUserDetails 를 따로 만들어 getMember() 란 메서드를 따로 만든다.

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails, CredentialsContainer {

    private final String username;
    private  String password;
    private final boolean enabled;
    private final boolean accountNonExpired;
    private final boolean credentialsNonExpired;
    private final boolean accountNonLocked;
    private final Set<GrantedAuthority> authorities;

    private final Member member;

    public Member getMember(){
        return member;
    }

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

    public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired,
            boolean credentialsNonExpired, boolean accountNonLocked,

이를 이용해서 CustomUserDetails 를 따로 반환한다.

@Override
    public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberRepository.findMemberByLoginId(username)
                .map(this::getUserDetails)
                .orElseThrow(()->new NotFoundException(ErrorCode.MESSAGE_NOT_FOUND));
    }

    public CustomUserDetails getUserDetails(Member member) {
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(member.getAuthority().toString());

        return new CustomUserDetails(member.getLoginId(), member.getPassword(), Collections.singleton(authority),member);
    }

@AuthenticationPrincipal 을 가진 어노테이션을 새로 생성한다.

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : #this.getMember()")
public @interface CurrentMember {

}

위 방식을 이용해서 컨트롤러 수정

public ResponseEntity<Void> savePost(@PathVariable("communityId") final Long communityId,@ModelAttribute @Valid final PostSaveRequestDto postSaveRequestDto,
            @CurrentMember Member member){
        postService.savePost(postSaveRequestDto,communityId,member);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

이로써 따로 가져온 Member 에 대해서 다시 아이디를 뭐 가져오거나 이럴 필요가 없어서
더 좋은 측면이 있어보인다.

다만 이 경우에도 마찬가지로 Null 에 대한 NPE 를 생각을 해줘야 한다.

profile
기록을 통해 실력을 쌓아가자

0개의 댓글