[Spring]Spring Security를 이용한 로그인처리-(5)

윤재열·2022년 3월 30일
1

Spring

목록 보기
47/72

시큐리티를 위한 UserDedailsService

  • 스프링 시큐리티에서는 회원이나 계정에 대해서 User라는 용어를 사용합니다.User라는 단어를 사용할 때는 상당히 주의해야 합니다. 이러한 이유 때문에 앞의 예제에서도 ClubMember와 같이 다른 이름을 사용하고 있습니다.
  • 회원 아이디라는 용어 대신 username이라는 단어를 사용합니다. 스프링 시큐리티에서는 username이라는 단어 자체가 회원을 구별할 수 있는 식별 데이터를 의미합니다. 문자열로 처리하는 점은 같지만 일반적으로 사용하는 회원의 이름이 아니라 오히려 id에 해당합니다.
  • username과 password를 동시에 사용하지 않습니다. 스프링 시큐리티는 UserDetailsService를 이용해서 회원의 존재만을 우선적으로 가져오고, 이후에 password가 틀리면 'Bad Cridential(잘못된 자격증명)'이라는 결과를 만들어 냅니다.(인증)
  • 사용자의 username과 password로 인증 과정이 끝나면 원하는 자원(URL)에 접근할 수 있는 적절한 권한이 있는지를 확인하고 인가 과정을 실행합니다.
    이 과정에서는 'Access Denied'와 같은 결과가 만들어 집니다.

UserDetails 인터페이스

  • loadUserByUsername()은 말 그대로 username이라는 회원 아이디와 같은 식별 값으로 회원 정보를 가져옵니다.
  • 메서드의 리턴 타입은 UserDetils라는 타입인데 이를 통해서 다음과 같은 정보를 알아낼 수 있도록 구성되어 있습니다.
- getAuthorities() : 사용자가 가지는 권한에 대한 정보
- getPassword() : 인증을 마무리하기 위한 패스워드 정보
- getUsername() : 인증에 필요한 아이디와 같은 정보
- 계정 만료 여부 : 더이상 사용이 불가능한 계정인지 알 수 있는 정보
- 계정 잠김 여부 : 현재 계정의 잠김 여부
  • ClubMember를 처리할 수 있는 2가지 방법
    -기존 DTO 클래스에 UserDetails인터페이스를 구현하는 방법
    -DTO와 같은 개념으로 별도의 클래스를 구성하고 이를 활용하는 방법
  • 여기서는 인터페이스를 구현한 별도의 클래스가 있기 때문에 이를 사용합니다.
@Log4j2
//User 클래스는 UserDetailsService로부터 핵심 유저 정보를 모델링한다.
//User 클래스를 상속하고 부모 클래스인 User 클래스의 생성자를 호출할 수 있는 코드를 만든다.
//부모 클래스인 User 클래스에 사용자 정의 생성자가 있으므로 반드시 호출할 필요가 있다.

//ClubAuthMemberDTO는 DTO 역할을 수행하는 클래스인 동시에 스프링 시큐리티에서 인가/인증 작업에 사용할 수 있다.
//password는 부모 클래스를 사용하므로 별도의 멤버 변수로 선언하지 않는다.
public class ClubAuthMemberDTO extends User {
    private String email;
    private String name;
    private boolean fromSocial;
    public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities){
        //User 클래스의 생성자를 호출한다.
        super(username, password, authorities);

        this.email=username;
        this.fromSocial=fromSocial;
    }
}
  • ClubMember가 ClubAuthMemberDTO라는 타입으로 처리된 가장 큰 이유는 사용자의 정보를 가져오는 핵심적인 역할을 하는 UserDetailsService라는 인터페이스 때문입니다.

  • 스프링 시큐리티의 구조에서 인증을 담당하는 AuthenticationManager는 내부적으로 UserDetailsService를 호출해서 사용자의 정보를 가져온다.

  • 현재 예제와 같이 JPA로 사용자의 정보를 가져오고 싶다면 이 부분을 UserDetailsService가 이용하는 구조로 작성할 필요가 있습니다.

  • 추가된 service패키지에는 이를 위한 ClubUserDetailsService 클래스를 다음과 같이 추가합니다.

mport lombok.extern.log4j.Log4j2;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Log4j2
//Service어노테이션을 사용해서 자동으로 스프링에서 빈으로 처리될 수 있게 한다.
//ClubUserDetailsService가 빈(Bean)으로 등록되면 이를 자동으로 스프링 시큐리티에서
//UserDetailsService로 인식하기 때문에 기존에 임시로 코드로 직접 설정한
//configure(AuthenticationManagerBuilder auth) 부분을 사용하지 않도록 수정한다.
@Service
public class ClubUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("ClubUserDetailsService loadUserByUsername"+username);
        return null;
    }
}
  • 이제 SecureConfig.java 에서 정의했던 configure 메서드가 필요없으므로 지우거나 주석 처리한다.
  • 정상적인 처리를 위해서 ClubUserDetailsService와 ClubMemberRepository를 연동하는 것은 아래와 같이 처리할 수 있다.
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.young.club.entity.ClubMember;
import org.young.club.repository.ClubMemberRepository;
import org.young.club.security.dto.ClubAuthMemberDTO;

import java.util.Optional;
import java.util.stream.Collectors;

@Log4j2
//Service어노테이션을 사용해서 자동으로 스프링에서 빈으로 처리될 수 있게 한다.
//ClubUserDetailsService가 빈(Bean)으로 등록되면 이를 자동으로 스프링 시큐리티에서
//UserDetailsService로 인식하기 때문에 기존에 임시로 코드로 직접 설정한
//configure(AuthenticationManagerBuilder auth) 부분을 사용하지 않도록 수정한다.
@Service
@RequiredArgsConstructor
public class ClubUserDetailsService implements UserDetailsService {
    private final ClubMemberRepository clubMemberRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("ClubUserDetailsService loadUserByUsername"+username);

        Optional<ClubMember> result=clubMemberRepository.findByEmail(username, false);
        System.out.println("username:"+username);
        if(result.isPresent()==false){
            throw new UsernameNotFoundException("Check Email or Social ");
        }

        ClubMember clubMember=result.get();

        System.out.println(clubMember.getRoleSet().toString());
        log.info("--------------------------");
        log.info(clubMember);

        //ClubMember를 UserDetails 타입으로 처리하기 위해서 ClubAuthMemberDTO 타입으로 변환
        ClubAuthMemberDTO clubAuthMember=new ClubAuthMemberDTO(
                clubMember.getEmail(),
                clubMember.getPassword(),
                clubMember.isFromSocial(),
                //ClubMemberRole은 스프링 시큐리티에서 사용하는 SimpleGrantedAuthority로 변환,
                //이때 'ROLE_'라는 접두어를 추가해서 사용한다.
                //user95@zerock.org 같은 경우 롤이 3개다 [USER, MANAGER, ADMIN]
                //이 각각을 [ROLE_ADMIN, ROLE_MANAGER, ROLE_USER]로 변환해서 Set으로 넣어주고 그 컬렉션을 반환한다.
                clubMember.getRoleSet().stream()
                .map(role->new SimpleGrantedAuthority("ROLE_"+role.name())).collect(Collectors.toSet()));

        clubAuthMember.setName(clubMember.getName());
        System.out.println(clubAuthMember.getAuthorities().toString());
        return clubAuthMember;
    }
}

  • 정상적으로 로그인한 결과입니다.
  • 중요한 것은 AuthenticationManager는 내부적으로 UserDetailsService를 호출해서 사용자의 정보를 가져옵니다.
  • 즉 loadUserByUsername 메서드를 통해서 사용자 정보(clubAuthMember)를 넘겨받는다. 그 clubAuthMember는 DTO형태로 포장되서 반환되는 것이다.
profile
블로그 이전합니다! https://jyyoun1022.tistory.com/

0개의 댓글