ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] 2-4. 스프링 시큐리티, 회원 가입 구현하기
    프로젝트/도서 관리 시스템 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>
    반응형

    댓글

Designed by Tistory.