<aside> 2️⃣ error1

</aside>

2023-08-03T15:21:15.063+09:00 ERROR 39577 --- [nio-8080-exec-4] c.j.p.exception.GlobalControllerAdvice   : Error occurs java.lang.SecurityException: Principal is not an instance of UserDetails

이 오류 메시지는 Spring Security에서 발생하는 것으로 보이며, UserDetails 인스턴스를 기대하는 곳에 다른 타입의 주체(principal) 객체가 전달되었을 때 발생합니다.

Spring Security에서는 UserDetails 인터페이스를 구현한 사용자 정의 클래스를 사용하여 인증된 사용자의 세부 정보를 저장하고 관리합니다. 이 UserDetails 객체는 인증 과정에서 생성되며, 이후에는 **SecurityContextHolder**를 통해 접근할 수 있습니다.

이 오류는 **UserDetails**를 반환해야 하는 메소드나 **UserDetails**를 필요로 하는 코드 부분에서 **UserDetails**가 아닌 다른 타입의 객체가 반환되거나 전달되었을 때 발생합니다. 이는 주로 **UserDetailsService**의 loadUserByUsername 메소드에서 **UserDetails**가 아닌 다른 타입의 객체를 반환하거나, Authentication 객체를 생성할 때 **UserDetails**가 아닌 다른 타입의 주체(principal)를 사용했을 때 발생합니다.

이 문제를 해결하려면 **UserDetailsService**의 loadUserByUsername 메소드가 UserDetails 객체를 반환하도록 수정하거나, Authentication 객체를 생성할 때 UserDetails 객체를 주체(principal)로 사용하도록 코드를 수정해야 합니다. 또한, UserDetails 객체를 필요로 하는 모든 코드 부분에서 UserDetails 객체가 제대로 사용되고 있는지 확인해야 합니다.

이래서 혹시 security config에서 잘못 설정했나 가봤는데 문제없었다. 그럼 뭘까? 고민하다 에러메시지를 보니 내가 log.error로 남긴것이었다.

그래서 찾아갔더니 이거였다.

/**
 * User 클래스가 username 속성만 가지고 있다고 가정하고 작성한 것이다. 실제 User 클래스에 어떤 속성들이 있는지에 따라 이 코드를 적절히 수정해야 한다.
 * 또한, 이 코드는 UserDetails 객체에서 사용자의 정보를 가져와 User 객체를 새로 생성하고 있다.
 * 만약 UserDetails 객체가 이미 User 객체를 나타내고 있다면, 즉 UserDetails 객체가 User 클래스의 인스턴스라면, UserDetails 객체를 직접 User 타입으로 캐스팅하여 반환할 수 있다.
 */
public User getAuthenticatedUser() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null) {
        throw new SecurityException("No authentication data provided");
    }

    Object principal = authentication.getPrincipal();
    if (!(principal instanceof UserDetails)) {
        throw new SecurityException("Principal is not an instance of UserDetails");
    }

    // Cast UserDetails to User
    User user = (User) principal;

    return user;
}

UserDetails 인터페이스는 Spring Security에서 사용자의 정보를 담는 인터페이스입니다. 이 인터페이스를 구현한 클래스의 인스턴스는 Authentication 객체의 getPrincipal() 메소드를 통해 얻을 수 있습니다.

UserDetails 인터페이스를 구현한 클래스의 인스턴스가 아닌 다른 객체가 Authentication 객체의 getPrincipal() 메소드를 통해 반환되는 경우, principal instanceof UserDetails 표현식은 **false**를 반환하고, 따라서 **SecurityException**이 발생합니다.

이 문제를 해결하려면 Authentication 객체의 getPrincipal() 메소드가 UserDetails 인터페이스를 구현한 클래스의 인스턴스를 반환하도록 해야 합니다. 이를 위해서는 UserDetails 인터페이스를 구현한 클래스의 인스턴스를 Authentication 객체에 설정해야 합니다.

이 설정은 일반적으로 AuthenticationProvider 또는 **UserDetailsService**의 구현에서 이루어집니다. **AuthenticationProvider**의 authenticate() 메소드 또는 **UserDetailsService**의 loadUserByUsername() 메소드에서 UserDetails 인터페이스를 구현한 클래스의 인스턴스를 생성하고, 이를 Authentication 객체에 설정합니다.

따라서, AuthenticationProvider 또는 **UserDetailsService**의 구현을 확인하고, UserDetails 인터페이스를 구현한 클래스의 인스턴스가 제대로 생성되고 Authentication 객체에 설정되는지 확인해야 합니다.

이래서 방법을 찾아보기 이전에 내가 UserDetails를 구현한 구현체를 확인했다.

package com.jinan.profile.dto.security;

import com.jinan.profile.dto.user.UserDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

@Slf4j
@Getter
@AllArgsConstructor
public class SecurityUserDetailsDto implements UserDetails {

    @Delegate
    private UserDto userDto;
    private Collection<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return userDto.password();
    }

    @Override
    public String getUsername() {
        return userDto.loginId();
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}

SecurityUserDetailsDto 클래스는 UserDetails 인터페이스를 구현하고 있으므로, 이 클래스의 인스턴스는 Authentication 객체의 getPrincipal() 메소드를 통해 반환될 수 있습니다.