Spring boot & JPA

Spring Boot - 쇼핑몰 프로젝트 04 (스프링 시큐리티를 이용한 회원 가입 및 로그인 - 1)

록's 2023. 4. 4. 17:53
728x90
반응형

스프링 시큐리티를 이용한 회원 가입 및 로그인

 

스프링 시큐리티 소개

  1. 애플리케이션을 만들기 위해서는 보통 인증/인가 등의 보안이 필요
  2. 웹에서 인증이란 해당 리소스에 대해서 작업을 수행할 수 있는 주체인지 확인하는 것
  3. 인가는 인증 과정 이후에 일어나며 리소스에 접근 시 인가된 유저인지 확인(접근 권한 확인)
  4. 스프링 시큐리티를 이용하여 인증과 인가 구현

 

스프링 시큐리티 설정 추가

 

pom.xml

// pom.xml

... 생략 ...

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

... 생략 ...

 

 

  • 스프링 시큐리티 의존성 추가 시 모든 요청은 인증을 필요로함
  • 기존에 진행했던 예제 URL (http://localhost/thymeleaf/ex07) 에 접근 시 스프링 시큐리티에서 제공하는 로그인 페이지로 이동

 

  • 기본적으로 제공하는 아이디는 user이고, 비밀번호는 애플리케이션을 실행할때마다 콘솔창에 출력됨

 

  • URL에 localhost/logout을 입력하면 스프링 시큐리티에서 기본으로 제공하는 로그아웃화면 제공

 

지금은 “user” 계정 밖에 없으며, 애플리케이션 실행할 때마다 비밀번호도 바뀜.

이 상태로는 애플리케이션을 운영할 수 없기 때문에 회원 가입 기능이 필요

 

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

 

 

SecurityConfig 클래스 작성하기

com.shop.config.SecurityConfig.java

// SecurityConfig.java

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.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();
    }
}

 

 

 

 

회원가입 기능 구현

 

 

 

회원 가입 기능 구현하기

com.shop.constant.Role.java

// Role.java

package com.shop.constant;

public enum Role {
    USER, ADMIN
}

 

 

  • 회원 가입 화면으로부터 넘어오는 가입 정보를 담을 DTO 생성

 

com.shop.dto.MemberFormDto.java

// MemberFormDto.java

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

 

com.shop.entity.Member.java

// Member.java

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(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.USER);
        return member;
    }
}

 

 

com.shop.repository.MemberRepository.java

// MemberRepository.java

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

 

 

MemberService.java

// MemberService.java

package com.shop.service;

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

 

 

 

MemberServiceTest.java

// MemberServiceTest.java

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.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@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());
    }
}

 

 

MemberServiceTest.java

// MemberServiceTest.java

... 기존 임포트 생략 ...

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

... 생략 ...

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

 

 

 

 

회원 가입 페이지 작성하기

 

com.shop.controller.MemberController.java

// MemberController.java

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

 

 

 

 

resources/templates/member/memberForm.html

 

// 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">
            <lavel th:for="name">이름</lavel>
            <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="address" 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>

 

 

CSRF(Cross Site Request Forgery)

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

 

 

 

회원 가입 컨트롤러 소스코드 작성하기

// MemberController.java

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

// mainController.java

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

 

 

 

 

resources/templates/main.html

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

 

 

 

 

 

회원 가입 기능 구현

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

 

 

회원가입 처리하기

pom.xml

 

// pom.xml

	... 생략 ...

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

 

 

 

 

  • 유효한 값인지 판단하는 소스가 여러 군데 흩어지면 관리하기가 힘듬.
  • 자바 빈 밸리데이션을 이용하면 객체의 값을 효율적 검증 가능

 

 

 

MemberFormDto.java

// MemberFormDto.java

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

 

 

 

 

 

MemberController.java

// MemberController.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";
    }
}

 

 

 

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

 


 

https://rogi221.tistory.com/173

 

Spring Boot - 쇼핑몰 프로젝트 04 (스프링 시큐리티를 이용한 회원 가입 및 로그인 - 2)

https://rogi221.tistory.com/172 Spring Boot - 쇼핑몰 프로젝트 04 (스프링 시큐리티를 이용한 회원 가입 및 로그인 - 1) 스프링 시큐리티를 이용한 회원 가입 및 로그인 스프링 시큐리티 소개 애플리케이션

rogi221.tistory.com

 

728x90
반응형