goblin
리니팅
goblin

공지사항

전체 방문자
오늘
어제
  • 분류 전체보기 (75)
    • 개발 (31)
      • Spring (12)
      • JPA (4)
      • JAVA (4)
      • Python (6)
      • Docker (1)
      • Error (3)
      • Spring Cloud로 개발하는 MSA (1)
    • 알고리즘 (32)
    • 자료구조 (3)
    • 컴퓨터 개론 (3)
    • 개인 프로젝트 (4)
      • 쇼핑몰 만들기 (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

인기 글

태그

  • 조합
  • springboot
  • 자료구조
  • 다이나믹 프로그래밍
  • 클래스
  • 정렬
  • gradle
  • 스프링
  • BOJ
  • python
  • JPA
  • tdd
  • 다이나믹프로그래밍
  • sorting
  • 파워자바
  • 알고리즘
  • Intellij
  • 객체
  • 프로그래머스
  • 백준
  • 파이썬
  • 문자열
  • 동적계획법
  • 스프링부트
  • 코딩테스트연습
  • 코딩테스트
  • 구현
  • Spring
  • dp
  • inflearn

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
goblin

리니팅

쇼핑몰 만들기 1. 로그인/로그아웃 구현 (Spring Security)
개인 프로젝트/쇼핑몰 만들기

쇼핑몰 만들기 1. 로그인/로그아웃 구현 (Spring Security)

2022. 7. 16. 17:54
728x90

이번 글에서는 스프링 시큐리티를 이용하여 로그인/로그아웃 기능을 구현한다.

로그인/로그아웃 과정은 스프링 시큐리티가 대신 처리해 주기 때문에 Controller만 구현했다.

 

로그인을 하기 위해서는 회원 정보를 조회해야 한다.

TDD를 적용해 보고자 하기 떄문에, 회원 조회 테스트 코드를 먼저 작성한다.

 

https://arinlee.tistory.com/55

 

[SpringBoot] 테스트 코드 작성, TDD

최근의 추세는, 대부분의 서비스 회사가 테스트 코드에 관해 요구하고 있습니다. 이 글에서는 테스트 코드 작성의 기본에 대해 다루겠습니다. 먼저 TDD와 단위 테스트는 다르다는 것을 알아야 합

arinlee.tistory.com

TDD와 단위 테스트는 다르다..! 위 게시물을 참고하면 좋을 듯 싶다..ㅎㅎ

 

회원 조회 테스트

@ExtendWith(MockitoExtension.class)
class MemberServiceTest {

    @InjectMocks
    private MemberService target;

    @Mock
    private MemberRepository memberRepository;

    private final String email = "test@email.com";
    private final Member member = Member.builder()
            .email("test@email.com")
            .memberName("tester")
            .address("서울특별시")
            .password("password")
            .memberType(Type.BASE)
            .role(Role.USER)
            .build();
            
    @Test
    public void 회원조회테스트_성공() throws Exception {
        //given
        doReturn(Optional.of(member)).when(memberRepository).findByEmail(email);

        //when
        Member result = target.findByEmail(email);

        //then
        assertThat(result).isEqualTo(member);
    }

    @Test
    public void 회원조회테스트_실패() throws Exception {
        //given
        doReturn(Optional.empty()).when(memberRepository).findByEmail(email);

        //when
        final BusinessException result = assertThrows(BusinessException.class, () -> target.findByEmail(email));

        //then
        assertThat(result.getMessage()).isEqualTo(ErrorCode.NO_MATCHING_MEMBER.getMessage());
    }

    
}

테스트를 실행하면 컴파일 에러가 나는 것은 당연하다.

컴파일 에러를 보고 필요한 코드를 추가한다.

    private void validateDuplicateMember(Member member) {
        Optional<Member> optionalMember = memberRepository.findByEmail(member.getEmail());
        if (optionalMember.isPresent()) {
            throw new BusinessException(ErrorCode.ALREADY_REGISTERED_MEMBER);
        }
    }

    public Member findByEmail(String email) {
        return memberRepository.findByEmail(email)
                .orElseThrow(() -> new BusinessException(ErrorCode.NO_MATCHING_MEMBER));
    }

테스트가 통과됐다면 스프링 시큐리티 설정을 한다.

 

 

본격적으로 시작하기에 앞서 UserDetailsService와 UserDetail에 대해 간단히 정리하겠다.

 

1. UserDetailsService

  • 데이터베이스에서 회원 정보를 가져오는 역할
  • loadUserByUsername() 메소드가 존재, 회원 정보를 조회하여 사용자의 정보와 권한을 갖는 UserDetails 인터페이스를 반환

→ 스프링 시큐리티에서 UserDetailService를 구현하는 클래스를 통해 로그인 기능을 구현하는 것

 

2. UserDetail

  • UserDetails : 스프링 시큐리티에서 회원의 정보를 담기 위해 사용하는 인터페이스
  • User : UserDetails 인터페이스를 구현하고 있는 클래스
  • 위 인터페이스를 직접 구현하거나 스프링 시큐리티에서 제공하는 User 클래스를 사용한다.

 

MemberService

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException{
        Member member = findByEmail(email);

        if (member == null){
            throw new UsernameNotFoundException(email);
        }

        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }

MemberService가 UserDetailsService를 구현한다. UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 오버라이딩하여 로그인할 유저의 email을 파라미터로 전달받도록 했다.

 

SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final AuthenticationFailureHandler loginFailureHandler;
    private final MemberService memberService;

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.csrf().disable();

        http.formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .usernameParameter("email")
                .failureHandler(loginFailureHandler)
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                ;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(memberService)
                .passwordEncoder(passwordEncoder());
    }
}
  • WebSecurityConfigurerAdapter를 상속받는 클래스에 @EnableWebSecurity 어노테이션을 선언하면 SpringSecurityFilterChain이 자동으로 포함된다. 이를 통해 보안 설정을 커스터마이징할 수 있다.
  • configure(http) : http 요청에 대한 보안을 설정한다.
  • PasswordEncoder : BCryptPasswordEncoder의 해시 함수를 이용하여 비밀번호를 암호화하여 저장한다. 이를 빈으로 등록하여 사용하도록 한다.

 

로그인 기능은 스프링 시큐리티를 이용하기 때문에 Repository나 Service 테스트를 생략하고 바로 Controller 테스트부터 진행하겠다.

@ExtendWith(MockitoExtension.class)
class LoginControllerTest {

    @InjectMocks
    private LoginController target;

    private MockMvc mockMvc;

    private final String email = "test@email.com";
    private final String password = "123";

    @BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.standaloneSetup(target)
                .build();
    }

    @Test
    public void 로그인테스트_실패() throws Exception {
        //given

        //when
        ResultActions resultActions = mockMvc.perform(formLogin().user(email).password(password));

        //then
        resultActions.andExpect(unauthenticated());
    }

    @Test
    public void 로그인뷰봔환_테스트() throws Exception {
        //given
        final String url = "/login";
        final String view = "login/loginform";

        //when
        ResultActions resultActions = mockMvc.perform(get(url)).andDo(print());

        //then
        resultActions
                .andExpect(view().name(view));
    }

}

 

 

먼저 각 테스트 실행 전에 초기화를 도와주는 @BeforeEach를 사용했다.

로그인뷰 반환 테스트를 먼저 해보면, 테스트 코드만 작성하고 아직 컨트롤러는 만들지 않은 상태이기 때문에 당연히 에러가 발생한다. 

No ModelAndView found라고 한다. Controller를 작성해준다.

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginMember(){
        return "login/loginform";
    }
}

뷰를 잘 반환하기 때문에 테스트는 통과한다. (loginform 파일은 별도로 작성했다.)

 

 


TDD는 처음 적용해보기 때문에 실수도 많고 헤매기도 많이 헤맸다..

하필 처음으로 적용한게 로그인이라 더 어려웠던 것 같기도 하다. 그래도 조금씩 적응하면 다음 기능부터는 더 깔끔하게 정리할 수 있을 것 같다!!

 

728x90
반응형

'개인 프로젝트 > 쇼핑몰 만들기' 카테고리의 다른 글

쇼핑몰 만들기 3. 상품 등록 기능 구현 - 1 (SpringBoot)  (0) 2022.08.08
쇼핑몰 만들기 2. 회원 정보 수정 기능  (0) 2022.08.08
쇼핑몰 만들기 0. 프로젝트 생성 및 환경 설정  (0) 2022.07.12
    '개인 프로젝트/쇼핑몰 만들기' 카테고리의 다른 글
    • 쇼핑몰 만들기 3. 상품 등록 기능 구현 - 1 (SpringBoot)
    • 쇼핑몰 만들기 2. 회원 정보 수정 기능
    • 쇼핑몰 만들기 0. 프로젝트 생성 및 환경 설정
    goblin
    goblin

    티스토리툴바