프로젝트/도서 관리 시스템

[Spring Boot] 2-4. 스프링 시큐리티, 회원 가입 구현하기

s워니얌 2023. 7. 5. 13:23

 

 

 

 

 

스프링 시큐리티를 이용한 로그인 처리는 아래 포스팅에 정리해두었다. 아래 포스팅을 봐야지만 회원 가입을 구현할 수 있다. 

 

https://wonisdaily.tistory.com/270

 

[Spring Boot] 2-3. 스프링 시큐리티, 로그인/로그아웃 구현

스프링 시큐리티에 관한 개념은 아래 포스팅에서 따로 정리해뒀다. https://wonisdaily.tistory.com/268 [Spring Boot] Spring Security 설정, 예제, 용어와 흐름 📑 Spring Security 설정 스프링 시큐리티를 구현하는

wonisdaily.tistory.com

 

 


 

📑 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>
반응형