로그인 처리를 위한 UserDetailsService

https://yeo-computerclass.tistory.com/347

 

[spring security] Spring Security 흐름 / 인증(Authentication) & 인가(Authorization)

Spring Security 흐름 Filter(필터) Spring Security의 웹 인프라는 표준 서블릿 필터를 기반으로 합니다. Spring Security 내부에 여러 개의 필터가 각자 특정 책임을 갖고 Request를 처리하는 필터 체인(Filter..

yeo-computerclass.tistory.com

 

위 포스팅한 글을 먼저 읽고오는 것을 추천한다.

 


UserDetailsService

위 글에서 "UserDetailsService 인터페이스의 구현체가 사용자의 정보와 사용자가 가진 권한 정보를 반환한다." 하였다. 

이런 처리 과정에 대해 설명하도록 하겠다.

 

  • UserDetailsService는 로그인 처리를 위한 사용자의 정보를 가져올 때 username과 password를 동시에 가져와 사용하지 않는다.
    • 일단 username을 이용하여 사용자의 존재를 판단한 후에
    • password를 가져와 틀리면 인증 실패라는 결과를 반환하고
    • 일치할 경우(인증 과정 끝) 원하는 URL에 접근할 수 있는 권한(인가)이 있는지 판단한다.

  • UserDetailsService는 UserDetails 인터페이스를 구현한 것
    • UserDetails 인터페이스는 loadUserByUsername() 메소드 하나만 가지고 있다. 이를 구현한 UserDetailsService 역시 loadUserByUsername() 메소드 하나만을 갖는다.
    • loadUserByUsername(): username이라는 회원 아이디를 이용하여 회원 정보를 Load 한다. 
      때문에 Spring Security에서는 username이라는 식별 데이터를 사용한다.
    • loadUserByUsername()의 반환 타입: UserDetails
      • getUsername(): 인증에 필요한 아이디 정보
      • getPassword(): 인증에 필요한 비밀번호 정보
      • getAuthorities(): 사용자의 권한에 대한 정보

  • Spring Security에서는 사용자 자체 혹은 계정에 대해 User라는 용어를 사용한다.
    주의!:  User라는 용어와 겹치지 않게 주의할 필요가 있다.

 


회원을 처리하기 위한 구현

SecurityConfig 구현 & DB에 값 넣어놓기 등이 준비되어야 한다.

이에 대해 궁금하다면 다음 링크 참고하도록 하자! + 아래는 SecurityConfig 코드이다.

 

SecurityConfig 코드

@Configuration
@Log4j2
public class SecurityConfig {

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.authorizeHttpRequests((auth) -> {
            auth.antMatchers("/sample/all").permitAll();
            auth.antMatchers("/sample/member").hasRole("USER");
        });

        http.formLogin();
        http.csrf().disable();
        http.logout();

        return http.build();
    }

}

1. UserDetails 인터페이스 구현

회원의 정보와 가진 권한 정보를 처리하기 위해서는 두 가지 방법을 생각해 볼 수 있다.

 

  1. 기존 DTO 클래스에 UserDetails 인터페이스를 구현하기
  2. UserDetails를 구현한 클래스인 User를 상속하는 별도의 클래스(DTO) 만들기

 

두 번째 방법으로 구현하도록 하겠다. 

 

Member Entity 클래스

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Member extends DateEntity {

    @Id
    private String email;

    private String password;

    private String name;

    private boolean joinSocial;

    @ElementCollection(fetch = FetchType.LAZY)
    @Builder.Default
    private Set<MemberRole> roleSet = new HashSet<>();

    public void addMemberRole(MemberRole memberRole){
        roleSet.add(memberRole);
    }

}

 

AuthMemberDTO  extends User

@Log4j2
@Getter
@Setter
@ToString
public class AuthMemberDTO extends User {

    private String email;

    private String name;

    private boolean joinSocial;

    public AuthMemberDTO(String username, String password, boolean joinSocial, Collection<? extends GrantedAuthority> authorities){
        super(username, password, authorities);
        this.email = username;
        this.joinSocial = joinSocial;
    }
}
  • Member Entity 클래스와 호환되는 DTO 역할을 수행하는 클래스
  • Spring Security 인가 / 인증 작업에 사용할 수 있는 클래스
  • UserDetailsService의 반환 타입이 UserDetails이고 이 클래스는 UserDetails를 구현한 User 클래스를 상속받았다.
  • password 필드는 User 부모 클래스를 사용하였기 때문에 따로 선언하지 않았다.

 


2. UserDetailsService 구현

@Log4j2
@Service
@RequiredArgsConstructor
public class PracUserDetailsService implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("username: " + username);

        Optional<Member> result = memberRepository.findByEmail(username, false);

        if(result.isEmpty()){
            throw new UsernameNotFoundException("Check Email / Social");
        }

        Member member = result.get();
        log.info(member);

        //Entity -> DTO(+Auth)
        AuthMemberDTO authMemberDTO = new AuthMemberDTO(
                member.getEmail(),
                member.getPassword(),
                member.isJoinSocial(),
                member.getRoleSet().stream().map(role->
                        new SimpleGrantedAuthority("ROLE_" + role.name())).collect(Collectors.toList())
                        //.name() : Enum 자체적인 메소드, 상수를 String으로 반환
                );

        log.info("authoMemberDTO");
        log.info(authMemberDTO);

        authMemberDTO.setName(member.getName());

        log.info(authMemberDTO);

        return authMemberDTO;
    }
}
  • UserDetailsService를 구현한 Service 클래스
  • loadUserByUsername 메소드를 구현해주어야 한다. 이때 반환값은 UserDetails이다.
    authMemberDTO는 UserDetails의 구현 클래스인 User를 상속한 클래스이므로 이에 만족한다.

 


3. 결과

http://localhost:8080/sample/member 접속 

DB에 없는 값 입력 시

올바른 값 입력 시 (DB에 저장되어 있는 값 + ROLE_USER 권한 갖는 회원)


참고 - 코드로 배우는 스프링 부트 웹 프로젝트