[Spring Boot] 2-4. 스프링 시큐리티, 회원 가입 구현하기
스프링 시큐리티를 이용한 로그인 처리는 아래 포스팅에 정리해두었다. 아래 포스팅을 봐야지만 회원 가입을 구현할 수 있다.
https://wonisdaily.tistory.com/270
📑 memberSaveForm (domain)
앞에서 로그인시 domain 클래스로 UserDetails 인터페이스를 구현하는 클래스를 사용하였다. 처음엔 나도 그냥 DB에 생성해둔 컬럼들을 하나의 domain 클래스로 만들어서 사용하려고 했다.
그러나 김영한 강사님의 강의 내용 중 다음과 같은 내용을 발견했다.
회원을 등록하는 기능을 구현한다고 할때 회원 등록시 회원과 관련된 데이터만 전달받는 것이 아니라, 약관 정보도 추가로 받는 등 회원 테이블과 관계없는 수 많은 부가 데이터가 넘어온다. 그래서 보통 회원을 MemberVO라고 봤을때, MemberVO을 직접 전달받는 것이 아니라, 복잡한 폼의 데이터 컨트롤러까지 전달할 별도의 객체로 만들어 전달한다. 예를 들면 MemberSaveForm이라는 폼을 전달받은 전용 객체를 만들어 @ModelAttribute로 사용한다.
따라서 나는 MemberSaveForm과 MemberUpdateForm 으로 domain 객체를 나눌 생각이다.
package com.wish.library.security.domain;
import lombok.*;
import java.util.Date;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor //기본 생성자를 생성해준다.
@Builder
public class MemberSaveForm {
private String email; // 회원 아이디
private String password; //비밀번호
private String name;
private String nickname;
private Sex mfCode; //성별 (men, female)
private String cellNo;
}
📑 MemberMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wish.library.security.mapper.UserMapper">
<insert id="insert">
insert into members(email, password, name, nickname, mfCode, cellNo)
values (#{email}, #{password}, #{name}, #{nickname}, #{mfCode}, #{cellNo})
</insert>
</mapper>
📑 MemberMapper.java
package com.wish.library.security.mapper;
import com.wish.library.security.domain.MemberSaveForm;
import com.wish.library.security.domain.UserDTO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
int insert(MemberSaveForm member);
}
📑 회원 가입 테스트
package com.wish.library.security.mapper;
import com.wish.library.security.domain.MemberSaveForm;
import com.wish.library.security.domain.Sex;
import com.wish.library.security.service.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import static org.junit.jupiter.api.Assertions.*;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@Slf4j
class UserMapperTest {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserMapper userMapper;
@Test
public void 회원가입_테스트(){
MemberSaveForm member = MemberSaveForm.builder()
.email("test@gmail.com")
.password(passwordEncoder.encode("test"))
.name("test회원")
.nickname("test회원")
.mfCode(Sex.FEMALE)
.cellNo("01077778888")
.build();
int insertResult = userMapper.insert(member);
Assertions.assertThat(insertResult).isEqualTo(1);
}
}
📑 MemberServiceImpl.java
package com.wish.library.security.service;
import com.wish.library.security.domain.MemberSaveForm;
import com.wish.library.security.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
private final UserMapper mapper;
private final PasswordEncoder passwordEncoder;
@Transactional
@Override
public boolean save(MemberSaveForm member) {
log.info("============member save service");
try{
member.setPassword(passwordEncoder.encode(member.getPassword()));
mapper.insert(member);
return true;
}catch(Exception e){
e.printStackTrace();//에러의 발생 근원지를 찾아서 단계별로 에러를 출력
log.info("MemberService 회원가입 에러 ={}", e.getMessage());
}
return false;
}
}
여기서 form에서 받아온 정보들 중 비밀번호를 인코딩하여 저장한다. 그다음 mapper.insert(member)로 넘겨주는 것을 잊지말자!
📑 MemberController.java
package com.wish.library.security.controller;
import com.wish.library.security.domain.MemberSaveForm;
import com.wish.library.security.domain.UserDTO;
import com.wish.library.security.service.CustomUserService;
import com.wish.library.security.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* GET 방식, 화면 조회시
*/
@Controller
@RequiredArgsConstructor
@Slf4j
public class UserController {
private final CustomUserService customUserService;
private final MemberService memberService;
@GetMapping("/")
public String home(Model model){
String email = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(!(email.equals("anonymousUser"))) {
log.info("index page, security 조회 email, getPrincipal : " + email);
UserDTO user = (UserDTO) customUserService.loadUserByUsername(email);
user.setPassword(null);
model.addAttribute("user", user);
}
return "index";
}
@GetMapping("/login")
public String login(){
return "login";
}
@GetMapping("/join")
public String join(){
return "join";
}
@PostMapping("/join")
public String join(@ModelAttribute MemberSaveForm memberSaveForm, RedirectAttributes rttr) {
log.info("=================join Controller");
boolean saveResult = memberService.save(memberSaveForm);
if(saveResult){
rttr.addFlashAttribute("result", "회원 가입 완료");
return "redirect:/login";
}
return "join";
}
}
Service 단에서 넘겨준 값이 true일 경우에, 즉 회원 가입이 유효성 검사를 통과했을 경우 login 페이지로 넘겨준다. 그 사이 login.jsp 화면에 "회원 가입 완료"라는 result message를 전달해준다. 앞에 login.jsp에 script 부분에서 아래와 같이 처리해줬다. 아직 유효성 검사 로직은 만들기 전이다. 이전 세션을 이용한 로그인/회원가입에서는 javascript로 정규식을 사용한 유효성 검사를 처리해줬다. 이번 시큐리티에서는 @Validated 어노테이션을 사용할 수 있는 Bean Vaildation을 이용할 예정이다. 다음 포스팅에서 정리하겠다.
📑 join.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!doctype html>
<html lang="en" data-bs-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.111.3">
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<title>회원가입</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.3/examples/sign-in/">
<link href="assets/dist/css/sign-in.css" rel="stylesheet">
<jsp:include page="./includes/common_includes.jsp"></jsp:include>
<%-- <script type="text/javascript" src="assets/dist/js/commonAjax.js"></script>
<script type="text/javascript" src="assets/dist/js/member.js"></script>--%>
<style>
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
</style>
</head>
<script>
</script>
<body class="text-center">
<main class="form-signin w-100 m-auto">
<form name="join_form" action="/join" method="post">
<%--<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />--%>
<img class="mb-4" src="assets/brand/bootstrap-logo.svg" alt="" width="72" height="57"
onclick="location.href='/'">
<h1 class="h3 mb-3 fw-normal">Please join in</h1>
<div class="form-floating">
<input type="email" class="form-control" name="email" id="email" placeholder="name@example.com">
<label for="email">Email address</label><%--<button class="btn btn-light rounded-pill px-3" type="button" name="btn" id="btnEmailCheck">Duplicate check</button>--%>
<span id="id-check"></span>
</div>
<div class="form-floating">
<input type="password" class="form-control" name="password" id="password" placeholder="Password" autoComplete="off">
<label for="password">Password</label>
</div>
<div class="form-floating">
<input type="password" class="form-control" name="passwordConfirm" id="passwordConfirm" placeholder="passwordConfirm" autoComplete="off">
<label for="passwordConfirm">passwordConfirm</label>
</div>
<div class="form-floating">
<input type="text" class="form-control" name="name" id="name" placeholder="name">
<label for="name">name</label>
</div>
<div class="form-floating">
<input type="text" class="form-control" name="nickname" id="nickname" placeholder="nickname">
<label for="nickname">nickname</label>
<%--<button class="btn btn-light rounded-pill px-3" type="button" name="btn" id="btnNicknameCheck">Duplicate check</button>--%>
<span id="nickname-check"></span>
</div>
<div class="form-floating">
<div class="form-check">
<input id="men" name="mfCode" value="MEN" type="radio" class="form-check-input" checked>
<label class="form-check-label" for="men">Men</label>
</div>
<div class="form-check">
<input id="female" name="mfCode" value="FEMALE" type="radio" class="form-check-input">
<label class="form-check-label" for="female">Female</label>
</div>
</div>
<div class="form-floating">
<input type="text" class="form-control" name="cellNo" id="cellNo" placeholder="cellNo">
<label for="cellNo">cellNo</label>
</div>
<button class="w-50 btn btn-lg btn-dark" type="button" onclick="history.back();" >이전페이지로가기</button>
<%--<button class="w-50 btn btn-lg btn-primary" type="button" name="btn" id="btnSingUp" >Sign up</button>--%>
<button type="submit" class="w-50 btn btn-lg btn-primary">Sign up</button>
</form>
</main>
</body>
</html>