#2. 스프링 시큐리티를 활용한 로그인/회원가입 (1)
1. 스프링 시큐리티 설정
- config 패키지 생성 -> SecurityConfig.java 생성
package won.shop.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
// 추후 여기서 스프링 시큐리티 설정 관련 정보를 작성한다.
@Bean
public PasswordEncoder passwordEncoder(){
// 해수함수를 이용하여 비밀번호를 암호화하여 저장.
return new BCryptPasswordEncoder();
}
}
2. 회원가입 기능 구현
1) 회원 또는 관리자 역할 구분하는 enum 작성
- constant 패키지 생성 -> Role.java 작성
package won.shop.constant;
public enum Role {
USER,ADMIN
}
2) 가입정보 DTO 생성
- 정의 : 회원가입 화면에서 넘어온 가입정보를 담는 객체
- dto 패키지 생성 -> MemberFormDto.java
package won.shop.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class MemberFormDto { // 회원가입화면에서 넘어논 가입정보를 담는 객체
private String name;
private String email;
private String password;
private String address;
}
3) 회원 정보 저장 Member 엔티티 생성
- domain 패키지 생성 -> Member.java 생성
* MemberFormDto에 성별을 칼럼 추가
package won.shop.domain;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;
import won.shop.constant.Gender;
import won.shop.constant.Role;
import won.shop.dto.MemberFormDto;
@Entity
@Getter
@Setter
@ToString
public class Member {
@Id
@Column(name="member_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
private String password;
@Column(unique = true)
private String email;
private String address;
@Enumerated(EnumType.STRING)
@Builder.Default
private Gender gender = Gender.MAN;
@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());
member.setPassword(passwordEncoder.encode(memberFormDto.getPassword()));
member.setRole(Role.USER); // 가입시 role은 무조건 회원으로 저장
member.setGender(memberFormDto.getGender());
return member;
}
}
4) Member 데이터를 데이터베이스에 저장할 수 있도록 MemberRepsitory 생성
- repository / service 패키지 생성 -> 각각에 MemberRepository.java(디비에 정보를 저장), MemberService.java(비지니스 로직 담당)생성
- MemberRepository 인터페이스
package won.shop.Repository;
import org.springframework.data.jpa.repository.JpaRepository;
import won.shop.domain.Member;
public interface MemberRepository extends JpaRepository<Member,Long> {
Member findByEmail(String email); // 회원가입시 중복된 이메일 체크
}
- MemberService
package won.shop.Service;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import won.shop.Repository.MemberRepository;
import won.shop.domain.Member;
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member saveMember(Member member){
validateDubplicateMember(member);
return memberRepository.save(member);
}
private void validateDubplicateMember(Member member) {
Member findMember = memberRepository.findByEmail(member.getEmail());
if(findMember != null){
throw new IllegalStateException("이미 가입된 회원입니다.");
// IllegalStateException : 메세지가 허가되지 않거나 부적절한 argment를 받았을때 예외를 발생
}
}
}
5) 회원가입 테스트
상황1 : 정상적으로 회원가입 수행
package won.shop.Service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import won.shop.Repository.MemberRepository;
import won.shop.domain.Member;
import won.shop.dto.MemberFormDto;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional // 테스트시 롤백 수행
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
PasswordEncoder passwordEncoder;
public Member createMember(){
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail("test@gmail.com");
memberFormDto.setName("홍길동");
memberFormDto.setAddress("부산시 행복동");
memberFormDto.setPassword("1234");
return Member.CreateMember(memberFormDto,passwordEncoder);
}
@Test
@DisplayName("회원가입 테스트")
@Rollback(value = false)
public void saveMemberTest(){
Member member = createMember(); // 회원 테스트 데이터 생성
Member savedmember = memberService.saveMember(member);
// assertEquals (기댓값, 실제 저장된값)
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());
assertEquals(member.getGender(),savedmember.getGender());
}
}
데이터베이스에도 값을 확인하고 싶어서 rollback = False 로 설정
* 공부해야할 것
gender 칼럼에 기본값을 설정해도 null 이 들어온다..
@Enumerated(EnumType.STRING)
//@ColumnDefault // 디비에 저장되는 시점에 값이 없을경우 데이터를 초기화 함. VS @Builder.Default : 객체를 생성하는 시점에서 값을 초기화해준다.
@Builder.Default
private Gender gender = Gender.MAN;
이렇다고하는데 일단은 회원가입시 새로운 폼을 생성할때 기본값을 셋팅해주는 반향으로 작업해야겠다.
상황2 : 예외 발생
@Test
@DisplayName("중복 회원가입 테스트")
public void duplicatedMememberSavedTest(){
Member member1 = createMember(); // 회원 테스트 데이터 생성
Member member2 = createMember(); // 회원 테스트 데이터 생성
Member savedmember1 = memberService.saveMember(member1);
Throwable e = assertThrows(IllegalStateException.class,()->{
memberService.saveMember(member2);
});
assertEquals("이미 가입된 회원입니다.",e.getMessage());
}
6) 회원 가입 페이지 작성
- controller 패키지 생성 -> MemberController.java 생성
package won.shop.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import won.shop.Service.MemberService;
import won.shop.constant.Gender;
import won.shop.domain.Member;
import won.shop.dto.MemberFormDto;
@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final PasswordEncoder PasswordEncoder;
@ModelAttribute("gender")
public Gender[] GetGender() {
return Gender.values();
}
@GetMapping("/new")
public String memberForm(@ModelAttribute MemberFormDto memberFormDto){
// model.addAttribute("memberFormDto",new MemberFormDto());
return "/member/memberForm";
}
@PostMapping("/new")
public String memberFromSave(@Validated MemberFormDto memberFormDto,
BindingResult bindingResult, Model model){
if(bindingResult.hasErrors()){
return "/member/memberForm";
}
try{
Member newMember = Member.CreateMember(memberFormDto, PasswordEncoder);
memberService.saveMember(newMember);
}catch (Exception e){
model.addAttribute("errorMessage",e.getMessage());
return "/member/memberForm";
}
return "redirect:/"; // 회원 가입 후 메인 화면으로 이동
}
}
7) 회원가입 화면
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/layout/_blank_layout.html}">
<div layout:fragment="content" class="container">
<div class="row py-5 m-auto" style="max-width: 80%;">
<div class="text-center p-3">
<h2 class="m-auto" th:text="#{join}" ></h2>
</div>
<form class="col-md-9 m-auto" th:action method="post" th:object="${memberFormDto}" role="form">
<div class="row">
<div class="form-group col-md-4 mb-3">
<label for="email" th:text="#{mem.email}">Email</label>
<input type="email" class="form-control mt-1" id="email" th:field="*{email}"
th:errorclass="field-error" placeholder="Email">
<div class="field-error" th:errors="*{email}"></div>
</div>
<div class="form-group col-md-6 mb-3">
<label for="password" th:text="#{mem.password}"></label>
<input type="password" class="form-control mt-1 password" id="password" th:field="*{password}"
th:errorclass="field-error" placeholder="Password">
<div class="field-error" th:errors="*{password}"></div>
</div>
<div class="form-group col-md-6 mb-3">
<label for="name" th:text="#{mem.name}">Name</label>
<input type="text" class="form-control mt-1" id="name" th:field="*{name}"
th:errorclass="field-error" placeholder="Name">
<div class="field-error" th:errors="*{name}"></div>
</div>
</div>
<div class="mb-3">
<label for="contact" th:text="주소"></label>
<input type="text" class="form-control mt-1" id="address" th:field="*{address}"
th:errorclass="field-error" placeholder="Contact">
<div class="field-error" th:errors="*{address}"></div>
</div>
<div class="mb-3">
<label for="contact" th:text="#{mem.contact}"></label>
<input type="text" class="form-control mt-1" id="contact" name="contact" th:field="*{contact}"
th:errorclass="field-error" placeholder="Contact">
<div class="field-error" th:errors="*{contact}"></div>
</div>
<div class="mb-3">
<label th:text="#{mem.memtype}"></label>
<div class="d-flex my-3">
<th:block th:each="gen,stat : ${gender}">
<div class="form-check me-3">
<input class="form-radio-input" type="radio" name="gender" th:field="*{gender}" th:value="${gen.key}">
<label class="form-radio-label" th:for="${#ids.prev('gender')}" th:text="${gen.name}">
Default radio
</label>
</div>
</th:block>
</div>
</div>
<div class="row">
<div class="col text-end mt-4">
<button type="submit" class="btn btn-success btn-lg px-3" th:text="#{join}" ></button>
<button type="button" class="btn btn-light btn-lg px-3" th:text="#{button.cancel}" th:onclick="|location.href='@{/}'|"></button>
</div>
</div>
</form>
</div>
<script th:inline="javascript">
</script>
</div>
8) 회원가입 후 메인화면으로 갈 수 있게 MainContorller.java 와 main.html 작성
package won.shop;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class MainController {
@GetMapping("/")
public String main(){
return "/main";
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
메인화면입니다.
</body>
</html>