Spring Boot - 쇼핑몰 프로젝트 04 (스프링 시큐리티를 이용한 회원 가입 및 로그인 - 1)
스프링 시큐리티를 이용한 회원 가입 및 로그인
스프링 시큐리티 소개
- 애플리케이션을 만들기 위해서는 보통 인증/인가 등의 보안이 필요
- 웹에서 인증이란 해당 리소스에 대해서 작업을 수행할 수 있는 주체인지 확인하는 것
- 인가는 인증 과정 이후에 일어나며 리소스에 접근 시 인가된 유저인지 확인(접근 권한 확인)
- 스프링 시큐리티를 이용하여 인증과 인가 구현
스프링 시큐리티 설정 추가
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