Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

회원가입 및 로그인 기능 추가 #3

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
Daniel-Taeyoun marked this conversation as resolved.
Show resolved Hide resolved
compile group: 'commons-codec', name: 'commons-codec', version: '1.10'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.4'


compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java' //mysql connect 드라이버 설치
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
compile 'org.springframework.boot:spring-boot-starter-log4j2'
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.10.3'

testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.springframework.boot:spring-boot-starter-log4j2'
testImplementation 'mysql:mysql-connector-java'
testImplementation 'org.hamcrest:hamcrest:2.2'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.naming.onlineshoppingmall.config;

public class MessageException extends RuntimeException {
Daniel-Taeyoun marked this conversation as resolved.
Show resolved Hide resolved
public MessageException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package me.naming.onlineshoppingmall.config;

import com.google.gson.Gson;
import java.time.LocalDateTime;
import me.naming.onlineshoppingmall.domain.ResponseData;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

// @Controller or @RestController에서 발생하는 예외를 관리 할 수 있게 만들어주는 어노테이션
// @ExceptionHandler를 통해 각각의 예외처리 메세지를 원하는 형태로 작성 할 수 있다.
@ControllerAdvice
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

@ExceptionHandler(value = { MessageException.class })
protected ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) {
HttpHeaders httpHeaders = new HttpHeaders();
Gson gson = new Gson();

httpHeaders.setContentType(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE));
String path = ((ServletWebRequest)request).getRequest().getRequestURI();

ResponseData responseData = ResponseData.builder()
.localDateTime(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.path(path)
.message(ex.getMessage())
.build();
return handleExceptionInternal(ex, gson.toJson(responseData), httpHeaders, HttpStatus.INTERNAL_SERVER_ERROR, request);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 문제를 복잡하게 해결하신 것 같은데 이런 에러메세지를 전달하는 로직을 더 심플하게 짜는법을 알아보시면 좋겠습니다~

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

//상수만을 관리하기 위한 클래스
public class CommonConstant {

private CommonConstant() {};

public static final String UTF8 = "UTF-8";
Daniel-Taeyoun marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package me.naming.onlineshoppingmall.constant;

public class RequestUrlConstant {
public static final String MEMBERS = "/members";
public static final String MEMBERS_SIGNUP = MEMBERS+"/signup";
Daniel-Taeyoun marked this conversation as resolved.
Show resolved Hide resolved

public static final String LOGIN = "/login";
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
package me.naming.onlineshoppingmall.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import me.naming.onlineshoppingmall.constant.RequestUrlConstant;
import me.naming.onlineshoppingmall.domain.Member;
import me.naming.onlineshoppingmall.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class MemberController {

private static final Logger logger = LogManager.getLogger(MemberController.class);
@Autowired
private MemberService memberService;

@GetMapping
public void hello() {
// 회원가입
@PostMapping(value = RequestUrlConstant.MEMBERS_SIGNUP)
@ResponseStatus(HttpStatus.CREATED)
public void memberJoin(@RequestBody @Valid Member member) {
memberService.signUp(member);
}

// 회원정보 갖고오기
@GetMapping(value = RequestUrlConstant.MEMBERS +"/{memNo}")
@ResponseStatus(HttpStatus.OK)
public Member getMemberInfo(@PathVariable(name = "memNo") Long memNo) {
return memberService.getMemberInfo(memNo);
}

@GetMapping(value = RequestUrlConstant.LOGIN)
@ResponseStatus(HttpStatus.OK)
public void login(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
HttpSession httpSession = request.getSession();
httpSession.setAttribute("loginInfo", memberService.getLoginInfo(userLoginRequest.getEmail(), userLoginRequest.getPassword()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 로그인 처리는 비즈니스 로직 같습니다. controller에서 처리해줘야할까요?
  2. 패스워드를 응답에 넣어주면 보안에 취약하지 않을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 처리는 비즈니스 로직이기 때문에 서비스 부분에서 처리해주었습니다.
또한, 패스워드의 경우 응답 값에 넣어주는 것이 아니라 회원 정보 조회의 경우에만 사용되고, 회원번호를 세션 정보로 전달합니다.

}

@Getter
private static class UserLoginRequest {
@NonNull String email;
@NonNull String password;
}
}
70 changes: 50 additions & 20 deletions src/main/java/me/naming/onlineshoppingmall/domain/Member.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package me.naming.onlineshoppingmall.domain;

import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
Expand All @@ -9,59 +10,88 @@
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = "MEMBERS")
@Getter
@NoArgsConstructor
@ToString
public class Member {

@Builder
public Member(String email, String password, String name, String gender, Date birthDate) {
this.email = email;
this.password = password;
this.name = name;
this.gender = gender;
this.birthDate = birthDate;
}

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEM_NO")
private Long memNo;

@Column
@Column(name = "EMAIL")
@NotNull
// @Pattern(regexp = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$")
@Email(message = "이메일 형식에 맞지 않습니다.")
private String email;

@Column
// 영문(소문자, 대문자), 숫자, 특수문자 조합 9~12자리 조합
@Column(name = "PASSWORD")
@NotNull
@Setter
@Pattern(regexp = "^(?=.*\\d)(?=.*[~`!@#$%\\^&*()-])(?=.*[a-z])(?=.*[A-Z]).{9,12}$",
message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 9자 ~ 12자의 비밀번호여야 합니다.")
private String password;

@Column
@Column(name = "NAME", length = 30)
@NotBlank(message = "이름은 필수 입력 값입니다.")
@NotNull
private String name;

@Column(name = "GENDER", length = 1)
@NotBlank(message = "성별은 필수 입력 값입니다.")
@NotNull
private String gender;

@Temporal(TemporalType.TIMESTAMP)
@Column
@Column(name = "STATUS")
@Setter
private MemberStatus memberStatus;

@Column(name = "HP", length = 11)
@NotNull
private Long hp;

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "BIRTH_DTS") @NotNull
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy/MM/dd", timezone = "Asia/Seoul)")
private Date birthDate;

@Temporal(TemporalType.TIMESTAMP)
@Column
@CreationTimestamp
@Column(name = "REG_DTS", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private Date regDts;

@Temporal(TemporalType.TIMESTAMP)
@Column
@CreationTimestamp
@Column(name = "MOD_DTS", columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private Date modDts;
}

public enum MemberStatus{
// DEFAULT(가입), DELETE(탈퇴), DORMANT(휴면)
DEFAULT, DELETE, DORMANT
}

@Builder
public Member(String email, String password, String name, String gender, MemberStatus memberStatus, Date birthDate, Long hp) {
this.email = email;
this.password = password;
this.name = name;
this.gender = gender;
this.memberStatus = memberStatus;
this.birthDate = birthDate;
this.hp = hp;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package me.naming.onlineshoppingmall.domain;

import java.time.LocalDateTime;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import org.springframework.http.HttpStatus;

@Builder
@ToString
@Getter
public class ResponseData {
private LocalDateTime localDateTime;
private HttpStatus status;
private String path;
private String message;

public ResponseData(LocalDateTime localDateTime, HttpStatus status, String path, String message) {
this.localDateTime = localDateTime;
this.status = status;
this.path = path;
this.message = message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

import me.naming.onlineshoppingmall.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByMemNo(Long memNo);
}
Member findByEmail(String email);

@Query("select mb.memNo from Member mb where mb.email = ?1 and mb.password = ?2")
Long findMemnoForLoginInfo(String email, String password);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. findMemnoByLoginInfo 가 더 자연스러울 것 같네요
  2. 굳이 이렇게 쿼리를 하나 선언하는것보다 email로 조회해서 password를 검증하는 메소드를 만드는게 더 낫지 않을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신 피드백 내용 반영했습니다. 감사합니다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package me.naming.onlineshoppingmall.service;

import me.naming.onlineshoppingmall.config.MessageException;
import me.naming.onlineshoppingmall.domain.Member;
import me.naming.onlineshoppingmall.repository.MemberRepository;
import me.naming.onlineshoppingmall.util.EncryptUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MemberService {

@Autowired
private MemberRepository memberRepository;

@Autowired
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일반적으로 생성자 주입을 권장하고는 합니다. 이유가 무엇일까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생성자 주입으로 사용할 경우 주입받을 필드 값을 final로 선언이 가능하고 순환 참조를 방지 할 수 있습니다.
또한, Mockito를 활용한 테스트 코드 작성 시 원하는 구현체를 유연하게 주입 할 수 있습니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autowired 어노테이션이 꼭 필요한가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스프링에서 자동적으로 지원해주고 있기 때문에 꼭 필요하지 않습니다. 해당 피드백 내용은 코드에 반영했습니다.

private EncryptUtil encryptUtil;

private final Logger logger = LogManager.getLogger(MemberService.class);

/**
* @description : 회원가입
*/
public void signUp(Member member) {

if (memberRepository.findByEmail(member.getEmail()) != null) {
throw new MessageException("이미 가입된 이메일 주소입니다.");
}
member.setMemberStatus(Member.MemberStatus.DEFAULT);

// 비밀번호 암호화(SHA-512)
String encodePassword = encryptUtil.shaEncode(member.getPassword());
if (StringUtils.isNotEmpty(encodePassword)) {
member.setPassword(encodePassword);
memberRepository.save(member);
}
}

public Member getMemberInfo(Long memNo) {
return memberRepository.findByMemNo(memNo);
}

public String getLoginInfo(String email, String password) {
Long memNo = memberRepository.findMemnoForLoginInfo(email, password);
String encodeMemNo = encryptUtil.aesEncode(String.valueOf(memNo));
logger.info("** encodeMemNo : {}", encodeMemNo);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 메소드에 많은 요청이 접근하면 엄청난 로그를 생성할 것 같네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 해당 부분은 삭제했습니다. 감사합니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 디버깅에 꼭 필요하다면 레벨을 낮추는것도 괜찮습니다~

return encodeMemNo;
}
}
Loading