๐Ÿ’ท๋ถ€ํŠธ์ŠคํŠธ๋žฉ์„ ์ด์šฉํ•œ ์›น๊ณผ Spring Security์™€ Validation์„ ์ ์šฉํ•œ ๐Ÿ’ทํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ

gdhiยท2023๋…„ 12์›” 6์ผ
post-thumbnail

๐Ÿ’ท๋ถ€ํŠธ์ŠคํŠธ๋žฉ ์‡ผํ•‘๋ชฐ ๋งŒ๋“ค๊ธฐ

์›นํŽ˜์ด์ง€์˜ ๋””์ž์ธ ๋ฐ ์›น ํผ๋ธ”๋ฆฌ์‹ฑ ๐Ÿ‘‰ ์Šคํ”„๋ง ๊ตฌํ˜„ ํ•  ๋•Œ ํž˜๋“ ์ ์„ ํŠธ์œ„ํ„ฐ์—์„œ ๋งŒ๋“  ์˜คํ”ˆ์†Œ์Šค ๋ถ€ํŠธ์ŠคํŠธ๋žฉ์„ ํ†ตํ•ด ์ˆ˜๋น„๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ถ€ํŠธ์ŠคํŠธ๋žฉ CDN ๐Ÿ‘‰ ๋น ๋ฅด๊ฒŒ ์„œ๋น„์Šค ์ œ๊ณต, ๋‹ค์šด๋กœ๋“œ๊ฐ€ ํ•„์š” ์—†๋‹ค

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>

Jquery CDN


๐Ÿ“Œ์ถ”๊ฐ€ํ•ด๋ณด๊ธฐ

๋”ฐ๋ผ์น˜์ง€๋ง๊ณ  ์ง์ ‘ ์ฐพ์•„๋ณด๊ณ  ๊ฒ€์ƒ‰ํ•˜๋ฉฐ ์–ด๋–ค ์‹์œผ๋กœ ์ ์šฉ ๋˜๋Š”์ง€ ๋ณด๋ฉด์„œ ํ•˜์ž


๐Ÿ“layout1.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" lang="ko">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <th:block layout:fragment = "script"></th:block>
    <th:block layout:fragment = "css"></th:block>

    <!-- CSS only -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">

    <!-- JS, Popper.js and Jquery -->
    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js" integrity="sha384-BBtl+eGJRgqQAUMxJ7pMwbEyER4l1g+O15P+16Ep7Q9Q+zqX6gSbd85u4mG4QzX+" crossorigin="anonymous"></script>
    
</head>
<body>
    <div th:replace = "~{fragments/header::header}"></div>

    <div layout:fragment = "content" class = "content"></div>

    <div th:replace = "~{fragments/footer::footer}"></div>
</body>
</html>



๐Ÿ“header.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div th:fragment = "header">
    <nav class = "navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
        <div class="container-fluid">
            <button class="navbar-toggler" type="button"
                    data-bs-toggle="collapse"
                    data-bs-target="#navbarTogglerDemo03"
                    aria-controls="navbarTogglerDemo03"
                    aria-expanded="false"
                    aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <a class="navbar-brand" href="/">Shop</a>

            <div class="collapse navbar-collapse" id="navbarTogglerDemo03">
                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                    <li class="nav-item">
                        <a class="nav-link" href="/admin/item/new">์ƒํ’ˆ ๋“ฑ๋ก</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/admin/items">์ƒํ’ˆ ๊ด€๋ฆฌ</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/cart">์žฅ๋ฐ”๊ตฌ๋‹ˆ</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/orders">๊ตฌ๋งค์ด๋ ฅ</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/members/login">๋กœ๊ทธ์ธ</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/members/logout">๋กœ๊ทธ์•„์›ƒ</a>
                    </li>
                </ul>
                <form class="d-flex" th:action="@{/}" method="get">
                    <input name="searchQuery" class="form-control me-2" type="search"
                           placeholder="Search" aria-label="Search">
                    <button class="btn btn-outline-light my-2 my-sm-0" type="submit">Search</button>
                </form>
            </div>
        </div>
    </nav>
</div>

</body>
</html>



๐Ÿ“footer.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div class = "footer" th:fragment = "footer">
        <footer class="page-footer font-small cyan darken-3">
            <div class="footer-copyright text-center py-3">
                2024 Shopping Mall TheJ WebSite
            </div>
        </footer>
    </div>
</body>
</html>



๐Ÿ“๊ฒฐ๊ณผ


๐Ÿ‘‰ ๊ธฐ๋ณธ ํŽ˜์ด์ง€

๐Ÿ‘‰ ํ™•๋Œ€ ์‹œ์— ํ† ๊ธ€ ๊ฐ€๋Šฅ ๐Ÿ”ฅ๋ถ€ํŠธ์ŠคํŠธ๋žฉ์œผ๋กœ ๋ฐ˜์‘ ํ˜• ์›น ์ž๋™์œผ๋กœ!



๐Ÿ“ŒCSS ์ ์šฉํ•˜๊ธฐ

๐Ÿ“layout1.css

html{
    position: relative;
    min-height: 100%;
    margin: 0;
}
body{
    min-height: 100%;
}
.footer{
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    padding: 15px 0;
    text-align: center;
}
.content{
    margin-bottom: 100px;
    margin-top: 50px;
    margin-left: 200px;
    margin-right: 200px;
}        



๐Ÿ“layout1.html

...

    <!-- CSS only -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
    <link th:href="@{/css/layout1.css}" rel="stylesheet"> ๐Ÿ‘‰ ์ถ”๊ฐ€

    <!-- JS, Popper.js and Jquery -->
    ...



๐Ÿ“๊ฒฐ๊ณผ









๐Ÿ’ทSpring Security ์ ์šฉํ•˜๊ธฐ ( + ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ)

Spring ๊ธฐ๋ฐ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณด์•ˆ(์ธ์ฆ๊ณผ ์ธ๊ฐ€ ๋“ฑ)์„ ๋‹ด๋‹นํ•˜๋Š” ์Šคํ”„๋ง ํ•˜์œ„ ํ”„๋ ˆ์ž„์›Œํฌ
๐Ÿ‘‰ ์ธ์ฆ/์ธ๊ฐ€๋กœ ์ตœ์†Œํ•œ์œผ๋กœ ํ•ด๋ณผ ๊ฒƒ. ๊ทธ ์ด์ƒ์€ ์ž์„ธํžˆ ๊ณต๋ถ€๋ฅผ ํ•ด์•ผํ•œ๋‹ค.

์‡ผํ•‘๋ชฐ ์—์„œ ์ธ์ฆ/์ธ๊ฐ€

  • ์ธ์ฆ์ด ํ•„์š” ์—†๋Š” ๊ฒฝ์šฐ : ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€ ์กฐํšŒ
  • ์ธ์ฆ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ : ์ƒํ’ˆ ์ฃผ๋ฌธ
  • ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ : ์ƒํ’ˆ ๋“ฑ๋ก

์ตœ์‹  3 ๋ฒ„์ „์œผ๋กœ !

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

๐Ÿ‘‰ ์˜์กด์„ฑ ์ถ”๊ฐ€

๐Ÿ‘‰
id : user
pw : d4604623-b7de-4a15-9860-b41b7e733282, ์ผํšŒ์šฉ

๐Ÿ‘‰ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ ์šฉ.


๐Ÿ“Œconfig ํŒจํ‚ค์ง€ ์ƒ์„ฑ


๐Ÿ“SecurityConfig ์„ค์ • ํด๋ž˜์Šค ์ƒ์„ฑ

@Configuration
@EnableWebSecurity

์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”



๐Ÿ“Role ์—ญํ•  Enum ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.constant;

public enum Role {
    USER, ADMIN
}



๐Ÿ“MemberFormDto ํšŒ์›๊ฐ€์ž… DTO ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.dto;

import lombok.Getter;
import lombok.Setter;

// ํšŒ์›๊ฐ€์ž… Dto
@Getter
@Setter
public class MemberFormDto {
    private String name;
    private String email;
    private String password;
    private String address;
}



๐Ÿ“Member entity ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.entity;


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

@Entity
@Table (name = "member")
@Getter
@Setter
@ToString
public class Member {
    @Id
    @Column(name = "member_id")
    @GeneratedValue(strategy = GenerationType.AUTO) // autoincrement
    private Long id;
    private  String name;

    @Column (unique = true) // ์ค‘๋ณต X
    private String email;

    private String password;
    private String address;
    private String telNumber;

    @Enumerated(EnumType.STRING)
    private Role role;

    // Member๋ฅผ DB ๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ๋ฐฉ์‹
    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.setTelNumber(memberFormDto.getTelNumber());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        member.setPassword(password);
        member.setRole(Role.ADMIN);

        return 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> {

    // ์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ
    // findBy ๋Š” ์นด๋ฉœ ํ‘œ๊ธฐ, ๊ทธ๋ž˜์„œ Email
    Member findByEmail(String email); // select email, UNIQUE ์ด๋ฏ€๋กœ NULL ์•„๋‹ˆ๋ฉด 1
}



๐Ÿ“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
// final ๋˜๋Š” @NonNull ๋ช…๋ น์–ด๊ฐ€ ๋ถ™์œผ๋ฉด ๊ฐ์ฒด๋ฅผ ์ž๋™์œผ๋กœ ๋ถ™์—ฌ์ค€๋‹ค. @Autowired๊ฐ€ ํ•„์š” ์—†๋‹ค๋Š” ๋œป
@RequiredArgsConstructor // ๋กฌ๋ณต ์–ด๋…ธํ…Œ์ด์…˜
public class MemberService {

    //@Autowired
    //MemberRepository memberRepository;
    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());

        // Controller ์—์„œ try/catch ๋กœ ๋‚˜์˜ค๊ฒŒ
        if (findMember != null) {
            throw new IllegalStateException("์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค.");
        }
    }
}



๐Ÿคฆโ€โ™€๏ธ MemberServiceTest ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ

package com.shop.service;

import com.shop.dto.MemberFormDto;
import com.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 org.springframework.transaction.annotation.Transactional;

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

@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)๊ณผ ์‹ค์ œ ์ €์žฅ ๋œ(savedMember) ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๊ต
        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.getTelNumber(), savedMember.getTelNumber();

        System.out.println(savedMember.getEmail());
        System.out.println(savedMember.getName());
        System.out.println(savedMember.getAddress());
        System.out.println(savedMember.getPassword());
        System.out.println(savedMember.getRole());
        System.out.println(savedMember.getTelNumber());

    }



}

ํ…Œ์ŠคํŠธ JUnit Assert ๋ฉ”์†Œ๋“œ (1)

ํ…Œ์ŠคํŠธ JUnit Assert ๋ฉ”์†Œ๋“œ (2)

๋” ๋‹ค์–‘ํ•œ Assert ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ JUnit - AssertJ



๐Ÿ“๊ฒฐ๊ณผ

Hibernate: 
    select
        m1_0.member_id,
        m1_0.address,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        next value for member_seq
test@email.com
ํ™๊ธธ๋™
์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ํ•ฉ์ •๋™
$2a$10$4bBxO.1rPw6GV4tAofOZ7efgerYQ2wyujcE53zJ07cZRGM4NsRAXW
ADMIN

๐Ÿ‘‰ Equals ์ด๋ฏ€๋กœ ์ž˜ ์‹คํ–‰ ๋๋‹ค. ๋‹ค๋ฅด๋ฉด ์—๋Ÿฌ



๐Ÿ“์ค‘๋ณต ํšŒ์› ๊ฐ€์ž… ํ…Œ์ŠคํŠธ

    @Test
    @DisplayName("์ค‘๋ณต ํšŒ์› ๊ฐ€์ž… ํ…Œ์ŠคํŠธ")
    public void saveDuplicateMemberTest(){
        Member member1 = createMember();
        Member member2 = createMember();

        memberService.saveMember(member1);

        System.out.println("member1 : " + member1);

        // assertThrows ๐Ÿ‘‰ ์˜ˆ์™ธ์ฒ˜๋ฆฌ ์ƒํ™ฉ์„ ํ™•์ธ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
        // ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋กœ ๋ฐ”๋กœ์‹คํ–‰
        Throwable e = assertThrows(IllegalStateException.class, () -> {
            memberService.saveMember(member2);
            System.out.println("member2 : " + member2);});
        assertEquals("์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค.", e.getMessage());
    }


๐Ÿ‘‰ member1 ์—์„œ validateDuplicateMember ์— ์˜ํ•ด IllegalStateException("์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค.") ๋ฐœ์ƒ

๐Ÿ‘‰ ์œ„ member1์˜ ์—๋Ÿฌ "์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค." ์™€ e.getMessage()/IllegalStateException ๊ฐ€ ๋™์ผํ•˜๋ฉด ์‹คํ–‰ํ•œ๋‹ค



๐Ÿ“๊ฒฐ๊ณผ

Hibernate: 
    select
        next value for member_seq
member1 : Member(id=1, name=ํ™๊ธธ๋™, email=test@email.com, password=$2a$10$lxRf8mMn.UOlcaK81QVYUeEXD5CtdwmqVeifSbrHLiDpauhF5dnha, address=์„œ์šธ์‹œ ๋งˆํฌ๊ตฌ ํ•ฉ์ •๋™, role=ADMIN)
Hibernate: 
    insert 
    into
        member
        (address, email, name, password, role, member_id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        m1_0.member_id,
        m1_0.address,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?

๐Ÿ‘‰



๐Ÿ“ MemberController ์ปจํŠธ๋กค๋Ÿฌ ํด๋ž˜์Šค ์ƒ์„ฑ

package com.shop.controller;

import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.Value;
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.*;


@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    private final PasswordEncoder passwordEncoder;

    // method = "get" ์ผ ๋•Œ ์‹คํ–‰
    @GetMapping(value = "/new")
    public String memberForm(Model model){

        model.addAttribute("memberFormDto", new MemberFormDto());

        return "member/memberForm";
    }

    // method = "post" ์ผ ๋•Œ ์‹คํ–‰ DB์— ์ €์žฅ
    @PostMapping(value = "/new")
    // memberForm ์˜ค๋ฒ„๋กœ๋”ฉ
    public String memberForm(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){

        // ์—๋Ÿฌ๋‚˜๋ฉด ๋‹ค์‹œ ๊ฐ€์ž…
        if(bindingResult.hasErrors()){
            return "member/memberForm";
        }

        try {
            Member member = Member.createMember(memberFormDto, passwordEncoder);

            memberService.saveMember(member);
            // ๋งž์ถฐ๋†“์€ IllegalStateException ์—๋Ÿฌ
        }catch (IllegalStateException e){
        
            model.addAttribute("errorMessage", e.getMessage());

            return "member/memberForm";
        }

        return "redirect:/";
    }
}



๐Ÿ“ 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>
<!-- ์‚ฌ์šฉ์ž JS ์ถ”๊ฐ€-->
<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="์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”">
            <!--์กฐ๊ฑด๋ฌธ์œผ๋กœ name ์—์„œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋กœ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒ ํ•˜๋ฉด th:errors = "*{name}" ์— ์˜ํ•ด ("์ด๋ฆ„์ด ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค") ๊ฐ™์€ name์— ๋Œ€ํ•œ ์—๋Ÿฌ๋ฅผ ์‹คํ–‰
            Spring validate๋ฅผ ์ง์ ‘ํ•˜๋ฉฐ ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ ์—๋Ÿฌ๊ตฌ๋ฌธ์ด Controller์—์„œ ์˜ค์ง€ ์•Š์„ ๋•Œ Incorrect data ์ถœ๋ ฅ -->
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="fieldError">Incorrect date</p>
        </div>
        <div class="form-group">
            <label th:for="email">์ด๋ฉ”์ผ์ฃผ์†Œ</label>
            <input type="text" th:field="*{email}" class="form-control" placeholder="์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”">
            <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="fieldError">Incorrect date</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
                date</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 date</p>
        </div>
        <div class="form-group">
            <label th:for="telNumber">์ „ํ™” ๋ฒˆํ˜ธ</label>
            <input type="text" th:field="*{telNumber}" class="form-control" placeholder="์ „ํ™”๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”">
            <p th:if="${#fields.hasErrors('telNumber')}" th:errors="*{telNumber}" class="fieldError">Incorrect date</p>
        </div>

        <div style="text-align: center">
            <button type="submit" class="btn btn-success" style="">Submit</button>
        </div>

        <!-- CSRF๋ฅผ ๋ฐฉ์–ดํ•˜๊ธฐ ์œ„ํ•ด ์ถ”๊ฐ€. POST ๋ฐฉ์‹์ผ ๋•Œ ์‚ฌ์šฉํ•˜๋ฉฐ CSRF ํ† ํฐ์ด ์žˆ์–ด์•ผํ•œ๋‹ค.
         ํ—ˆ์šฉํ•œ ์š”์ฒญ์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ํ† ํฐ ์ด๋‹ค. -->
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">

    </form>
</div>

</html>



๐Ÿ“MainController ํด๋ž˜์Šค ์ƒ์„ฑ

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



๐Ÿ“main.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}" lang="ko">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div layout:fragment = "content">
    <h1>๋ฉ”์ธ ํŽ˜์ด์ง€ ์ž…๋‹ˆ๋‹ค.</h1>
</div>
</body>
</html>



๐Ÿ“SecurityConfig ํด๋ž˜์Šค ์ด์–ด์„œ ์ž‘์„ฑ

package com.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // ์ „๋ถ€ ํ—ˆ๋ฝ
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.authorizeRequests(auth -> auth.requestMatchers("/", "/members/**").permitAll())
                .formLogin(formLogin -> formLogin.permitAll())
                .logout(logout -> logout.permitAll());

        return  http.build();

    }

    // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™”
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


}



๐Ÿ“Œ๊ฒฐ๊ณผ

๐Ÿ‘‰ http://localhost/members/new.์•„์ง ์›น ํŽ˜์ด์ง€๋“ค์ด ๋ฏธ์™„์„ฑ์ด๊ธฐ ๋•Œ๋ฌธ์— 403 ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋’ค๋กœ๊ฐ€๊ธฐํ•˜๋ฉฐ ์‹คํ–‰ํ•˜๋ฉด ๋œ๋‹ค

๐Ÿ‘‰ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋Š” ์ „๋ถ€ ์ž˜ ์ž‘๋™ํ•œ๋‹ค. ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ด๋ฉ”์ผ์€ ์ œ์™ธํ•˜๊ณ  EMPTY ๊ฐ’์—๋งŒ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์‹คํ–‰ํ•˜๊ฒŒ ๋˜์–ด ์žˆ๋‹ค.


๐Ÿ‘‰ ์ œ์ถœ ํ›„ root ๋กœ ์ด๋™


๐Ÿ‘‰ ์•„์ง ROLE ์€ ์„ธ๋ถ€ ์„ค์ • ํ•˜์ง€ ์•Š์•˜๋‹ค. ์œ„ ๋“ฑ๋ก๋œ ์ด๋ฉ”์ผ๋กœ ๊ฐ€์ž…์„ ์ง„ํ–‰ํ•˜๋ฉด



โ“์–ด๋–ค ๊ตฌ์กฐ๋กœ ์‹คํ–‰?

โ— ๊ตฌ์กฐ ํŒŒ์•…์„ ์ž˜ ํ•˜๋ฉด์„œ ๋„˜์–ด๊ฐ€์•ผ ๋” ๋ณต์žกํ•ด ์งˆ ๋•Œ ์ดํ•ด๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ณ„์† ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๋ฉด ๋ˆˆ๋ฉ์ด ์ฒ˜๋Ÿผ ๊ตด๋Ÿฌ๊ฐ€ ์ดํ•ด๊ฐ€ ๋ถˆ๊ฐ€ ํ•  ๊ฒƒ

  1. memberForm.html ํšŒ์›๊ฐ€์ž… submit ์œผ๋กœ form action


  2. MemberFormDto ๋ฅผ Controller ์— ์ „๋‹ฌ th:object = "${memberFormDtd}" ๋กœ dto์™€ th:field = "*{name}", "*{email}", "*{password}", "*{address}", "*{telNumber}"์ด ์—ฐ๊ฒฐ๋˜์–ด ์ด๋ฆ„๊ณผ ๋งž๋Š” ๋ณ€์ˆ˜์— ๋Œ€์ž…
    Controller์˜ @Valid ๊ฐ€ ๋ถ™์€ memberFormDto ๋กœ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. BindingResult ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ๋ฐ”์ธ๋”ฉ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๊ณ , ์—๋Ÿฌ๊ฐ€ ์žˆ์œผ๋ฉด "member/memberForm" ๋ทฐ๋กœ ๋‹ค์‹œ ์ด๋™



  3. Service ๋กœ ์ด๋™ ์ด ๋•Œ MemberFormDto ๋ฅผ Member๋กœ ๋ฐ›๋Š”๋‹ค


  4. saveMember ์‹คํ–‰validateDuplicateMember MemberRepository ์˜ findByEmail๋กœ ์ค‘๋ณต ์ฒดํฌ


  5. ๋ฌธ์ œ์—†์œผ๋ฉด createMember ํ›„ saveMember๋กœ ์ €์žฅ


  6. ๋ฌธ์ œ ์žˆ์œผ๋ฉด(์ด๋ฉ”์ผ ์ค‘๋ณต) try/catch๋กœ saveMember์—์„œ ๋ฐ›์€ "์ด๋ฏธ ๊ฐ€์ž…๋œ ํšŒ์›์ž…๋‹ˆ๋‹ค." ๋ฅผ Model๋กœ ๋ณด๋‚ด [[${errorMessage}]]; ์— ๋‹ด์•„ alert(errorMessage) ์ถœ๋ ฅ

0๊ฐœ์˜ ๋Œ“๊ธ€