하루살이 개발자

[Instagram 클론코딩] 2. 회원가입 본문

Project/Instagram 클론코딩

[Instagram 클론코딩] 2. 회원가입

하루살이 2022. 2. 14. 14:47

[프론트]

프론트 패키지 구조

signup.jsp (회원가입)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Photogram</title>
    <link rel="stylesheet" href="/css/style.css">
    <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"
        integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous" />
</head>

<body>
    <div class="container">
        <main class="loginMain">
           <!--회원가입섹션-->
            <section class="login">
                <article class="login__form__container">
                  
                   <!--회원가입 폼-->
                    <div class="login__form">
                        <!--로고-->
                        <h1><img src="/images/logo.jpg" alt=""></h1>
                         <!--로고end-->
                         
                         <!--회원가입 인풋-->
                        <form class="login__input" action="/auth/sinup" method="post"> 
                            <input type="text" name="username" placeholder="유저네임" required="required" csrf="KFC"/>
                            <input type="password" name="password" placeholder="패스워드" required="required" csrf="KFC"/>
                            <input type="email" name="email" placeholder="이메일" required="required" csrf="KFC"/>
                            <input type="text" name="name" placeholder="이름" required="required" csrf="KFC"/>
                            <button>가입</button>
                        </form>
                        <!--회원가입 인풋end-->
                    </div>
                    <!--회원가입 폼end-->
                    
                    <!--계정이 있으신가요?-->
                    <div class="login__register">
                        <span>계정이 있으신가요?</span>
                        <a href="/auth/signin">로그인</a>
                    </div>
                    <!--계정이 있으신가요?end-->
                    
                </article>
            </section>
        </main>
    </div>
</body>

</html>

 

 

 

[백엔드]

뱍앤드 패키지 구조

 

 

User 회원 모델 생성

import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

// JPA - Java Persistence API (자바로 데이터를 영구적으로 저장(DB)할 수 있는 API를 제공)

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity // 디비에 테이블을 생성
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) // 번호 증가 전략이 데이터베이스를 따라간다.
	private int id;
	
	@Column(length = 100,  unique = true) // OAuth2 로그인을 위해 칼럼 늘리기, unuque(중복 불가)
	private String username;

	@Column(nullable = false) // null 불가능
	private String password;

	// null 불가능(프론트에서 막았지만, 포스트맨 등에서 접근 할 수 있으므로 백엔드에서도 막아야 함)
	@Column(nullable = false)
	private String name;

	private String website; // 웹 사이트
	private String bio; // 자기 소개

	@Column(nullable = false)
	private String email;

	private String phone;
	private String gender;

	private String profileImageUrl; // 사진
	private String role; // 권한
	
	 // 나는 연관관계의 주인이 아니다. 그러므로 테이블에 칼럼을 만들지마.
	// User를 Select할 때 해당 User id로 등록된 image들을 다 가져와
	// Lazy = User를 Select할 때 해당 User id로 등록된 image들을 가져오지마 - 대신 getImages() 함수의 image들이 호출될 때 가져와!!
	// Eager = User를 Select할 때 해당 User id로 등록된 image들을 전부 Join해서 가져와!!
	@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
	@JsonIgnoreProperties({"user"})
	private List<Image> images; // 양방향 매핑
	
	private LocalDateTime createDate;
	
	@PrePersist // 디비에 INSERT 되기 직전에 실행
	public void createDate() {

		this.createDate = LocalDateTime.now();
	}

	@Override
	public String toString() {
		return "User [id=" + id + ", username=" + username + ", password=" + password + ", name=" + name + ", website="
				+ website + ", bio=" + bio + ", email=" + email + ", phone=" + phone + ", gender=" + gender
				+ ", profileImageUrl=" + profileImageUrl + ", role=" + role +", createDate="
				+ createDate + "]";
	}
	
}

 

SignupDto 회원가입 모델 생성

package com.cos.photogramstart.web.dto.auth;

import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import com.cos.photogramstart.domain.user.User;

import lombok.Data;

// 요청하는 Dto
// username, pw, email, text 를 담고있음

@Data // Getter, Setter
public class SignupDto {
	@Size(min = 2, max = 20)
	@NotBlank // 무조건 입력 받아야 함
	private String username;
	@NotBlank
	private String password;
	@NotBlank
	private String email;
	@NotBlank
	private String name;
	
	public User toEntity() { // SignupDto를 기반으로 객체 만들기
		return User.builder()
				.username(username)
				.password(password)
				.email(email)
				.name(name)
				.build();
	}
}

 

SecurityConfig 생성

package com.cos.photogramstart.config;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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.core.Authentication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import com.cos.photogramstart.config.oauth.OAuth2DetailsService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@EnableWebSecurity // 해당 파일로 시큐리티를 활성화
@Configuration // IoC     
public class SecurityConfig extends WebSecurityConfigurerAdapter{

	private final OAuth2DetailsService oAuth2DetailsService;
	
	@Bean // 암호화
	public BCryptPasswordEncoder encode() {

		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		// super 삭제 - 기존 시큐리티가 가지고 있는 기능이 다 비활성화됨.
		http.csrf().disable();
		http.authorizeRequests()
			.antMatchers("/", "/user/**", "/image/**", "/subscribe/**", "/comment/**", "/api/**").authenticated()
			.anyRequest().permitAll()
			.and()
			.formLogin()
			.loginPage("/auth/signin") // GET
			.loginProcessingUrl("/auth/signin") // POST -> 스프링 시큐리티가 로그인 프로세스 진행
			.defaultSuccessUrl("/")
			.and()
			.oauth2Login() // form로그인도 하는데, oauth2로그인도 할꺼야!!
			.userInfoEndpoint() // oauth2로그인을 하면 최종응답을 회원정보를 바로 받을 수 있다.
			.userService(oAuth2DetailsService);
	}

}

 

AuthController 회원가입 컨트롤러

package com.cos.photogramstart.web;

import java.util.HashMap;
import java.util.Map;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.cos.photogramstart.domain.user.User;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.service.AuthService;
import com.cos.photogramstart.web.dto.auth.SignupDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor // final 필드를 DI 할때 사용(final 필드를 따름)
@Controller // 1. IoC 2. 파일을 리턴하는 컨트롤러
public class AuthController {

	private final AuthService authService;

	@GetMapping("/auth/signin")
	public String signinForm() {
		return "auth/signin";
	}

	@GetMapping("/auth/signup")
	public String signupForm() {
		return "auth/signup";
	}

	/*
	회원가입 버튼 -> /auth/signup -> /auth/signin
	* 회원가입 버튼을 눌렀는데 아무 것 도 안뜸(404오류) 왜? -> CSRF 토큰이 실행중이기 때문
	* CSRF 토큰이란? 서버로 들어가기 전에  시큐리티 CSRF 토큰을 먼저 검사하는 구조임
	회원가입 페이지에 요청을 하면 응답(signup.jsp)하는데 응답 전에 CSRF 토큰이 생성
	정상적인 사용자인지 확인하기 위해 CSRF토큰을 사용(정상적인 경로로 접근한 사용자인지 확인하기 위해)
	여기선 CSRF 비활성화 할 것(기본적으로 활성화 되어있음) : SecurityConfig에서 비활성화 하기
	 */

	/*
	@Valid <- 유효성 검사 (spring-boot-starter-validation 의존성 추가 후 사용 가능)
	 */
	@PostMapping("/auth/signup")
	public String signup(@Valid SignupDto signupDto, BindingResult bindingResult) { // key=value (x-www-form-urlencoded)
		// @Valid 유효성 검사, BindingResult 클래스:
		// User < - SignupDto(회원가입 시 SignupDto에 있는 객체에 값 넣기)
		User user = signupDto.toEntity(); // 생성
		authService.회원가입(user); // DB에 집어넣기(service 필요)
		//System.out.println(userEntity);
		// 로그를 남기는 후처리!!
		return "auth/signin";

	}
}

* 유효성 검사 예외처리

 

CustomValidationException

package com.cos.photogramstart.handler.ex;

import java.util.Map;

public class CustomValidationException extends RuntimeException{ // 런타임 에러 낚아채기 위해
	
	// 객체를 구분할 때!!
	private static final long serialVersionUID = 1L;

	private Map<String, String> errorMap;
	
	public CustomValidationException(String message, Map<String, String> errorMap) {
		super(message);
		this.errorMap = errorMap;
	}
	
	public Map<String, String> getErrorMap(){

		return errorMap;
	}
}

 

CMRespDto 공통 응답(경우1)

package com.cos.photogramstart.web.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
// 공통 응답 Dto

@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> { // <T> 제너릭 이용
	private int code; // 1(성공), -1(실패)
	private String message;
	private T data;
}

 

Script 자바스크립트로 반환(경우2)

package com.cos.photogramstart.util;

// JS 이용(유효성 검사시 실패했을 경우 경고창 띄우고 자동으로 뒤로 돌아감)
public class Script {
	
	public static String back(String msg) {
		StringBuffer sb = new StringBuffer();
		sb.append("<script>");
		sb.append("alert('"+msg+"');"); // msg 띄우기(경고창 띄우기)
		sb.append("history.back();"); // 경고창 띄우고 뒤로 돌아감
		sb.append("</script>");
		return sb.toString();
	}
}

 

ControllerExceptionHanlder 유효성 검사 실패 처리 방법 2가지

package com.cos.photogramstart.handler;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.handler.ex.CustomApiException;
import com.cos.photogramstart.handler.ex.CustomException;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.util.Script;
import com.cos.photogramstart.web.dto.CMRespDto;
// 오류 발새 시 낚아채서 응답하기
@RestController
@ControllerAdvice
public class ControllerExceptionHanlder {
 
	@ExceptionHandler(CustomValidationException.class)
	public String validationException(CustomValidationException e) {
		/* CMRespDto, Script 비교
		 1. 클라이언트에게 응답할 때는 Script 좋음.
		 2. Ajax통신 - CMRespDto
		 3. Android 통신 - CMRespDto
		 */
		if(e.getErrorMap() == null) {
			return Script.back(e.getMessage());
		}else {
			return Script.back(e.getErrorMap().toString());
		}
		
	}
	// 유효성 검사 실패시 처리방법 경우1) JS 형태로
	@ExceptionHandler(CustomException.class)
	public String exception(CustomException e) {

		return Script.back(e.getMessage());
	}

	// 유효성 검사 실패시 처리방법 경우2) CMRespDto
	// 어떤 타입을 넣을 지 모르면 <?>
	@ExceptionHandler(CustomValidationApiException.class)
	public ResponseEntity<?> validationApiException(CustomValidationApiException e) {
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), e.getErrorMap()), HttpStatus.BAD_REQUEST);
	}
	
	@ExceptionHandler(CustomApiException.class)
	public ResponseEntity<?> apiException(CustomApiException e) {
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), null), HttpStatus.BAD_REQUEST);
	}
}

 

* main

PhotogramStartApplication

package com.cos.photogramstart;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PhotogramStartApplication {

	public static void main(String[] args) {
		SpringApplication.run(PhotogramStartApplication.class, args);
	}

}