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

Commit

Permalink
Add 회원가입 기능 (#5)
Browse files Browse the repository at this point in the history
* Add 회원가입 기능

- 회원가입 기능을 통해 Careers를 로그인 한 사용자들만 이용할 수 있도록 한다.
- 이름, 이메일, 비밀번호 모두를 입력 받는다.(Null 체크)
- 이메일은 이메일 형식에 맞게 작성해야 한다.
- 비밀번호는 문자, 숫자, 특수문자로 구성되어야 한다.
- 비밀번호는 암호화하여 DB에 저장한다.(sha-256)
- 중복된 이메일인지 체크하여 중복가입을 제한한다.

#3

* Fix 디비 primary key id로 수정

* Fix 큐레이터 테이블 unique index 설정

* Fix 회원가입 http method 변경

put -> post

* Fix 롬복사용하여 생성자 자동주입

* Fix 사용하지 않는 요청 메서드 제거

* Fix 이메일 중복 예외처리

* Fix http status code 처리

이메일, 비밀번호 위반 시 status code 반환하도록 수정

* Fix model코드 가독성 처리

* Fix 암호화코드와 유저서비스 기능 분리

단일책임원칙 적용

* Fix Select쿼리문 email컬럼 명시적으로 표현

* Fix mysql 사용자계정 변경

* Fix 롬복 Data 애노테이션 제거

명확한 의도를 설정하기위해 Data 애노테이션 대신 각각 설정

* Fix 롬복 애노테이션 정리

* Fix StringBuffer를 StringBuilder로 변경

여러 스레드가 해당 부분을 접근할 일이 현재로서는 없으므로 StringBuilder로 수정

* Fix 사용하지 않는 애노테이션 제거

* Update src/main/java/com/dev/careers/service/encryption/SHA256Encryption.java

Fix 코딩 컨벤션 위반 수정

Co-authored-by: f-lab <54677861+f-lab-dev@users.noreply.github.com>

* Fix 인터페이스, 클래스 명명규칙 수정

Encryption -> Encryptor 로 변경

* Fix 메소드 명명규칙 수정

* Fix 요청에따른 결과 반환 void로 수정

* Fix void반환으로 인한 테스트케이스 수정

* Fix 성능보완을 위해 쿼리로 중복검증 체크

* Fix 성능향상을 위해 쿼리문 수정

exists를 사용하여 쿼리성능향상 및 이메일 존재 유무에 대해 명확하게 표현

* Fix checkstyle 위반된 부분 수정

카멜케이스 적용

Co-authored-by: f-lab <54677861+f-lab-dev@users.noreply.github.com>
  • Loading branch information
phantom08266 and f-lab-dev committed Apr 6, 2021
1 parent fcf17ab commit 60ed0cc
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 3 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ dependencies {
implementation group: 'mysql', name: 'mysql-connector-java'
implementation group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '2.1.4'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-log4j2'
// 요청파라미터 검증하기 위해 추가
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.4.3'
//javax.annotation.meta.When 경고로 인해 구글에서 해당 버그 수정한 의존성 추가
implementation 'com.google.code.findbugs:jsr305:3.0.2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
9 changes: 9 additions & 0 deletions sql/ddl.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
create table Curator(
id int(10) not null auto_increment,
email varchar(64) not null,
name varchar(64) not null,
password varchar(64) not null,
salt varchar(64) not null,
primary key (id),
unique index idx_email (email)
);
34 changes: 34 additions & 0 deletions src/main/java/com/dev/careers/controller/CuratorController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.dev.careers.controller;

import com.dev.careers.model.Curator;
import com.dev.careers.service.CuratorService;
import com.dev.careers.service.error.ViolationException;
import java.util.Optional;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;


@RequiredArgsConstructor
@RestController
public class CuratorController {

private final CuratorService curatorService;

@PostMapping("/curators/join")
public void putMember(@Valid @ModelAttribute Curator curator, BindingResult bindingResult)
throws Exception {
if (bindingResult.hasErrors()) {
Optional<ObjectError> objectError = bindingResult.getAllErrors().stream().findFirst();
if (objectError.isPresent()) {
throw new ViolationException();
}
}

curatorService.join(curator);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/dev/careers/mapper/CuratorMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dev.careers.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface CuratorMapper {

Integer insertCurator(
@Param("email") String email,
@Param("name") String name,
@Param("password") String password,
@Param("salt") String salt);

boolean checkEmailExists(@Param("email") String email);
}
31 changes: 31 additions & 0 deletions src/main/java/com/dev/careers/model/Curator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.dev.careers.model;

import javax.validation.constraints.Email;
import javax.validation.constraints.Pattern;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.Nullable;

@Getter
@RequiredArgsConstructor
public class Curator {
@Nullable
private int id;

@NonNull
@Email(message = "Email Format Violation")
private String email;

@NonNull
private String name;

//최소 8자리에 숫자, 문자, 특수문자 각각 1개 이상 포함
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[$@$!%*#?&])[A-Za-z\\d$@$!%*#?&]{8,}$",
message = "Password Format Violation")
@NonNull
private String password;

@Nullable
private String salt;
}
29 changes: 29 additions & 0 deletions src/main/java/com/dev/careers/service/CuratorService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dev.careers.service;

import com.dev.careers.mapper.CuratorMapper;
import com.dev.careers.model.Curator;
import com.dev.careers.service.encryption.PasswordEncryptor;
import com.dev.careers.service.error.DuplicatedEmailException;
import java.security.NoSuchAlgorithmException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class CuratorService {
private final CuratorMapper curatorMapper;
private final PasswordEncryptor passwordEncryptor;

public void join(Curator curator) throws NoSuchAlgorithmException {
//중복검증
if (curatorMapper.checkEmailExists(curator.getEmail()))
throw new DuplicatedEmailException();

String salt = passwordEncryptor.makeSalt();
curatorMapper.insertCurator(
curator.getEmail(),
curator.getName(),
passwordEncryptor.hashing(curator.getPassword().getBytes(), salt),
salt);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dev.careers.service.encryption;

import java.security.NoSuchAlgorithmException;

public interface PasswordEncryptor {
String makeSalt();
String hashing(byte[] password, String salt) throws NoSuchAlgorithmException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.dev.careers.service.encryption;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import org.springframework.stereotype.Component;

@Component
public class SHA256Encryptor implements PasswordEncryptor {
private final static int SALT_SIZE = 16;

@Override
public String makeSalt() {
SecureRandom srd = new SecureRandom();
byte[] data = new byte[SALT_SIZE];
srd.nextBytes(data);

return byteArrayToString(data);
}

//Salt와 키 스트레칭 방식으로 구현
@Override
public String hashing(byte[] password, String salt) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");

StringBuilder builder = new StringBuilder();
for (int i = 0; i < 10; i++) {
builder.append(byteArrayToString(password));
builder.append(salt);
md.update(builder.toString().getBytes());
password = md.digest();
}
return new String(password);
}

public String byteArrayToString(byte[] bytes) {
StringBuilder builder = new StringBuilder();
for (byte data : bytes) {
builder.append(String.format("%02X ", data));
}
return builder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dev.careers.service.error;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class CuratorExceptionHandler {

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = {DuplicatedEmailException.class, ViolationException.class})
public void badRequest(final RuntimeException ex) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dev.careers.service.error;

public class DuplicatedEmailException extends RuntimeException {

public DuplicatedEmailException() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.dev.careers.service.error;

public class ViolationException extends RuntimeException {

public ViolationException() {
}
}
10 changes: 7 additions & 3 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
spring.datasource.url=jdbc:mysql://localhost:3306/<디비이름>?useUnicode=true@characterEncoding=utf8&serverTimezone=Asia/Seoul
spring.datasource.username=<유저이름>
spring.datasource.password=<유저비밀번호>
spring.datasource.url=jdbc:mysql://localhost:3306/Careers?useUnicode=true@characterEncoding=utf8&serverTimezone=Asia/Seoul
#<유저이름>
spring.datasource.username=dev
# <유저비밀번호>
spring.datasource.password=1234
mybatis.type-aliases-package=com.dev.careers.model
mybatis.mapper-locations=mybatis/*.xml
12 changes: 12 additions & 0 deletions src/main/resources/mybatis/CuratorMapper.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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.dev.careers.mapper.CuratorMapper">
<select id="insertCurator" resultType="int">
INSERT INTO Curator (email, name, password, salt)
VALUES (#{email}, #{name}, #{password}, #{salt});
</select>

<select id="checkEmailExists" resultType="boolean">
select exists(select 1 from curator where email = #{email});
</select>
</mapper>
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.dev.careers.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.dev.careers.service.error.CuratorExceptionHandler;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@Transactional
class CuratorControllerTest {

@Autowired
CuratorController curatorController;
MockMvc mockMvc;

@BeforeEach
public void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(curatorController)
.setControllerAdvice(new CuratorExceptionHandler()).build();
}

@Test
@DisplayName("정상적인 회원가입")
public void joinCurator() throws Exception {
mockMvc.perform(post("/curators/join")
.param("email", "test@google.com")
.param("name", "홍길동")
.param("password", "test123!@"))
.andDo(print())
.andExpect(status().isOk());
}

@Test
@DisplayName("잘못된 이메일 형식 요청")
public void violationEmail() throws Exception {
mockMvc.perform(post("/curators/join")
.param("email", "test123.com")
.param("name", "홍길동")
.param("password", "test123!@"))
.andDo(print())
.andExpect(status().isBadRequest());
}

@Test
@DisplayName("잘못된 비밀번호 형식 요청")
public void violationPassword() throws Exception {
mockMvc.perform(post("/curators/join")
.param("email", "test@google.com")
.param("name", "홍길동")
.param("password", "123"))
.andDo(print())
.andExpect(status().isBadRequest());
}
}
31 changes: 31 additions & 0 deletions src/test/java/com/dev/careers/service/CuratorServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.dev.careers.service;

import com.dev.careers.model.Curator;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.transaction.annotation.Transactional;

@Transactional
@SpringBootTest
class CuratorServiceTest {

@Autowired
CuratorService curatorService;

@Test
@DisplayName("중복된 이메일 회원가입 요청")
public void dupicatedEmail() throws Exception {
Curator curator = new Curator(
"test@google.com",
"홍길동",
"test123!@"
);
curatorService.join(curator);
org.junit.jupiter.api.Assertions.assertThrows(
DuplicateKeyException.class,
() -> curatorService.join(curator));
}
}

0 comments on commit 60ed0cc

Please sign in to comment.