프로젝트

Spring Security로 회원가입을 해보자 (2) - SecurityConfig 및 회원가입

ummchicken 2023. 3. 2. 01:51

안녕하세요.

1편인 Spring Security로 회원가입을 해보자 (1) - 회원 엔티티 설계에 이어, 

SecurityConfig 및 회원가입에 관한 여러 가지 설정들을 하겠습니다.

로그인은 다음 편에 쓸 것 같습니다.

 

1편 : Spring Security로 회원가입을 해보자 (1) - 회원 엔티티 설계

3편 : Spring Security로 회원가입을 해보자 (3) - SecurityConfig 및 로그인

 

 

1편과 마찬가지로, 계속해서 수정될 수 있습니다.

※ 틀린 부분이 있을 수도 있습니다.

 

 

먼저 SecurityConfig 입니다.

아마 후에 많이 바뀔 것 같은 부분입니다;;

✔️ SecurityConfig 

[SecurityConfig]

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .formLogin().disable() 
                .httpBasic().disable() 
                .csrf().disable() 
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests() 
//                .antMatchers("/**").permitAll(); 
                .antMatchers("/login", "/signUp","/").permitAll()
                .anyRequest().authenticated(); 

        return http.build();
    }

    // 비밀번호 암호화 저장
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}
  • formLogin().disable() 
    • formLogin 인증 방법을 비활성화합니다.
    • json을 통해 로그인을 진행하기 위함입니다.
  • httpBasic().disable() 
    • httpBasic 인증 방법을 비활성화합니다.
    • formLogin 설정을 하지 않으면, HttpBasic 인증을 받아야만 접근이 가능하다.
      • 특정 리소스에 접근할 때 username과 password를 물어봄
  • csrf().disable() 
    • Spring Security는 csrf 토큰 없이 요청하면 해당 요청을 막습니다.
    • 따라서 잠시 비활성화해줍니다.
  • authorizeRequests() 
    • HttpServletRequest에 따라 접근을 제한합니다.
  • antMatchers("/**").permitAll()
    • 모든 url에 대해서 접근할 수 있도록 허용합니다.
    • 일단 잠깐 주석처리 해놨습니다.
  • antMatchers("/login", "/signUp","/").permitAll()
    • antMatchers() 메소드로 특정 경로를 지정하며, permitAll(),hasRole() 메소드로 권한에 따른 접근 설정을 합니다.
  • anyRequest().authenticated()
    • 그 외의 경로는 인증된 사용자만이 접근이 가능합니다.
  • form을 활성화하면 더 많은 설정들이 있지만, 지금은 view를 만들지 않고 json으로 테스트만 할 것이므로 잠시 생략하겠습니다.

 

 

 

 

다음은 Password가 Bcrypt로 암호화되는지 테스트해 보겠습니다.

 

먼저, 기본 틀 입니다.

@SpringBootTest
public class PasswordEncoderTest {

    @Autowired
    PasswordEncoder passwordEncoder;

    // 테스트 코드 추가

}

 

 

 

다음은 password가 암호화되는지 테스트입니다.

encode된 password는 본래 password와 일치하면 안 됩니다.

@Test
void 패스워드_암호화_encodeWithBcryptTest() throws Exception {
    // given
    String password = "ummchicken";

    // when
    String encodePassword = passwordEncoder.encode(password);

    // then
    assertThat(encodePassword).isNotEqualTo(password);
}

 

 

 

다음은 암호화 시 항상 랜덤한 값을 반환하는 테스트입니다.

salt 를 통한 암호화이기 때문입니다.

항상 다른 결과가 반환되면 됩니다.

(나름대로 스프링 시큐리티 분석한 포스팅)

@Test
void 패스워드_랜덤_암호화() throws Exception {
    // given
    String password = "ummchicken";

    // when
    String encodePassword = passwordEncoder.encode(password);
    String encodePassword2 = passwordEncoder.encode(password);

    // then
    assertThat(encodePassword).isNotEqualTo(encodePassword2);
}

 

 

 

다음은 암호화된 비밀번호 매치 테스트입니다.

단순 equals로 하면 안 되고, 제공하는 matches로 해야 합니다.

@Test
void 암호화된_비밀번호_매치() throws Exception {
    // given
    String password = "ummchicken";

    // when
    String encodePassword = passwordEncoder.encode(password);

    // then
    assertThat(passwordEncoder.matches(password, encodePassword)).isTrue();
}

 

 

 

 

다음은 회원가입을 위한 MemberSaveRequestDto 입니다.

✔️ MemberSaveRequestDto

[MemberSaveRequestDto]

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class MemberSaveRequestDto {

    @NotBlank(message = "비밀번호를 입력해주세요")
    @Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,30}$",
            message = "비밀번호는 8~30 자리이면서 1개 이상의 알파벳, 숫자, 특수문자를 포함해야합니다.")
    private String password;

    @NotEmpty(message = "이메일은 필수 입력 값입니다.")
    @Email(message = "이메일 형식으로 입력해주세요.")
    private String email;

    @NotBlank(message = "닉네임을 입력해주세요.")
    @Size(min=2, message = "닉네임이 너무 짧습니다.")
    private String nickname;


    private Role role;

    // DTO -> Entity
    public Member toEntity() {
        Member member = Member.builder()
                .password(password)
                .email(email)
                .nickname(nickname)
                .role(role)
                .build();
        return member;
    }

}
  • 그리고 Member 엔티티의 password 필드에 nullable = false 조건을 추가했습니다.

 

 

 

 

다음은 MemberService 입니다.

✔️ MemberService

[MemberService]

@RequiredArgsConstructor
@Service
public class MemberService {

    private final MemberRepository memberRepository;

    // 회원 저장
    @Transactional
    public Long saveMember(MemberSaveRequestDto memberSaveRequestDto) {
        validateDuplicateMember(memberSaveRequestDto);
        return memberRepository.save(memberSaveRequestDto.toEntity()).getId();
    }

    /**
     * 회원가입 중복 검사
     * 이메일 중복 불가능
     * */
    private void validateDuplicateMember(MemberSaveRequestDto memberSaveRequestDto) {

        if(memberRepository.findByEmail(memberSaveRequestDto.getEmail()).isPresent()) {
            throw new IllegalArgumentException("이미 존재하는 회원입니다.");
        }
    }

}
  • update에 관련된 건, 로그인 기능을 생성한 후에 가능하니 일단 회원가입 중복 검사만 만들었습니다.

 

 

여기서 삽질을 했는데요, 참 어이없는 실수를 했습니다.

원래 코드는 

private void validateDuplicateMember(MemberSaveRequestDto memberSaveRequestDto) {
    Member findMember = memberRepository.findByEmail(memberSaveRequestDto.getEmail())
            .orElseThrow(() -> new IllegalArgumentException("해당 회원이 없습니다. email = " + memberSaveRequestDto.getEmail()));

    if(findMember != null) {
        throw new IllegalArgumentException("이미 존재하는 회원입니다.");
    }
}

이거였습니다.

 

하지만 회원가입 테스트부터 실패를 하는 겁니다?

에러 메세지를 봤더니 회원을 찾을 수가 없다는...

 

뭔가 하고 코드를 다시 살펴봤더니, 

중복 검사를 하는 메소드인 validateDuplicateMember에 findMember 부분이었네요.

회원을 저장하기도 전에, 해당 회원을 찾으려니 당연히 예외가 발생했던 것입니다.

 

따라서 코드를 isPresent로 바꿨더니 통과됩니다.

 

아무튼 어이없는 썰이었구요.

 

 

테스트 코드로 가보겠습니다.

 

 

제가 막 짠 거라 틀렸을 수도 있어요.

어쨌든 통과가 되긴 됩니다...;;

 

✔️ MemberServiceTest

먼저 기본 세팅입니다.

[MemberServiceTest]

@SpringBootTest
@Transactional
class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    EntityManager em;

    @AfterEach
    private void after(){
        em.clear();
    }

    private void clear(){
        em.flush();
        em.clear();
    }

    private MemberSaveRequestDto memberSaveRequestDto() {
        return new MemberSaveRequestDto("12345678", "aaa@naver.com", "myNickname", Role.USER);
    }
    
    
    // 테스트 코드 추가
    
    
}
  • MemberSaveRequestDto에 회원 정보를 저장하는 메소드를 따로 만들어 놓습니다.
    • 매번 만들기 번거로우니...

 

 

 

다음은 회원가입 테스트코드입니다.

@Test
public void 회원가입_테스트() {
    // given
    MemberSaveRequestDto memberSaveRequestDto = memberSaveRequestDto();

    // when
    memberService.saveMember(memberSaveRequestDto);
    clear();

    // then
    Member member = memberRepository.findByEmail(memberSaveRequestDto.getEmail()).orElseThrow(
            () -> new IllegalArgumentException("해당 회원 없습니다. email = " + memberSaveRequestDto.getEmail())
    );

    assertThat(member.getId()).isNotNull();
    assertThat(member.getEmail()).isEqualTo(memberSaveRequestDto.getEmail());
    assertThat(member.getNickname()).isEqualTo(memberSaveRequestDto.getNickname());
    assertThat(member.getRole()).isSameAs(Role.USER);
}

 

 

 

다음은 이메일 중복 회원가입 코드입니다.

@Test
public void 중복_회원가입_이메일중복() {
    // given
    MemberSaveRequestDto memberSaveRequestDto = memberSaveRequestDto();
    memberService.saveMember(memberSaveRequestDto);
    clear();

    // when, then
    assertThat(assertThrows(Exception.class,
            () -> memberService.saveMember(memberSaveRequestDto)));

}

 

 

 

다음은 회원가입 시, 입력하지 않은 필드가 있을 때 오류 발생 코드입니다.

@Test
public void 회원가입_실패_입력하지않은_필드가있으면_오류() {
    // given
    MemberSaveRequestDto memberSaveRequestDto1 = new MemberSaveRequestDto(null, "aaa@naver.com", "nickname", Role.USER);
    MemberSaveRequestDto memberSaveRequestDto2 = new MemberSaveRequestDto(passwordEncoder.encode("12345678!!"), null, "nickname", Role.USER);
    MemberSaveRequestDto memberSaveRequestDto3 = new MemberSaveRequestDto(passwordEncoder.encode("12345678!!"), "aaa@naver.com", null, Role.USER);

    // when, then
    assertThrows(Exception.class,
            () -> memberService.saveMember(memberSaveRequestDto1));

    assertThrows(Exception.class,
            () -> memberService.saveMember(memberSaveRequestDto2));

    assertThrows(Exception.class,
            () -> memberService.saveMember(memberSaveRequestDto3));
}
  • Member 엔티티에서 nullable = false로 설정해 줬기 때문에 가능한 것입니다.

 

 

여기서도 삽질을 좀 했습니다.

MemberSaveRequsetDto에 @NotBlank 속성을 추가했기 때문에 

필드를 입력하지 않은 것을 잡아내는 건 줄 알았습니다.

 

하지만, 테스트 코드가 실패하는 것입니다?

 

 

에러메세지를 보니, password를 입력하지 않아서 예외가 발생해야 하는데, 

아무 예외도 발생하지 않았다는 것입니다.

 

 

뭔가 하고 봤더니...

Member 엔티티에 password 필드만 nullable = false 설정을 안 해놨더군요.

 

 

그래서 혹시나 추가하고 다시 테스트코드를 돌려봤더니, 

통과됩니다.

 

 

또 하나를 배워가네요.

김영한 개발자님 강의를 듣고 있는데, 

validation 부분을 다음 날에 들으려고 미뤄뒀더니...

역시 학습은 게을리하면 안 된다는 것을 알게 되었습니다.

 

 

 

 

이번 포스팅은 여기까지입니다.

 

다음 포스팅은 loadUserByUsername을 포함한 로그인/로그아웃, 회원정보 수정이 될 것 같습니다.

 

 

 

 

- 회원가입 설정 끝 -