포토폴리오/Spring-Boot,JPA - 쇼핑몰사이트 v2

#2. 스프링 시큐리티를 활용한 로그인/회원가입 (1)

샐님 2023. 8. 3. 00:15
728x90
반응형

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>
728x90
반응형