이번 글에서는 스프링 시큐리티를 이용하여 로그인/로그아웃 기능을 구현한다.
로그인/로그아웃 과정은 스프링 시큐리티가 대신 처리해 주기 때문에 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는 처음 적용해보기 때문에 실수도 많고 헤매기도 많이 헤맸다..
하필 처음으로 적용한게 로그인이라 더 어려웠던 것 같기도 하다. 그래도 조금씩 적응하면 다음 기능부터는 더 깔끔하게 정리할 수 있을 것 같다!!
'개인 프로젝트 > 쇼핑몰 만들기' 카테고리의 다른 글
쇼핑몰 만들기 3. 상품 등록 기능 구현 - 1 (SpringBoot) (0) | 2022.08.08 |
---|---|
쇼핑몰 만들기 2. 회원 정보 수정 기능 (0) | 2022.08.08 |
쇼핑몰 만들기 0. 프로젝트 생성 및 환경 설정 (0) | 2022.07.12 |