의존성을 추가해보겠습니다.
<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이 자동으로 포함됩니다.
메소드 오버라이딩을 통해 보안 설정을 커스터마이징할 수 있습니다.
비밀번호를 데이터베이스에 그대로 저장하면 안되기 때문에 해시 함수를 이용해서 비밀번호를 암호화하여 저장합니다.
멤버가 일반 유저인지, 관리자인지 구분할 수 있는 역할이 있어야합니다.
public enum Role {
USER, ADMIN
}
가입정보를 담을 dto를 생성해보겠습니당.
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MemberFormDto {
private String name;
private String email;
private String password;
private String address;
}
회원 정보를 저장하는 Member 엔티티를 마들어보겠습니다.
@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 클래스를 작성해보겠습니다.
@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("test@email.com");
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 메소드를 이용하면 예외 처리 테스트가 가능합니다. 첫 번째 파라미터에는 발생할 예외 타입을 넣어줍니다. 발생한 예외 메세지가 예상 결과와 맞는지 검증합니다.
@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>
어노테이션 | 설명 |
---|---|
@NotEmpty | Null 체크 및 문자열의 경우 길이 0인지 검사 |
@NotBlank | Null 체크 및 문자열의 경우 길이 0 및 빈 문자열(" ") 검사 |
@Length(min=, max=) | 최대, 최소 길이 검사 |
이메일 형식인지 검사 | |
@Max(숫자) | 지정한 값보다 작은지 검사 |
@Min(숫자) | 지정한 값보다 큰지 검사 |
@Null | 값이 NULL인지 검사 |
@NotNull | 값이 NULL이 아닌지 검사 |
유효성을 검증할 클래스의 필드에 어노테이션을 선언합니다.
@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;
}
회원 가입이 성공하면 메인 페이지로 리다이렉트 시켜주고,
회원 정보 검증 및 중복회원 가입 조건에 의해 실패한다면 다시 회원 가입 페이지로 돌아가 실패 이유를 화면에 출력해 주겠습니다.
@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()를 호출하여 에러가 있다면 회원 가입 페이지로 이동합니다.
다음엔 로그인/로그아웃을 구현해보겠습니다~