본문 바로가기
프로그래밍/Springboot-토이프로젝트

【게시판-09】회원가입

by 코이킹 2022. 9. 20.
반응형

안녕하세요 코이킹입니다.
이번 포스트는 회원가입 구현에 대한 내용이 되겠습니다. 

 


1. 목표 

- 스프링 부트를 사용하여 복잡한 프로세스가 포함된 기능을 갖춘 웹 어플리케이션을 구현할 수 있다.  

- JavaScript(JQuery)를 사용하여 폼 데이터를 전송할 수 있다. 

- 암호화라는 키워드에 대해 알기

 

 

2. 어떻게 구현할지에 대한 설명 

회원가입의 핵심을 정의해보면 '유저로 부터 입력받은 계정 정보를 DB에 저장하는 것'이 되겠습니다. 

 

CRUD 기능구현에서 했던 것과 같이, 해야 할 일을 먼저 추려내 보겠습니다. 

스스로 질문하기 답변 추려내기
데이터 다루기 회원가입시 데이터는 입력방법? 회원가입 페이지를 추가하여, 데이터 입력 폼에서 유저정보를 입력 받는다.
회원가입 페이지의 이동경로 페이지의 헤더 부분의 링크에서 이동
회원가입 처리요청시 데이터의 전송방법 POST방식으로 데이터 송신할 것. 
회원가입 페이지 요청, 회원가입 처리 요청 수신방법 회원기능 컨트롤러에서 회원가입 페이지 요청, 회원가입 처리 요청을 매칭하는 메서드를 추가.
프로세스 회원은 복수의 계정을 가질 수 있는지? 회원하나 (이메일을 기준)당 한개의 계정을 가짐.
회원가입 중복 체크는 어떻게 할 것인지? 회원가입 중복확인요청을 매칭하는 메서드를 회원기능 컨트롤러에 추가하기, 중복 확인 요청은 비동기로 보내기.
회원의 권한은 어떻게 설정할지? 처음 가입시는 '일반유저'권한 부여
회원의 비밀번호는 어떻게 암호화 할 것인지? 단방향 암호화를 사용

 

이 포스트 부터는 해야 할 일을 추려내기 이전에 플로우 차트와 시퀀스 차트를 작성하고 나서부터 소스코드 설명을 하려 합니다. 

간단한 CRUD 기능 구현은 화면 <->  DB간에 데이터를 이동하고 컨트롤하는 것이 중점이었기에 프로세스 자체가 어렵지 않았습니다만, 기능의 시작부터 끝까지 다수의 프로세스가 복잡하게 얽혀 있다면 어디서부터 코딩을 해야 할지 감을 잡지 못하실 수 있습니다. 

이럴 때 플로우 차트와 시퀀스 차트를 작성하고 코드를 작성하면 구현 시 무엇을 하고 있고, 무엇이 보틀넥인지 진척이 어느 정도 인지 파악하기가 쉽습니다. 

 

3. 프로세스 흐름

 

4. 데이터 흐름 

① 브라우저에서 서버로 회원가입 페이지 표시 요청을 보내고, 서버에서 브라우저로 회원가입 페이지를 리턴합니다. 

② 브라우저에서 ID중복체크 요청을 보내고 결과를 브라우저로 리턴한다. 통신은 비동기로 이루어 집니다. 

 결과는 JSON 데이터 형식으로 리턴되면 JSON데이터의 data파라미터의 결과(boolean)에 따라 페이지 상의

JavaScript변수 idDuplicateCheckStatus의 값을 변경합니다.. 

 -> data파라미터의 결과 false : idDuplicateCheckStatus를 true로 하여 회원가입 처리 요청을 보낼 수 있도록 합니다.

 -> data파라미터의 결과 true : idDuplicateCheckStatus의 값을 기본값인 false로 두어 회원가입 처리요청을 보낼 수 없도록 합니다.. 

③ 회원가입 처리요청를 보냅니다. 회원가입 처리 요청의 일련의 처리를 행한 후 결과를 리턴합니다. 

 -> 정상적으로 처리된 경우 : 로그인 페이지를 표시합니다..

 -> 예외/에러가 발생한 경우 : 메시지 페이지를 거쳐, 회원가입 페이지를 표시합니다.  

 

5. 소스코드와 해석

1) 회원가입 페이지 템플릿 : /template-springboot/src/main/resources/templates/auth/join.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
  layout:decorate="~{common/layout}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, inital-scale=1.0"/>

</head>
<body>

<div layout:fragment="contents">

    <form method="post" th:action=@{/auth/join}>
    <h1 class="h3 mb-3 fw-normal text-center">Please Join Member</h1>

    <div class="mb-3 col-12 row">
        <label class="form-label">ID (E-mail)</label>
    	<div class="col">
    	    <input type="text" id="memberId" name="memberId" class="form-control" aria-describedby="emailHelp">
    	</div>
    	<div class="col">
    	    <button id="id-duplicate-check" class="btn btn-info btn-sm" type="button">ID Duplicate Check</button>
    	</div>

    </div>

	<div class="mb-3 col-8">
		<label class="form-label"> Password </label>
		<input class="form-control" type="password" name="password" id="password">
	</div>

    <div class="nav justify-content-center col-8">
    	<input class="btn btn-success" type="button" value="Join Complete" onclick="joinMember(this.form);">
    </div>
    </form>

    <hr/>

	<div>
		<button class="btn btn-secondary py-2 my-2" type="button" onclick="location.href='javascript:history.back();'">Return to Previous Page</button>
		<button class="btn btn-info py-2 my-2" type="button" onclick="location.href='/board/list'">Back to List</button>
	</div>

<script type="text/javascript" th:inline="javascript">

var baseApi  = "http://localhost:8080"
var authApi = baseApi+"/auth"

var idDuplicateCheckStatus = false;

$(document).on("click", '#id-duplicate-check', function(event) {
	event.preventDefault();

	var memberId = $("#memberId").val();
	if (memberId === "") {
		alert("Please Input E-mail");
		$('#memberId').focus();
		return;
	}

	$.ajax({
		url: authApi+"/checkId",
		method : 'POST',
		contentType : 'application/x-www-form-urlencoded; charset=UTF-8',
		data: {'memberId' : memberId},
		success :  function(res){
			//alert(JSON.stringify(res));
			if (res.data === true) {
				alert(memberId+" is aleardy exist. Please Login Or Input other Email Id");
				$('#memberId').focus();
			} else if (res.data === false) {
				alert("You can use "+memberId);
				idDuplicateCheckStatus = true;
				$('#id-duplicate-check').attr('disabled', 'disabled');
				$('#password').focus();
			} else {
				alert(res.data);
			}
		},
	    error: function(xhr, textStatus, error){
	      console.log(xhr.statusText);
	      console.log(textStatus);
	      console.log(error);
	      alert(error);
	    }
	});
});


function joinMember(form) {

	if (!idDuplicateCheckStatus) {
		alert("Please do ID Duplicate Check");
		return;
	}

	var memberId = form.memberId.value;
	var password = form.password.value;

	if (memberId === "") {
		alert("Please Input E-mail");
		$('#memberId').focus();
		return;
	}

	if (password === "") {
		alert("Please Input Password");
		$('#password').focus();
		return;
	}

	form.submit();
}

</script>

</div>
</body>
</html>

- 14~36행 : 회원가입을 위한 데이터 입력 폼입니다.  

 -> 24행의 버튼을 클릭하면 ID 중복체크요청 JavaScript함수가 실행됩니다. 

 -> 24행의 버튼을 클릭하면 작성한 폼 데이터를 매개변수로 하여, 회원가입 처리 요청 JavaScript함수가 실행됩니다.

 

- 50행 : ID중복체크를 통과했는지를 구분해주는 변수입니다. 

- 52~88행 :  ID 중복체크 버튼이 클릭되면, 실행되는 함수입니다. 

 -> 53행 : 버튼 클릭 시 기본으로 발생되는 이벤트를 막아줍니다. 폼 태그 안에 존재하는 버튼 요소를 클릭하면 폼 데이터가 전송되므로 기본 이벤트 방지를 해주어야 합니다. 

  -> 55~60행 : ID가 입력되어 있지 않다면, ID를 입력해 달라는 메시지를 출력하고, ID 중복체크 요청의 이후 처리는 실행되지 않습니다. 

 -> 62~87행 : JQuery를 사용하여 서버에 ID 중복체크 요청을 보냅니다. 67행의 res가 요청의 결과 값입니다. 

     res.data 파라미터의 결과가 true면 ID 중복이므로 다른 ID를 사용하라는 메시지를 출력합니다. 

    res.data 파라미터의 결과가 false면 사용 가능한 ID이므로 idDuplicateCheckStatus의 값을 true로 바꿔주어 회원가입 처리 요청을 보낼 수 있도록 해 주고, ID 중복체크 요청은 더 이상 기능하지 않아도 되므로 비활성 해줍니다. 

   ID의 확인이 끝났으므로 다음 입력 칸인 PW입력 칸으로 입력 포커싱을 변경합니다. 

   예기치 못한 에러가 발생하면, 리턴한 데이터를 메시지로 띄워줍니다. 

 

91~113행 : 회원가입 처리 요청 함수입니다. 회원가입 버튼이 클릭되면 실행됩니다. 입력 폼의 데이터의 null체크를 한 후 

입력 폼의 데이터에 문제가 있다면 문제가 발생한 데이터의 입력 창으로 포커싱을 변경합니다. 

입력 폼의 데이터에 문제가 없다면 서버로 회원가입 처리 요청을 보냅니다.

 

2) 회원 기능 컨트롤러 : /template-springboot/src/main/java/com/sb/template/controller/MembershipController.java

package com.sb.template.controller;

import java.util.List;
import java.util.stream.Collectors;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sb.template.dto.ResponseDto;
import com.sb.template.entity.Member;
import com.sb.template.enums.ResponseInfo;
import com.sb.template.forms.AuthForm;
import com.sb.template.service.MemberService;

import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping("auth")
@Slf4j
public class MembershipController {

	@Autowired
	private MemberService memberService;


	@RequestMapping(method = RequestMethod.GET, path = "/join")
	public String joinMember(@ModelAttribute AuthForm form) {

		return "auth/join";
	}


	@RequestMapping(method = RequestMethod.POST, path = "/join")
	public String joinMember(@ModelAttribute @Validated AuthForm form, BindingResult bindingResult, Model model) {

		if (bindingResult.hasErrors()) {
			List<String> errors = bindingResult.getAllErrors().stream().map(e -> e.getDefaultMessage()).collect(Collectors.toList());
			model.addAttribute("message", String.join(", ", errors));
			model.addAttribute("redirectUrl", "/auth/join");
			return "/common/message";
		}

        Member member = memberService.createMember(form);

		if (member == null) {
			model.addAttribute("message", form.getMemberId()+" is already exist!!");
			model.addAttribute("redirectUrl", "/auth/join");
			return "/common/message";
		}

		model.addAttribute("message", member.getMemberId()+"has been joined!!");
		model.addAttribute("redirectUrl", "/auth/login");

		return "/common/message";
	}


	@PostMapping(path = "/checkId")
	@ResponseBody
	public ResponseEntity<?> checkId(
			@RequestParam(required = true,
			name = "memberId")
			@Email
			@NotBlank
			String memberId
			) {

		log.info("Input memberId : {}", memberId);

		return ResponseEntity.ok(ResponseDto.builder()
				.resultCode(ResponseInfo.SUCCESS.getResultCode())
				.message(ResponseInfo.SUCCESS.getMessage())
				.data(memberService.existMemberId(memberId))
				.build()
				);
	}


	@RequestMapping(method = RequestMethod.GET, path = "/login")
	public String loginForm() {
		return "auth/login";
	}
}

※ 구현된 대부분의 코드는 게시판 기능 구현 시에 사용된 것과 동일하므로 자세한 설명은 생략합니다. 

- 40, 47, 73, 93행 : 회원 기능의 각 요청에 매핑되는 메서드 입니다. 

- 72행 : @ResponseBody를 사용하면, 서버에서 리턴할 때 페이지의 템플릿이 아니라 데이터를 보낼 수 있습니다. 

 리턴하는 데이터의 형태는 기본적으로 JSON형식입니다. 

 

3) 회원기능 서비스 : /template-springboot/src/main/java/com/sb/template/service/MemberService.java

package com.sb.template.service;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.sb.template.entity.Member;
import com.sb.template.enums.MemberRole;
import com.sb.template.forms.AuthForm;
import com.sb.template.repo.MemberRepository;
import com.sb.template.utils.EncUtil;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class MemberService {

	@Autowired
	private MemberRepository memberRepository;

	@Autowired
	private EncUtil enc;

	public Member createMember(AuthForm form) {

		Optional<Member> memberOp = memberRepository.findByMemberId(form.getMemberId());
		if (!memberOp.isEmpty()) {
			log.warn("{} is already exist!!", form.getMemberId());
			return null;
		}

		Member member = form.toEntity();
		member.setPassword(enc.generateSHA512(member.getPassword()));
		member.setRole(MemberRole.COMMON.value);

		member = memberRepository.save(member);
		return member;
	}


	public boolean existMemberId(String memberId) {
		Optional<Member> result = memberRepository.findByMemberId(memberId);
		return result.isEmpty() ? false : true;
	}

}

※ 구현된 대부분의 코드는 게시판 기능 구현 시에 사용된 것과 동일하므로 자세한 설명은 생략합니다. 

- 35행 : 유저 데이터를 DB에 저장하기 전에 유저로부터 입력된 패스워드를 SHA-512 형식으로 암호화하여 저장합니다. 

- 36행 : 유저 데이터를 DB에 저장하기 전에 유저의 권한을 설정합니다.

 

※ 암호화에 대해서 

패스워드 같은 민감한 정보는 누구나 알아볼 수 있는 상태로 DB에 저장하면 안 됩니다. 민감한 정보를 평문에서 의미 없는 데이터로 변경하는 것을 암호화라고 보면 됩니다. 

이 포스트의 주제는 웹 애플리케이션의 구현이므로 자세한 설명은 생략하고 찾아서 보면 좋은 키워드만 공유하도록 하겠습니다. 

 - 암호화와 복호화란?

 - 양방향 암호화 

 - 대칭키와 비대칭키 

 - 단방향 암호화 

 - 해시 알고리즘

 

6. 동작확인 

- ID 중복체크에서 한번 실패합니다.

- ID 중복체크에서 성공했지만, 패스워드의 형식 불일치로 회원가입 폼으로 돌아옵니다.

- ID 중복체크를 성공하고, 회원가입 처리에 성공하여 로그인 페이지로 이동합니다.

7. 전체 소스코드 

https://github.com/leeyoungseung/template-springboot/tree/feature/09_membership_board_join

 

GitHub - leeyoungseung/template-springboot

Contribute to leeyoungseung/template-springboot development by creating an account on GitHub.

github.com


이것으로 회원가입 구현에 대한 포스트는 마치겠습니다.

다음 포스트는 세션을 활용한 로그인과 로그아웃에 대한 내용이 되겠습니다. 

반응형

댓글