ACHO.pk devlog

[Springboot] 스프링 시큐리티를 이용한 회원가입 본문

프레임워크/Springboot

[Springboot] 스프링 시큐리티를 이용한 회원가입

Acho 2023. 2. 23. 18:18

1. Spring Security (스프링 시큐리티) 란?

애플리케이션의 보안에서 중요한 2가지

  • 인증
  • 인가

웹에서 인증이란 해당 리소스에 대해서 작업을 수행할 수 있는 주체인지 확인하는 것이다. 예시로 블로그 글을 작성할 때 "로그인" 이라는 인증 절차를 거쳐야 한다.

인가는 인증 과정 이후에 일어나는 것으로 커뮤니티를 관리하는 관리자 페이지에 접근하는 URL을 입력했을 때 해당 URL은 커뮤니티의 관리자만 접근할 수 있어야한다. 인가된 유저라면 해당 URL에 대한 권한이 있기 때문에 접근이 가능하다. 

 

📚 스프링 시큐리티의 기능에는 무엇이 있을까?

사용자 권한에 따른 URL  접근 제어

  • 패스워드 부호화 
  • LDAP (Lightweight Directory Access Protocol) :  인터넷 프로토콜을 통해 분산 디렉토리 정보 서비스를 유지 관리히고 액세스하기 위한 개방형 애플리케이션 프로토콜.
  • 기본 엑세스 인증 :  네트워크를 통해 요청을 할 때 사용자 이름과 비밀번호를 제공하기 위해 사용되는 기본 액세스 인증을 지원함.
  • HTTP 허가 : Apche Ant 경로 또는 정규 표현을 사용하여 웹 요청 URL을 HTTP로 인가하기 위해 이 기능을 제공함.

등이 있다. 

아래 링크를 참고해보자 !

https://www.javatpoint.com/spring-security-features

 

Spring Security Features - javatpoint

Spring Security Features, Introduction, Features, Project Modules, XML Example, Java Example, Login Logout, Spring Boot, Spring Core, Spring with JPA, Spring with Hibernate, Spring with Struts, Spring MVC, Spring Integration etc.

www.javatpoint.com

 

 

 

2. security dependency 추가하기 

pom.xml에 security와 관련된 의존성을 추가해준다.

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

 

스프링 시큐리티를 추가했다면 이제 모든 요청은 인증을 필요로 한다. 

즉, 의존성을 추가하는 것만으로도 모든 요청에 인증을 요구한다는 것이다.

 

 

2-1. 로그인하기

그럼 이제 스프링 시큐리티 로그인을 해보자

특정 url에 접근하지 않아도 localhost:8080에 접근을 하면 스프링 시큐리티에서 제공하는 로그인 페이지로 이동한다.

스프링 시큐리티에서는 기본적으로 아이디와 비번을 제공한다. 아이디는 user이고, 비밀번호는 애플리케이션을 실행할 때마다 콘솔창에 출력해서 보여준다. 

sign in을 하게 되면 접속하고 싶은 url에 접근할 수 있게 된다. 

 

 

2-2. 로그아웃하기

스프링 시큐리티에서 로그아웃 기능도 제공한다. URL에 http://localhost:8080/logout 을 입력하면 로그아웃을 할지 물어보는 화면이 나타난다. 

로그아웃 후 서버에 요청을 하면 다시 인증을 요구한다.

 

 

2-3. 스프링 시큐리티 설정하기

SecurityConfig 소스를 작성한다. 현재는 모든 요청에 인증을 필요로 하지만 SecurityConfig.java의 configure 메소드에 설정을 추가하지 않으면 요청에 인증을 요구하지 않는다. 

package com.shop.shop.config;


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;


@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이 자동으로 포함된다. WebSecurityConfigurerAdapter를 상속받아서 메소드 오버라이딩을 통해 보안 설정을 커스터마이징할 수있다.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter

 

▹http 요청에 대한 보안을 설정한다. 페이지 권한 설정, 로그인 페이지 설정, 로그아웃 메소드 등에 대한 설정을 작성한다.

protected void configure(HttpSecurity http) throws Exception

 

▹비밀번호를 데이터베이스에 그대로 저장했을 경우, 데이터베이스가 해킹당하면 고객의 회원 정보가 그대로 노출되기 때문에 BCryptPasswordEncoder의 해시 함수를 이용하여 비밀번호를 암호화해서 저장한다. BCryptPasswordEncoder을 빈으로 등록하여 사용한다.

public PasswordEncoder passwordEncoder()

 

 

3. 회원가입 기능 구현하기

상품을 주문하려면 회원 가입을 해야 하며, 각 페이지마다 필요한 권한도 다르다.

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

 

3-1. Role.java 생성

각각의 멤버가 일반 유저인지 관리자인지 구분할 수 있는 역할이 있어야 한다 ROLE

이를 구분하기 위해서 constant > Role.java 코드를 작성한다.

package com.shop.shop.constant;

public enum Role {
    USER, ADMIN
}

 ▹ Role의 값으로 USER와 ADMIN 2개를 입력한다.

 

 

3-2. Dto 생성

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

package com.shop.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-3. Member Entity 생성

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

package com.shop.shop.entity;

import com.shop.shop.constant.Role;
import com.shop.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(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.ADMIN);
        return member;
    }
}

▹ 회원은 이메일 통해 유일하게 구분해야 하기 때문에, 동일한 값이 데이터베이스에 들어올 수 없도록 unique 속성을 지정한다.

@Column(unique = true)

 

▹ 자바의 enum 타입을 엔티티의 속성으로 지정할 수 있다. Enum을 사용할 때 기본적으로 순서가 저장되는데, enum의 순서가 바뀔 경우 문제가 발생할 수 있으므로 "EnumTypes.STRING" 옵션을 사용해서 String으로 저장하기를 권장한다.

@Enumerated(EnumType.STRING)

 

▹Member 엔티티를 생성하는 메소드로 Member 엔티티에 회원을 생성하는 메소드를 만들어서 관리를 한다면 코드가 변경되더라도 한 군데만 수정하면 되는 이점이 있다.

public static Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder)

 

▹스프링 시큐리티 설정 클래스에 등록한 BCryptPassWordEncoder Bean을 파라미터로 넘겨서 비밀번호를 암호화한다.

String password = passwordEncoder.encode(memberFormDto.getPassword());

 

 

3-4. Member Repository 생성

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

package com.shop.shop.repository;

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

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByEmail(String email);
}

▹ 회원가입 시 중복된 회원이 있는지 검사하기 위해서 이메일로 회원을 검사할 수 있도록 쿼리 메소드를 작성한다.

 

 

3-5. Member Service 생성

MemberService 클래스를 작성한다.

package com.shop.shop.service;

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

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

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

@Transactional

 📚 @Transactional의 개념

클래스 또는 메서드에 선언이 가능하다. 클래스에 선언하게 되면 모든 메소드에 적용되고, 특정 메소드에 선언시 선언된 메소드에서만 적용이 된다. 트랙잭션에 대한 전파, 격리, 시간 초과, 읽기 전용 및 롤백 조건을 설정할 수 있고, 트랜잭션 관리자를 지정할 수 있다.

 

📚 트랜잭션이란 ?

데이터베이스의 상태를 변경시키기 위해(insert, update, delete, insert와 같은 행동) 수행하는 작업 단위를 의미한다. 트랜잭션은 상황에 따라 여러 개가 만들어질 수 있으며,  commit(저장), rollback(철회) 될 수 있다. 

트랜잭션은 2개 이상의 쿼리를 하나의 커넥션으로 묶어 DB에 전송하고, 이 과정에서 에러가 발생할 경우 자동으로 모든 과정을 원래대로 되돌려놓는다.

  • commit : 트랜잭션 작업을 성공적으로 마치고 DB에 영구적으로 반영한다.
  • rollback : 오류가 발생하면 트랜잭션을 처리를 시작하기 이전의 상태로 되돌린다.

 

 

▹ final이나 @NonNull이 붙은 필드에 생성자를 생성해준다. 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록이 가능하다면 @AutoWired 어노테이션 없이 의존성 주입이 가능하다.

@RequiredArgsConstructor
private final MemberRepository memberRepository;

 

▹이미 가입된 회원의 경우 IllegalStateException 예외를 발생시킨다.

private void validateDuplicateMember(Member member)

 

 

3-6. 회원가입 기능 테스트코드

package com.shop.shop.service;


import com.shop.shop.dto.MemberFormDto;
import com.shop.shop.entity.Member;
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 javax.transaction.Transactional;

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

@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 어노테이션을 선언할 경우, 테스트 실행 후 롤백 처리가 된다. 이를 통해 같은 메소드를 반복적으로 테스트할 수 있다.

@Transactional

 

▹ 회원 정보를 입력한 Member 엔티티를 만드는 메소드를 작성한다.

public Member createMember()

 

▹Junit의 Assertions 클래스의 assertEquals 메소드를 이용하여 저장하려고 요청했던 값과 실제 저장된 데이터를 비교한다. 첫 번째 파라미터에는 기대 값, 두 번째 파라미터에는 실제로 저장된 값을 넣어준다.

public void saveMemberTest()

 

▹Junit의 Assertions 클래스의 assertEquals 메소드를 이용하면 예외 처리 테스트가 가능하다. 첫 번째 파라미터에는 발생할 예외 타입을 넣어준다.

Throwable e = assertThrows(IllegalStateException.class, () -> {
    memberService.saveMember(member2);});

 

▹발생한 예외 메시지가 예상 결과와 맞는지 검증한다.

assertEquals("이미 가입된 회원입니다.", e.getMessage());

정상 작동

여기까지 회원 가입 로직을 완성했다 !

 

 

3-6. Member Controller 생성

회원 가입을 위한 페이지를 만들기 위해 Controller 패키지 아래에 MemberController를 만들어준다.

package com.shop.shop.controller;

import com.shop.shop.dto.MemberFormDto;
import com.shop.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.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";
    }
}

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

public String memberForm(Model model)

 

 

이전 글인 Thymeleaf 글에서 사용했던 부트스트랩을 이욯한다.

templates > member > memberForm.html

<!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>

▹회원가입 실패시 에러 메시지를 경고창을 이용해서 보여준다.

$(document).ready(function(){
    var errorMessage = [[${errorMessage}]];
    if(errorMessage != null){
        alert(errorMessage);
    }
});

 

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

th:value="${_csrf.token}"

 

📚 CSRF 

특정 사용자를 대상으로 하지 않고, 불특정 다수를 대상으로 로그인된 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록, 송금 등)를 하게 만드는 공격이다.

 

 

MemberController

package com.shop.shop.controller;

import com.shop.shop.dto.MemberFormDto;
import com.shop.shop.entity.Member;
import com.shop.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;

@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 를 생성한다.

package com.shop.shop.controller;

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

@Controller
@RequiredArgsConstructor
public class MainController {

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

}

 

 

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>

회원가입 페이지

 

3-7. 회원가입시 서버로 넘어오는 값 검증하기

pom.xml에 의존성을 추가해준다.

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

 

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

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

 

 

MemberController

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

package com.shop.shop.controller;

import com.shop.shop.dto.MemberFormDto;
import com.shop.shop.entity.Member;
import com.shop.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 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에 담아준다. bindingResult.hasErrors()를 호출하여 에러가 있다면 회원 가입 페이지로 이동한다.

public String newMember(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){

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

 

▹회원가입 시 중복 회원 가입 예외가 발생하면 에러 메시지를 뷰로 전달한다.

model.addAttribute("errorMessage", e.getMessage());

 

 

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

 

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

 

📌 비밀번호는 암호화해서 잘 들어가는데 "Role"부분이 이상하게 ADMIN으로 된다.

이유는 찾아보고 정리해야겠다.

 

 

Comments