[Spring Boot] 쇼핑몰 프로젝트 (5) - 스프링 시큐리티를 이용한 회원가입

✏️ security dependency 추가하기

의존성을 추가해보겠습니다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

의존성 추가만으로 이제 모든 요청은 인증을 필요로합니다.
이 상태로는 애플리케이션을 운영할 수 없으므로 페이지마다 필요한 권한을 부여해야합니다.

  • 인증이 필요 없는 경우 : 상품 상세 페이지 조회
  • 인증이 필요한 경우 : 상품 주문
  • 관리자 권한이 필요한 경우 : 상품 등록

✏️ 스프링 시큐리티 설정하기

SecurityConfig 소스를 작성하겠습니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        
    }
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

WebSecurityConfigurerAdapter를 상속받는 클래스에 @EnableWebSecurity 어노테이션을 선언하면 SpringSecurityFilterChain이 자동으로 포함됩니다.
메소드 오버라이딩을 통해 보안 설정을 커스터마이징할 수 있습니다.
비밀번호를 데이터베이스에 그대로 저장하면 안되기 때문에 해시 함수를 이용해서 비밀번호를 암호화하여 저장합니다.

✏️ 회원 가입 기능 구현하기

멤버가 일반 유저인지, 관리자인지 구분할 수 있는 역할이 있어야합니다.

Role.java

public enum Role {
    USER, ADMIN
}

가입정보를 담을 dto를 생성해보겠습니당.

MemberFormDto.java

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberFormDto {

    private String name;

    private String email;

    private String password;

    private String address;
}

회원 정보를 저장하는 Member 엔티티를 마들어보겠습니다.

Member.java

@Entity
@Table(name="member")
@Getter @Setter
@ToString
public class Member {

    @Id
    @Column(name="member_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @Column(unique = true)
    private String email;

    private String password;

    private String address;

    @Enumerated(EnumType.STRING)
    private Role role;

    public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder) {
        Member member =  new Member();
        member.setName(memberFormDto.getName());
        member.setEmail(memberFormDto.getEmail());
        member.setAddress(memberFormDto.getAddress());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        member.setPassword(password);
        member.setRole(Role.USER);
        return member;
    }
}

참고)
자바의 enum타입을 엔티티의 속성으로 지정할 때 기본적으로 순서가 저장되는데, enum의 순서가 바뀔 경우 문제가 발생할 수 있으므로 "EnumType.STRING" 옵션을 사용해서 String으로 저장해야합니다

Member 엔티티를 데이터베이스에 저장할 수 있도록 MemberRepository를 만든 후 MemberService 클래스를 작성해보겠습니다.

MemberService.java

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    public Member saveMember(Member member){
        validateDuplicateMember(member);
        return memberRepository.save(member);
    }

    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if(findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }
}

비즈니스 로직을 담당하는 서비스 계층 클래스에 @Transactional 어노테이션을 선언합니다. 로직을 처리하다가 에러가 발생했다면 변경된 데이터를 로직을 수행하기 이전 상태로 콜백 시켜줍니다.

✏️ 테스트 코드

@SpringBootTest
@Transactional
@TestPropertySource(locations="classpath:application-test.properties")
class MemberServiceTest {

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Member createMember(){
        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail("[email protected]");
        memberFormDto.setName("홍길동");
        memberFormDto.setAddress("서울시 마포구 합정동");
        memberFormDto.setPassword("1234");
        return Member.createMember(memberFormDto, passwordEncoder);
    }

    @Test
    @DisplayName("회원가입 테스트")
    public void saveMemberTest(){
        Member member = createMember();
        Member savedMember = memberService.saveMember(member);
        assertEquals(member.getEmail(), savedMember.getEmail());
        assertEquals(member.getName(), savedMember.getName());
        assertEquals(member.getAddress(), savedMember.getAddress());
        assertEquals(member.getPassword(), savedMember.getPassword());
        assertEquals(member.getRole(), savedMember.getRole());
    }

    @Test
    @DisplayName("중복 회원 가입 테스트")
    public void saveDuplicateMemberTest(){
        Member member1 = createMember();
        Member member2 = createMember();
        memberService.saveMember(member1);
        Throwable e = assertThrows(IllegalStateException.class, () -> {
            memberService.saveMember(member2);});
        assertEquals("이미 가입된 회원입니다.", e.getMessage());
    }
}

테스트 클래스에 @Transactional 어노테이션을 선언할 경우, 테스트 실행 후 롤백 처리가 됩니다. 이를 통해 같은 메소드를 반복적으로 테스트할 수 있습니다.

assertThrows 메소드를 이용하면 예외 처리 테스트가 가능합니다. 첫 번째 파라미터에는 발생할 예외 타입을 넣어줍니다. 발생한 예외 메세지가 예상 결과와 맞는지 검증합니다.

✏️ 회원 가입 페이지 작성

MemberController.java

@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping(value = "/new")
    public String memberForm(Model model) {
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }

    @PostMapping(value = "/new")
    public String memberForm(MemberFormDto memberFormDto) {

        Member member = Member.createMember(memberFormDto, passwordEncoder);
        memberService.saveMember(member);

        return "redirect:/";
    }

}

✏️ 검증

의존성 추가

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

💡 빈 검증 어노테이션

어노테이션설명
@NotEmptyNull 체크 및 문자열의 경우 길이 0인지 검사
@NotBlankNull 체크 및 문자열의 경우 길이 0 및 빈 문자열(" ") 검사
@Length(min=, max=)최대, 최소 길이 검사
@Email이메일 형식인지 검사
@Max(숫자)지정한 값보다 작은지 검사
@Min(숫자)지정한 값보다 큰지 검사
@Null값이 NULL인지 검사
@NotNull값이 NULL이 아닌지 검사

유효성을 검증할 클래스의 필드에 어노테이션을 선언합니다.

MemberFormDto.java

@Getter
@Setter
public class MemberFormDto {

    @NotBlank(message = "이름은 필수 입력 값입니다.")
    private String name;

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

    @NotEmpty(message = "비밀번호는 필수 입력 값입니다.")
    @Length(min=8, max=16, message = "비밀번호는 8자 이상, 16자 이하로 입력해주세요")
    private String password;

    @NotEmpty(message = "주소는 필수 입력 값입니다.")
    private String address;
}

회원 가입이 성공하면 메인 페이지로 리다이렉트 시켜주고,
회원 정보 검증 및 중복회원 가입 조건에 의해 실패한다면 다시 회원 가입 페이지로 돌아가 실패 이유를 화면에 출력해 주겠습니다.

MemberController.java

    @PostMapping(value = "/new")
    public String newMember(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){

        if(bindingResult.hasErrors()){
            return "member/memberForm";
        }

        try {
            Member member = Member.createMember(memberFormDto, passwordEncoder);
            memberService.saveMember(member);
        } catch (IllegalStateException e){
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }

        return "redirect:/";
    }

검증하려는 객체의 앞에 @Valid 어노테이션을 선언하고, 파라미터로 bindingResult 객체를 추가합니다. 검사 후 bindingResult.hasErrors()를 호출하여 에러가 있다면 회원 가입 페이지로 이동합니다.

다음엔 로그인/로그아웃을 구현해보겠습니다~

좋은 웹페이지 즐겨찾기