스프링 시큐리티를 이용한 회원가입 기능 구현하기

개발자·2022년 1월 13일
0
post-thumbnail

각각의 멤버는 일반 유저인지, 아니면 관리자인지 구분할 수 있는 역할이 있어야 한다. 이를 구분하기 위해서 이와 관련된 코드를 작성해야 한다.

public enum Role {
	USER,ADMIN
}
  1. Role의 값으로 USER와 ADMIN 2개를 설정한다.

이후 회원 가입 화면으로부터 넘어오는 가입정보를 담은 DTO를 생성해야 한다.

package com.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;
}

이제 회원 정보를 저장하는 Member 엔티티를 만들어야 한다. 관리할 회원의 정보는 이름, 이메일, 비밀번호, 주소, 역할이다.

package com.shop.entity;

import com.shop.constant.Role;
import com.shop.dto.MemberFormDto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.*;

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

    @Id
    @Column
    @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(member.getName());
        member.setEmail(member.getEmail());
        member.setAddress(member.getAddress());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        member.setPassword(password);
        member.setRole(Role.USER);
        return member;
    }

}
  1. 회원은 이메일을 통해 유일하게 구분해야 하기 때문에, 동일한 값이 데이터베이스에 들어올 수 없도록 unique 속성을 지정한다.
  2. 자바의 enum 타입을 엔티티의 속성으로 지정할 수 있다. Enum을 사용할 때 기본적으로 순서가 지정되는데, enum의 순서가 바뀔 경우 문제가 발생할 수 있으므로 “EnumType.STRING” 옵션을 사용해서 String으로 저장하기를 권장한다.
  3. Member 엔티티를 생성하는 메소드이다. Member 엔티티에 회원을 생성하는 메소드를 만들어서 관리를 한다면 코드가 변경되더라도 한 군데만 수정하면 되는 이점이 있다.
  4. 스프링 시큐리티 설정 클래스에 등록한 BCryptPasswordEncoder Bean을 파라미터로 넘겨서 비밀번호를 암호화한다.

Member 엔티티를 데이터베이스에 저장할 수 있도록 MemberRepository를 만든다.

package com.shop.repository;

import com.shop.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByEmail(String email);
}
  1. 회원 가입 시 중복되는 회원이 있는지 검사하기 위해서 이메일로 회원을 검사할 수 있도록 쿼리 메소드를 작성한다.

service 패키지를 만들고 MemberService 클래스를 작성한다.

package com.shop.service;

import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@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("이미 가입된 회원입니다.");
        }
    }
}
  1. 비즈니스 로직을 담당하는 서비스 계층 클래스에 @Transactional 어노테이션을 선언한다. 로직을 처리하다가 에러가 발생한다면, 변경된 데이터를 로직 수행이전의 상태로 콜백시켜주는 역할을 한다.
  2. 빈을 주입하는 방법으로는 @Autowired 어노테이션을 이용하거나, 필드 주입(Setter 주입), 생성자 주입을 이용하는 방법이 있다. @RequiredArgsConstructor 어노테이션은 final이나 @NonNull이 붙은 필드에 생성자를 생성해준다. 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록이 가능하다면 @Autowired 어노테이션 없이 의존성 주입이 가능하다.
  3. 이미 가입된 회원의 경우 IllegalStateException 예외를 발생시킨다.

회원가입 기능이 정상적으로 동작하는지 테스트 코드를 작성해 검증해보도록 하겠다.

package com.shop.service;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.assertEquals;

@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@gmail.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());
    }
}
  1. 테스트 클래스에 @Transactional 어노테이션을 선언할 경우, 테스트 실행 후 롤백 처리가 가능하게 된다. 이를 통해 같은 메소드를 반복적으로 테 스트 할 수 있다.
  2. 회원 정보를 입력한 Member 엔티티를 만드는 메소드를 작성한다.
  3. JUnit의 Assertions 클래스의 assertEquals 메소드를 이용해 저장하려고 요청했던 값과 실제 저장된 데이터를 비교한다. 첫 번째 파라미터에는 기대 값, 두 번째 파라미터에는 실제로 저장된 값을 넣어준다.

다음으로 검증해 볼 내용은 중복된 이메일로 회원 가입을 시도할 경우 “이미 가입된 회원입니다.”라는 에러 메시지를 정상적으로 출력해주는지 테스트 코드를 작성하겠다.

@Test
@DisplayName("중복 회원 가입 테스트")
public void saveDuplicateMember() {
    Member member1 = createMember();
    Member member2 = createMember();
    memberService.saveMember(member1);

    Throwable e =assertThrows(IllegalStateException.class, () ->{
        memberService.saveMember(member2);
    });
assertEquals("이미 가입된 회원입니다.", e.getMessage());

}
  1. JUnit의 Assertions 클래스의 assertThrows 메소드를 이용하면 예외 처리 테스트가 가능하다. 첫 번째 파라미터에는 발생할 예외 타입을 넣어준다.
  2. 발생한 예외 메시지가 예상 결과와 맞는지 검증한다.

중복 회원 가입 테스트를 실행하면 예상한 예외가 발생하고, 테스트를 통과하는 것을 볼 수 있다. 회원 가입 로직이 변경되더라도 작성해둔 테스트를 실행하여 빠르게 테스트 및 검증이 가능하다.

회원 가입 로직을 완성했으므로 이제 회원 가입을 위한 페이지를 만들겠다. Controller 패키지 아래 MemberController 클래스를 만들어보자.

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/members")
@Controller
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

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

}
  1. 회원 가입 페이지로 이동할 수 있도록 MemberController 클래스에 메소드를 작성한다.

회원가입 페이지도 이전 Thymeleaf에서 사용했던 부트스트랩을 사용하겠다. 홈페이지의 예제 Forms에 나와있는 코드를 변형하여 사용한다. 홈페이지를 참고하면 여러가지 예시 코드와 결과 화면을 볼 수 있다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .fieldError {
            color: #bd2130;
        }
    </style>
</th:block>

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">

    <script th:inline="javascript">
        $(document).ready(function(){
            var errorMessage = [[${errorMessage}]];
            if(errorMessage != null){
                alert(errorMessage);
            }
        });
    </script>

</th:block>

<div layout:fragment="content">

    <form action="/members/new" role="form" method="post"  th:object="${memberFormDto}">
        <div class="form-group">
            <label th:for="name">이름</label>
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력해주세요">
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect data</p>
        </div>
        <div class="form-group">
            <label th:for="email">이메일주소</label>
            <input type="email" th:field="*{email}" class="form-control" placeholder="이메일을 입력해주세요">
            <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect data</p>
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" th:field="*{password}" class="form-control" placeholder="비밀번호 입력">
            <p th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="fieldError">Incorrect data</p>
        </div>
        <div class="form-group">
            <label th:for="address">주소</label>
            <input type="text" th:field="*{address}" class="form-control" placeholder="주소를 입력해주세요">
            <p th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="fieldError">Incorrect data</p>
        </div>
        <div style="text-align: center">
            <button type="submit" class="btn btn-primary" style="">Submit</button>
        </div>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    </form>

</div>

</html>
  1. 스프링 시큐리티를 사용할 경우 기본적으로 CSRF(Cross Site Request Forgery)를 방어하기 위해 모든 POST 방식의 데이터 전송에는 CSRF 토큰 값이 있어야 한다. CSRF 토큰은 실제 서버에서 허용한 요청이 맞는지 확인하기 위한 토큰이다. 사용자의 세션에 임의의 값을 저장하여 요청마다 그 값을 포함하여 전송하면 서버에서 세션에 저장된 값과 요청이 온 값이 일치하는지 확인하여 CSRF를 방어한다.

CSRF란 사이트간 위조 요청으로 사용자가 자신의 의지와 상관없이 해커가 의도한대로 수정, 등록, 삭제 등의 행위를 웹사이트 요청하게 하는 공격을 말한다.

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@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:/";
    }
}

회원가입 후 메인 페이지로 갈 수 있도록 MainController.java 소스를 하나 작성하겠다.

package com.shop.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {

    @GetMapping(value = "/")
    public String main() {
        return "main";
    }
}

resources/templates 폴더 아래 main.html 파일을 하나 생성한다. 메인 페이지는 추후 등록된 상품의 목록을 보여주도록 수정하겠다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<div layout:fragment="content">

    <h1>메인페이지입니다.</h1>

</div>

웹 브라우저에 “localhost/members/new” URL을 입력하면 회원 가입 페이지로 이동하는 것을 볼 수 있다. 회원 가입 등록을 위해 정보를 입력하고 버튼을 누르면 회원가입이 되면서 메인 페이지로 화면이 이동한다. 하지만 현재 상태에서는 이름이나 비밀번호를 입력하지 않아도 정상적으로 저장된다.

회원가입 페이지에서 서버로 넘어오는 값을 검증하기 위해서 pom.xml에 “spring.boot-stater-validation”을 추가하겠다.

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

유효한 값인지 판단하는 소스가 여러 군데 흩어지면 관리가 힘들다. 자바 빈 밸리데이션을 이용하면 객체의 값을 효율적으로 검증할 수 있다. 빈 검증 어노테이션을 몇가지 살펴보자.

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

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

package com.shop.dto;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

@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;
}

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

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@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:/";
    }

    @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);
        } catch (IllegalStateException e) {
            model.addAttribute("errorMessage", e.getMessage());
            return "member/memberForm";
        }

        return "redirect:/";
    }
}
  1. 검증하려는 객체의 앞에 @Valid 어노테이션을 선언하고, 파라미터로 bindingResult 객체를 추가한다. 검사 후 결과는 bindingResult에 담아준다. bindingResult.hasErrors()를 호출하여 에러가 있으면 회원가입 페이지로 이동한다.
  2. 회원가입 시 중복 회원 가입 예외가 발생하면 에러 메시지를 뷰로 전달한다.

유효하지 않은 회원 가입 정보를 입력 후 서버로 전송하면 해당 이유를 화면에서 보여준다.

회원가입이 정상적으로 이루어졌다면 메인페이지로 이동한다.

메인페이지로 이동한 것만 봐서는 실제로 데이터베이스에 데이터가 저장됐는지 궁금할 수 있다. 이전 내용에서 설치했던 MySQL Wrokbench tool을 실행시켜 로컬 데이터베이스에 접속한다. “shop” 데이터베이스를 선택 후 member 테이블을 조회하면 가입할 때 입력한 이메일 주소가 데이터에 추가된 것을 확인할 수 있다. 비밀번호도 입력한 값이 아닌 암호화되어 저장된다.

package com.shop.service;

import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    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("이미 가입된 회원입니다.");
        }
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException{
        Member member = memberRepository.findByEmail(email);

        if (member == null) {
            throw new UsernameNotFoundException(email);
        }
        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }
}
  1. MemberService가 UserDetailService를 구현한다.
  2. UserDetailsService 인터페이스의 loadUserByUsername() 메소드를 오버라이딩한다. 로그인할 유저의 email을 파라미터로 전달받는다.
  3. UserDetail을 구현하고 있는 User 객체를 반환해준다. User 객체를 생성하기 위해서 생성자로 회원의 이메일, 비밀번호, role을 파라미터로 넘겨준다.
package com.shop.config;

import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MemberService memberService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/members/login")
                .defaultSuccessUrl("/")
                .usernameParameter("email")
                .failureUrl("/members/login/error")
                .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
                .logoutSuccessUrl("/")
        ;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

이제 로그인 페이지를 만들도록 하겠다. 로그인 페이지에서는 회원의 아이디와 비밀번호를 입력하는 입력란과 회원가입을 하지 않았을 경우 회원가입 페이지로 이동할 수 있는 버튼을 만들겠다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        .error {
            color: #bd2130;
        }
    </style>
</th:block>

<div layout:fragment="content">

    <form role="form" method="post" action="/members/login">
        <div class="form-group">
            <label th:for="email">이메일주소</label>
            <input type="email" name="email" class="form-control" placeholder="이메일을 입력해주세요">
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" name="password" id="password" class="form-control" placeholder="비밀번호 입력">
        </div>
        <p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
        <button class="btn btn-primary">로그인</button>
        <button type="button" class="btn btn-primary" onClick="location.href='/members/new'">회원가입</button>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    </form>

</div>

</html>

로그인 페이지를 만들었으니까 이동할 수 있도록 MemberController에 로직을 구현하겠따. 또한 로그인 실패 시 “아이디 또는 비밀번호를 확인해주세요”라는 메세지를 담아서 로그인 페이지로 보내겠다.

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.shop.entity.Member;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.validation.BindingResult;
import javax.validation.Valid;

@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 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:/";
    }

    @GetMapping(value = "/login")
    public String loginMember() {
        return "/member/memberLoginForm";
    }

    @GetMapping(value = "/login/error")
    public String loginError(Model model) {
        model.addAttribute("loginErrorMsg", "아이디 또는 비밀번호를 확인해주세요");
        return "/member/memberLoginForm";
    }
}

드디어 로그인/로그아웃 기능이 구현 완료되었다. 회원가입 후 로그인 페이지로 이동하여 아이디와 비밀번호를 입력 후 <로그인> 버튼을 클릭한다. 현재 애플리케이션 재실행 시 데이터베이스의 테이블을 삭제 후 재생성하므로, 회원가입을 진행한 후 로그인을 해야한다. 로그인 페이지 경로는 네비게이션 바에 미리 입력해두었으니 로그인 메뉴를 클릭하면 아이디와 비밀번호를 입력하는 페이지로 이동한다.

해당 게시글은 변구훈, 『스프링 부트 쇼핑몰 프로젝트 with JPA』, 로드북, 2021를 참고하여 작성하였습니다.

profile
I DEVELOP THEREFORE, I AM 😄

0개의 댓글