From fc45a1d404ea12921d2f58d8f7c16c1c55c7fabc Mon Sep 17 00:00:00 2001 From: Taeyoon Date: Sat, 26 Sep 2020 16:37:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 +- .../config/MessageException.java | 7 + .../RestResponseEntityExceptionHandler.java | 37 ++++ .../constant/CommonConstant.java | 3 +- .../constant/RequestUrlConstant.java | 8 + .../controller/MemberController.java | 47 ++++- .../onlineshoppingmall/domain/Member.java | 70 +++++--- .../domain/ResponseData.java | 24 +++ .../repository/MemberRepository.java | 7 +- .../service/MemberService.java | 52 ++++++ .../onlineshoppingmall/util/EncryptUtil.java | 166 ++++++++++++++++++ src/main/resources/application.yml | 13 +- .../repository/MemberRepositoryTest.java | 4 +- 13 files changed, 415 insertions(+), 33 deletions(-) create mode 100644 src/main/java/me/naming/onlineshoppingmall/config/MessageException.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/service/MemberService.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java diff --git a/build.gradle b/build.gradle index 7fa3740..9173b56 100644 --- a/build.gradle +++ b/build.gradle @@ -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' + 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' diff --git a/src/main/java/me/naming/onlineshoppingmall/config/MessageException.java b/src/main/java/me/naming/onlineshoppingmall/config/MessageException.java new file mode 100644 index 0000000..3834a1e --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/config/MessageException.java @@ -0,0 +1,7 @@ +package me.naming.onlineshoppingmall.config; + +public class MessageException extends RuntimeException { + public MessageException(String message) { + super(message); + } +} diff --git a/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java b/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java new file mode 100644 index 0000000..7141769 --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java b/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java index 7022c30..54c2100 100644 --- a/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java +++ b/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java @@ -2,7 +2,8 @@ //상수만을 관리하기 위한 클래스 public class CommonConstant { - private CommonConstant() {}; + public static final String UTF8 = "UTF-8"; + } diff --git a/src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java b/src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java new file mode 100644 index 0000000..c078d55 --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java @@ -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"; + + public static final String LOGIN = "/login"; +} diff --git a/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java b/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java index bfa0ad8..c8f7d76 100644 --- a/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java +++ b/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java @@ -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())); + } + @Getter + private static class UserLoginRequest { + @NonNull String email; + @NonNull String password; } } diff --git a/src/main/java/me/naming/onlineshoppingmall/domain/Member.java b/src/main/java/me/naming/onlineshoppingmall/domain/Member.java index 12e7241..ad6b545 100644 --- a/src/main/java/me/naming/onlineshoppingmall/domain/Member.java +++ b/src/main/java/me/naming/onlineshoppingmall/domain/Member.java @@ -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; @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java b/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java new file mode 100644 index 0000000..1b44c5f --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java @@ -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; + } +} diff --git a/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java b/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java index 31d259e..7269549 100644 --- a/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java +++ b/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java @@ -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 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); +} \ No newline at end of file diff --git a/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java b/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java new file mode 100644 index 0000000..5d84cc2 --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java @@ -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 + 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); + return encodeMemNo; + } +} diff --git a/src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java b/src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java new file mode 100644 index 0000000..7f02aec --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java @@ -0,0 +1,166 @@ +package me.naming.onlineshoppingmall.util; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import me.naming.onlineshoppingmall.config.MessageException; +import me.naming.onlineshoppingmall.constant.CommonConstant; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * [ 단방향 암호화 방식이란?(ex. SHA ...) ] + * : 암호화된 메시지로는 원본 메시지를 구할 수 없는 방식을 '단방향성'이라고 부릅니다. + * 암호화된 메시지. 즉, 다이제스트를 통해 원본 메시지를 유추하기 어려우며 원본 메세지의 문자 한글자면 변경되고 + * 다이제스트 내용이 변경되는 것을 알 수 있습니다. + * + * - 단방향 해시 함수의 문제점은? + * 1) 인식 가능성 : 공격자가 다양한 다이제스트를 개별적으로 모으고 원본 메시지를 찾아내거나 동일한 효과의 메시지를 찾을 수 있습니다. + * 2) 속도 : 해시 함수 자체가 짧은 시간에 데이터를 검색하기 위해 설계된 것입니다. + * 이를 활용해 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 빠르게 비교할 수 있습니다. + * + * - 단방향 해시 함수 보완 + * 1) 솔팅 : 다이제스트를 생성할 때 추가되는 바이트 단위의 임의의 문자열입니다. + * 해당 방법을 사용하면 공격자가 원본 메시지를 알아내더라도 추가적으로 솔팅을 알아내야 하며 + * 위에서 언급한 '인식 가능성' 문제점을 개선 할 수 있습니다. + * 2) 키 스트레칭 : 원본 메시지를 반복적으로 다이제스트를 생성하는 것입니다. 단순 1회성으로 다이제스트를 생성하는 것이 아니라 + * 해시함수를 반복하여 시간 소요가 걸리게 만드는 것입니다. + * + * [ 양방향 암호화 방식이란?(ex. AES, RSA ..) ] + * : 양방향 알고리즘은 암호화된 다이제스트를 복호화 할 수 있는 알고리즘을 의미합니다. + * + * - 대칭키, 비대칭키 + * -> 대칭키란? 암복호화에 서로 동일한 키가 사용되는 암호화 방식 + * -> 비대칭키란? 암복호화에 서로 다른 키가 사용되는 암호화 방식이며, 하나의 키는 공개키로 사용됩니다. + * + * [ 결 론 ] + * : 사용자의 비밀번호를 선택함에 있어서는 단방향 암호화로 저장 관리되야 합니다. 왜냐하면, 사용자 본인 외에는 비밀번호 원문을 알 필요가 없기 때문입니다. + * 반면 사용자를 인증하기 위한 정보. 예를 들면, 로그인 이후 회원 번호를 통해 사용자를 인증한다면, 사용자 본인이 회원 번호를 인지 할 필요가 없습니다. + * 이런 경우 서비스 내부적으로만 암복호화 시켜 관리해주면 됨으로 양방향 암호화를 통해 관리해주면 된다고 생각합니다. + * + * [참고] + * - https://d2.naver.com/helloworld/318732 + * - https://dailyworker.github.io/AES-Algorithm-and-Chiper-mode/ + * - https://javaplant.tistory.com/26 + */ +@Component +public class EncryptUtil { + + @Value("${encrypt.aesKey}") + private String aesKey; + + @Value("${encrypt.shaKey}") + private String shaKey; + + private static final Logger logger = LogManager.getLogger(EncryptUtil.class); + + // 암호화 + public String aesEncode(String str) { + + byte[] encrypted = new byte[0]; + + try { + SecretKeySpec keySpec = getKeySpec(); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes())); + encrypted = cipher.doFinal(str.getBytes(CommonConstant.UTF8)); + + } catch (IllegalBlockSizeException e) { + logger.error("** IllegalBlockSizeException : {}", e); + } catch (BadPaddingException e) { + logger.error("** BadPaddingException : {}", e); + } catch (UnsupportedEncodingException e) { + logger.error("** UnsupportedEncodingException : {}", e); + } catch (InvalidKeyException e) { + logger.error("** InvalidKeyException : {}", e); + } catch (InvalidAlgorithmParameterException e) { + logger.error("** InvalidAlgorithmParameterException : {}", e); + } catch (NoSuchAlgorithmException e) { + logger.error("** NoSuchAlgorithmException : {}", e); + } catch (NoSuchPaddingException e) { + logger.error("** NoSuchPaddingException : {}", e); + } + return new String(Base64.encodeBase64(encrypted)); + } + + //복호화 + public String aesDecode(String str) { + String aesDecode = null; + + try { + SecretKeySpec keySpec = getKeySpec(); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes(CommonConstant.UTF8))); + + byte[] byteStr = Base64.decodeBase64(str.getBytes()); + aesDecode = new String(cipher.doFinal(byteStr), CommonConstant.UTF8); + + } catch (UnsupportedEncodingException e) { + logger.error("** Decode UnsupportedEncodingException : {}", e); + } catch (IllegalBlockSizeException e) { + logger.error("** Decode IllegalBlockSizeException : {}", e); + } catch (BadPaddingException e) { + logger.error("** Decode BadPaddingException : {}", e); + } catch (InvalidKeyException e) { + logger.error("** Decode InvalidKeyException : {}", e); + } catch (InvalidAlgorithmParameterException e) { + logger.error("** Decode InvalidAlgorithmParameterException : {}", e); + } catch (NoSuchAlgorithmException e) { + logger.error("** Decode NoSuchAlgorithmException : {}", e); + } catch (NoSuchPaddingException e) { + logger.error("** Decode NoSuchPaddingException : {}", e); + } + + if(StringUtils.isEmpty(aesDecode)) throw new MessageException("복호화 에러"); + return aesDecode; + } + + public String shaEncode(String password) { + String generatedPassword = null; + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + md.update(shaKey.getBytes(StandardCharsets.UTF_8)); + byte[] bytes = md.digest(password.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for(int i=0; i< bytes.length ;i++){ + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + generatedPassword = sb.toString(); + } catch (NoSuchAlgorithmException e) { + logger.error("** SHA512 NoSuchAlgorithmException : {}", e); + } + + if(StringUtils.isEmpty(generatedPassword)) { + throw new MessageException("비밀번호 암호화 에러"); + } + + return generatedPassword; + } + + private SecretKeySpec getKeySpec() throws UnsupportedEncodingException { + byte[] keyBytes = new byte[16]; + byte[] defaultByte = aesKey.substring(0, 16).getBytes(CommonConstant.UTF8); + + int len = defaultByte.length; + if (len > keyBytes.length) { + len = keyBytes.length; + } + + System.arraycopy(defaultByte, 0, keyBytes, 0, len); + return new SecretKeySpec(keyBytes, "AES"); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8fabe10..d103428 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -41,6 +41,10 @@ logging: file: name: ../logs/Online_Shopping_Mall/osm.log +encrypt: + aesKey: OnlineShoppingMall + shaKey: encryptPassword + --- spring: profiles: local @@ -65,9 +69,14 @@ spring: logging: level: org: + springframework.web: DEBUG hibernate: - SQL: debug + SQL: DEBUG type: descriptor: sql: - BasicBinder: trace \ No newline at end of file + BasicBinder: TRACE + +encrypt: + aesKey: OnlineShoppingMall + shaKey: encryptPassword \ No newline at end of file diff --git a/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java b/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java index f11d7bd..1cc7a26 100644 --- a/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java +++ b/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java @@ -36,9 +36,10 @@ class MemberRepositoryTest { // given Member member = Member.builder() .email("test@test.com") - .password("a1s2d3!@#") + .password("A1s2d3!@#232") .name("테스터") .gender("M") + .hp((long) 01022021234) .birthDate(tmpBirthDate) .build(); @@ -51,5 +52,6 @@ class MemberRepositoryTest { assertEquals(selectMember.getPassword(), member.getPassword()); assertEquals(selectMember.getName(), member.getName()); assertEquals(selectMember.getGender(), member.getGender()); + assertEquals(selectMember.getBirthDate(), member.getBirthDate()); } } \ No newline at end of file From 6070ae1a44d78983a45ddb38748d06e22d1c1e9f Mon Sep 17 00:00:00 2001 From: Taeyoon Date: Sun, 11 Oct 2020 21:56:14 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 13 +- .../config/MessageException.java | 7 - .../RestResponseEntityExceptionHandler.java | 9 +- .../constant/CommonConstant.java | 2 - .../constant/RequestUrlConstant.java | 8 - .../controller/MemberController.java | 17 +- .../onlineshoppingmall/domain/Member.java | 6 +- .../repository/MemberRepository.java | 5 +- .../service/AESServiceImpl.java | 103 +++++++++++ .../service/CryptoService.java | 43 +++++ .../service/MemberService.java | 42 +++-- .../service/SHAServiceImpl.java | 47 +++++ .../onlineshoppingmall/util/EncryptUtil.java | 166 ------------------ .../repository/MemberRepositoryTest.java | 13 +- .../service/MemberServiceTest.java | 132 ++++++++++++++ 15 files changed, 385 insertions(+), 228 deletions(-) delete mode 100644 src/main/java/me/naming/onlineshoppingmall/config/MessageException.java delete mode 100644 src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java create mode 100644 src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java delete mode 100644 src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java create mode 100644 src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java diff --git a/build.gradle b/build.gradle index 9173b56..0c5cffb 100644 --- a/build.gradle +++ b/build.gradle @@ -30,11 +30,9 @@ 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' 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' @@ -49,10 +47,15 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.hamcrest:hamcrest-library:2.2' + /** + * (10/11) Intellij 내부적으로 JUnit을 지원해주고 있습니다. + * 따라서 아래 jupiter를 설정하면, version conflict로 인해 테스트코드가 정상적으로 동작하지 않을 수 있습니다.(MemberService Test 코드 작성 중 발견) + * 참고 : https://youtrack.jetbrains.com/issue/IDEA-178404 + */ // JUnit5의 경우 api, engine을 함께 사용해야 한다. - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' - +// testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' +// testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + testRuntimeOnly 'com.h2database:h2' } diff --git a/src/main/java/me/naming/onlineshoppingmall/config/MessageException.java b/src/main/java/me/naming/onlineshoppingmall/config/MessageException.java deleted file mode 100644 index 3834a1e..0000000 --- a/src/main/java/me/naming/onlineshoppingmall/config/MessageException.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.naming.onlineshoppingmall.config; - -public class MessageException extends RuntimeException { - public MessageException(String message) { - super(message); - } -} diff --git a/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java b/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java index 7141769..02b77b7 100644 --- a/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java +++ b/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java @@ -1,6 +1,5 @@ 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; @@ -18,10 +17,9 @@ @ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { - @ExceptionHandler(value = { MessageException.class }) - protected ResponseEntity handleConflict(RuntimeException ex, WebRequest request) { + @ExceptionHandler(value = { RuntimeException.class }) + protected ResponseEntity 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(); @@ -32,6 +30,7 @@ protected ResponseEntity handleConflict(RuntimeException ex, WebRequest .path(path) .message(ex.getMessage()) .build(); - return handleExceptionInternal(ex, gson.toJson(responseData), httpHeaders, HttpStatus.INTERNAL_SERVER_ERROR, request); + + return new ResponseEntity<>(responseData, HttpStatus.INTERNAL_SERVER_ERROR); } } \ No newline at end of file diff --git a/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java b/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java index 54c2100..ebaaf09 100644 --- a/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java +++ b/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java @@ -4,6 +4,4 @@ public class CommonConstant { private CommonConstant() {}; - public static final String UTF8 = "UTF-8"; - } diff --git a/src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java b/src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java deleted file mode 100644 index c078d55..0000000 --- a/src/main/java/me/naming/onlineshoppingmall/constant/RequestUrlConstant.java +++ /dev/null @@ -1,8 +0,0 @@ -package me.naming.onlineshoppingmall.constant; - -public class RequestUrlConstant { - public static final String MEMBERS = "/members"; - public static final String MEMBERS_SIGNUP = MEMBERS+"/signup"; - - public static final String LOGIN = "/login"; -} diff --git a/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java b/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java index c8f7d76..444339a 100644 --- a/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java +++ b/src/main/java/me/naming/onlineshoppingmall/controller/MemberController.java @@ -1,12 +1,10 @@ package me.naming.onlineshoppingmall.controller; 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; @@ -22,28 +20,31 @@ @RestController public class MemberController { + private final MemberService memberService; + @Autowired - private MemberService memberService; + public MemberController(MemberService memberService) { + this.memberService = memberService; + } // 회원가입 - @PostMapping(value = RequestUrlConstant.MEMBERS_SIGNUP) + @PostMapping(value = "/members/signup") @ResponseStatus(HttpStatus.CREATED) public void memberJoin(@RequestBody @Valid Member member) { memberService.signUp(member); } // 회원정보 갖고오기 - @GetMapping(value = RequestUrlConstant.MEMBERS +"/{memNo}") + @GetMapping(value = "/members/{memNo}") @ResponseStatus(HttpStatus.OK) public Member getMemberInfo(@PathVariable(name = "memNo") Long memNo) { return memberService.getMemberInfo(memNo); } - @GetMapping(value = RequestUrlConstant.LOGIN) + @GetMapping(value = "/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())); + memberService.doLogin(userLoginRequest.getEmail(), userLoginRequest.getPassword(), request); } @Getter diff --git a/src/main/java/me/naming/onlineshoppingmall/domain/Member.java b/src/main/java/me/naming/onlineshoppingmall/domain/Member.java index ad6b545..776b016 100644 --- a/src/main/java/me/naming/onlineshoppingmall/domain/Member.java +++ b/src/main/java/me/naming/onlineshoppingmall/domain/Member.java @@ -11,8 +11,6 @@ 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; @@ -44,8 +42,8 @@ public class Member { @Column(name = "PASSWORD") @NotNull @Setter - @Pattern(regexp = "^(?=.*\\d)(?=.*[~`!@#$%\\^&*()-])(?=.*[a-z])(?=.*[A-Z]).{9,12}$", - message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 9자 ~ 12자의 비밀번호여야 합니다.") +// @Pattern(regexp = "^(?=.*\\d)(?=.*[~`!@#$%\\^&*()-])(?=.*[a-z])(?=.*[A-Z]).{9,12}$", +// message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 9자 ~ 12자의 비밀번호여야 합니다.") private String password; @Column(name = "NAME", length = 30) diff --git a/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java b/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java index 7269549..e6d35c7 100644 --- a/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java +++ b/src/main/java/me/naming/onlineshoppingmall/repository/MemberRepository.java @@ -2,12 +2,9 @@ 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 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); + Member findMemnoAndPasswordByEmail(String email); } \ No newline at end of file diff --git a/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java b/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java new file mode 100644 index 0000000..1ec3799 --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java @@ -0,0 +1,103 @@ +package me.naming.onlineshoppingmall.service; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class AESServiceImpl implements CryptoService{ + + @Value("${encrypt.aesKey}") + private String aesKey; + + private static final Logger logger = LogManager.getLogger(AESServiceImpl.class); + + @Override + public String encrypt(String plainText) { + + byte[] encrypted = new byte[0]; + + try { + SecretKeySpec keySpec = getKeySpec(); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes())); + encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8.name())); + + } catch (IllegalBlockSizeException e) { + logger.error("** IllegalBlockSizeException : {}", e); + } catch (BadPaddingException e) { + logger.error("** BadPaddingException : {}", e); + } catch (UnsupportedEncodingException e) { + logger.error("** UnsupportedEncodingException : {}", e); + } catch (InvalidKeyException e) { + logger.error("** InvalidKeyException : {}", e); + } catch (InvalidAlgorithmParameterException e) { + logger.error("** InvalidAlgorithmParameterException : {}", e); + } catch (NoSuchAlgorithmException e) { + logger.error("** NoSuchAlgorithmException : {}", e); + } catch (NoSuchPaddingException e) { + logger.error("** NoSuchPaddingException : {}", e); + } + return new String(Base64.encodeBase64(encrypted)); + } + + @Override + public String decrypt(String encryptText) { + String aesDecode = null; + + try { + SecretKeySpec keySpec = getKeySpec(); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes(StandardCharsets.UTF_8.name()))); + + byte[] byteStr = Base64.decodeBase64(encryptText.getBytes()); + aesDecode = new String(cipher.doFinal(byteStr), StandardCharsets.UTF_8.name()); + + } catch (UnsupportedEncodingException e) { + logger.error("** Decode UnsupportedEncodingException : {}", e); + } catch (IllegalBlockSizeException e) { + logger.error("** Decode IllegalBlockSizeException : {}", e); + } catch (BadPaddingException e) { + logger.error("** Decode BadPaddingException : {}", e); + } catch (InvalidKeyException e) { + logger.error("** Decode InvalidKeyException : {}", e); + } catch (InvalidAlgorithmParameterException e) { + logger.error("** Decode InvalidAlgorithmParameterException : {}", e); + } catch (NoSuchAlgorithmException e) { + logger.error("** Decode NoSuchAlgorithmException : {}", e); + } catch (NoSuchPaddingException e) { + logger.error("** Decode NoSuchPaddingException : {}", e); + } + + if(StringUtils.isEmpty(aesDecode)) throw new RuntimeException("복호화 에러"); + return aesDecode; + } + + private SecretKeySpec getKeySpec() throws UnsupportedEncodingException { + byte[] keyBytes = new byte[16]; + byte[] defaultByte = aesKey.substring(0, 16).getBytes(StandardCharsets.UTF_8.name()); + + int len = defaultByte.length; + if (len > keyBytes.length) { + len = keyBytes.length; + } + + System.arraycopy(defaultByte, 0, keyBytes, 0, len); + return new SecretKeySpec(keyBytes, "AES"); + } +} diff --git a/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java b/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java new file mode 100644 index 0000000..289472d --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java @@ -0,0 +1,43 @@ +package me.naming.onlineshoppingmall.service; + +/** + * [ 단방향 암호화 방식이란?(ex. SHA ...) ] + * : 암호화된 메시지로는 원본 메시지를 구할 수 없는 방식을 '단방향성'이라고 부릅니다. + * 암호화된 메시지. 즉, 다이제스트를 통해 원본 메시지를 유추하기 어려우며 원본 메세지의 문자 한글자면 변경되고 + * 다이제스트 내용이 변경되는 것을 알 수 있습니다. + * + * - 단방향 해시 함수의 문제점은? + * 1) 인식 가능성 : 공격자가 다양한 다이제스트를 개별적으로 모으고 원본 메시지를 찾아내거나 동일한 효과의 메시지를 찾을 수 있습니다. + * 2) 속도 : 해시 함수 자체가 짧은 시간에 데이터를 검색하기 위해 설계된 것입니다. + * 이를 활용해 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 빠르게 비교할 수 있습니다. + * + * - 단방향 해시 함수 보완 + * 1) 솔팅 : 다이제스트를 생성할 때 추가되는 바이트 단위의 임의의 문자열입니다. + * 해당 방법을 사용하면 공격자가 원본 메시지를 알아내더라도 추가적으로 솔팅을 알아내야 하며 + * 위에서 언급한 '인식 가능성' 문제점을 개선 할 수 있습니다. + * 2) 키 스트레칭 : 원본 메시지를 반복적으로 다이제스트를 생성하는 것입니다. 단순 1회성으로 다이제스트를 생성하는 것이 아니라 + * 해시함수를 반복하여 시간 소요가 걸리게 만드는 것입니다. + * + * [ 양방향 암호화 방식이란?(ex. AES, RSA ..) ] + * : 양방향 알고리즘은 암호화된 다이제스트를 복호화 할 수 있는 알고리즘을 의미합니다. + * + * - 대칭키, 비대칭키 + * -> 대칭키란? 암복호화에 서로 동일한 키가 사용되는 암호화 방식 + * -> 비대칭키란? 암복호화에 서로 다른 키가 사용되는 암호화 방식이며, 하나의 키는 공개키로 사용됩니다. + * + * [ 결 론 ] + * : 사용자의 비밀번호를 선택함에 있어서는 단방향 암호화로 저장 관리되야 합니다. 왜냐하면, 사용자 본인 외에는 비밀번호 원문을 알 필요가 없기 때문입니다. + * 반면 사용자를 인증하기 위한 정보. 예를 들면, 로그인 이후 회원 번호를 통해 사용자를 인증한다면, 사용자 본인이 회원 번호를 인지 할 필요가 없습니다. + * 이런 경우 서비스 내부적으로만 암복호화 시켜 관리해주면 됨으로 양방향 암호화를 통해 관리해주면 된다고 생각합니다. + * + * [참고] + * - https://d2.naver.com/helloworld/318732 + * - https://dailyworker.github.io/AES-Algorithm-and-Chiper-mode/ + * - https://javaplant.tistory.com/26 + */ +public interface CryptoService { + + public String encrypt(String plainText); + public String decrypt(String encryptText); + +} diff --git a/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java b/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java index 5d84cc2..3b78e01 100644 --- a/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java +++ b/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java @@ -1,9 +1,9 @@ package me.naming.onlineshoppingmall.service; -import me.naming.onlineshoppingmall.config.MessageException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; 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; @@ -13,26 +13,29 @@ @Service public class MemberService { - @Autowired - private MemberRepository memberRepository; + private final MemberRepository memberRepository; + private final CryptoService shaService; + private final CryptoService aesService; @Autowired - private EncryptUtil encryptUtil; + public MemberService(SHAServiceImpl shaService, AESServiceImpl aesService, + MemberRepository memberRepository) { + this.shaService = shaService; + this.aesService = aesService; + this.memberRepository = memberRepository; + } private final Logger logger = LogManager.getLogger(MemberService.class); - /** - * @description : 회원가입 - */ public void signUp(Member member) { if (memberRepository.findByEmail(member.getEmail()) != null) { - throw new MessageException("이미 가입된 이메일 주소입니다."); + throw new RuntimeException("이미 가입된 이메일 주소입니다."); } member.setMemberStatus(Member.MemberStatus.DEFAULT); // 비밀번호 암호화(SHA-512) - String encodePassword = encryptUtil.shaEncode(member.getPassword()); + String encodePassword = shaService.encrypt(member.getPassword()); if (StringUtils.isNotEmpty(encodePassword)) { member.setPassword(encodePassword); memberRepository.save(member); @@ -43,10 +46,19 @@ 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); - return encodeMemNo; + public void doLogin(String email, String password, HttpServletRequest request) { + HttpSession httpSession = request.getSession(); + + Member member = memberRepository.findMemnoAndPasswordByEmail(email); + String encodePassword = shaService.encrypt(password); + if(member == null) { + throw new RuntimeException("존재하지 않는 이메일 주소 입니다."); + } + if(!member.getPassword().equals(encodePassword)) { + throw new RuntimeException("비밀번호가 틀렸습니다"); + } + + String encodeMemNo = aesService.encrypt(String.valueOf(member.getMemNo())); + httpSession.setAttribute("loginInfo", encodeMemNo); } } diff --git a/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java b/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java new file mode 100644 index 0000000..2889042 --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java @@ -0,0 +1,47 @@ +package me.naming.onlineshoppingmall.service; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class SHAServiceImpl implements CryptoService { + + @Value("${encrypt.shaKey}") + private String shaKey; + + private static final Logger logger = LogManager.getLogger(SHAServiceImpl.class); + + @Override + public String encrypt(String plainText) { + String generatedPassword = null; + try { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + md.update(shaKey.getBytes(StandardCharsets.UTF_8)); + byte[] bytes = md.digest(plainText.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + for(int i=0; i< bytes.length ;i++){ + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + generatedPassword = sb.toString(); + } catch (NoSuchAlgorithmException e) { + logger.error("** SHA512 NoSuchAlgorithmException : {}", e); + } + + if(StringUtils.isEmpty(generatedPassword)) { + throw new RuntimeException("비밀번호 암호화 에러"); + } + + return generatedPassword; + } + + @Override + public String decrypt(String encryptText) { + return null; + } +} diff --git a/src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java b/src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java deleted file mode 100644 index 7f02aec..0000000 --- a/src/main/java/me/naming/onlineshoppingmall/util/EncryptUtil.java +++ /dev/null @@ -1,166 +0,0 @@ -package me.naming.onlineshoppingmall.util; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import me.naming.onlineshoppingmall.config.MessageException; -import me.naming.onlineshoppingmall.constant.CommonConstant; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -/** - * [ 단방향 암호화 방식이란?(ex. SHA ...) ] - * : 암호화된 메시지로는 원본 메시지를 구할 수 없는 방식을 '단방향성'이라고 부릅니다. - * 암호화된 메시지. 즉, 다이제스트를 통해 원본 메시지를 유추하기 어려우며 원본 메세지의 문자 한글자면 변경되고 - * 다이제스트 내용이 변경되는 것을 알 수 있습니다. - * - * - 단방향 해시 함수의 문제점은? - * 1) 인식 가능성 : 공격자가 다양한 다이제스트를 개별적으로 모으고 원본 메시지를 찾아내거나 동일한 효과의 메시지를 찾을 수 있습니다. - * 2) 속도 : 해시 함수 자체가 짧은 시간에 데이터를 검색하기 위해 설계된 것입니다. - * 이를 활용해 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 빠르게 비교할 수 있습니다. - * - * - 단방향 해시 함수 보완 - * 1) 솔팅 : 다이제스트를 생성할 때 추가되는 바이트 단위의 임의의 문자열입니다. - * 해당 방법을 사용하면 공격자가 원본 메시지를 알아내더라도 추가적으로 솔팅을 알아내야 하며 - * 위에서 언급한 '인식 가능성' 문제점을 개선 할 수 있습니다. - * 2) 키 스트레칭 : 원본 메시지를 반복적으로 다이제스트를 생성하는 것입니다. 단순 1회성으로 다이제스트를 생성하는 것이 아니라 - * 해시함수를 반복하여 시간 소요가 걸리게 만드는 것입니다. - * - * [ 양방향 암호화 방식이란?(ex. AES, RSA ..) ] - * : 양방향 알고리즘은 암호화된 다이제스트를 복호화 할 수 있는 알고리즘을 의미합니다. - * - * - 대칭키, 비대칭키 - * -> 대칭키란? 암복호화에 서로 동일한 키가 사용되는 암호화 방식 - * -> 비대칭키란? 암복호화에 서로 다른 키가 사용되는 암호화 방식이며, 하나의 키는 공개키로 사용됩니다. - * - * [ 결 론 ] - * : 사용자의 비밀번호를 선택함에 있어서는 단방향 암호화로 저장 관리되야 합니다. 왜냐하면, 사용자 본인 외에는 비밀번호 원문을 알 필요가 없기 때문입니다. - * 반면 사용자를 인증하기 위한 정보. 예를 들면, 로그인 이후 회원 번호를 통해 사용자를 인증한다면, 사용자 본인이 회원 번호를 인지 할 필요가 없습니다. - * 이런 경우 서비스 내부적으로만 암복호화 시켜 관리해주면 됨으로 양방향 암호화를 통해 관리해주면 된다고 생각합니다. - * - * [참고] - * - https://d2.naver.com/helloworld/318732 - * - https://dailyworker.github.io/AES-Algorithm-and-Chiper-mode/ - * - https://javaplant.tistory.com/26 - */ -@Component -public class EncryptUtil { - - @Value("${encrypt.aesKey}") - private String aesKey; - - @Value("${encrypt.shaKey}") - private String shaKey; - - private static final Logger logger = LogManager.getLogger(EncryptUtil.class); - - // 암호화 - public String aesEncode(String str) { - - byte[] encrypted = new byte[0]; - - try { - SecretKeySpec keySpec = getKeySpec(); - - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes())); - encrypted = cipher.doFinal(str.getBytes(CommonConstant.UTF8)); - - } catch (IllegalBlockSizeException e) { - logger.error("** IllegalBlockSizeException : {}", e); - } catch (BadPaddingException e) { - logger.error("** BadPaddingException : {}", e); - } catch (UnsupportedEncodingException e) { - logger.error("** UnsupportedEncodingException : {}", e); - } catch (InvalidKeyException e) { - logger.error("** InvalidKeyException : {}", e); - } catch (InvalidAlgorithmParameterException e) { - logger.error("** InvalidAlgorithmParameterException : {}", e); - } catch (NoSuchAlgorithmException e) { - logger.error("** NoSuchAlgorithmException : {}", e); - } catch (NoSuchPaddingException e) { - logger.error("** NoSuchPaddingException : {}", e); - } - return new String(Base64.encodeBase64(encrypted)); - } - - //복호화 - public String aesDecode(String str) { - String aesDecode = null; - - try { - SecretKeySpec keySpec = getKeySpec(); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes(CommonConstant.UTF8))); - - byte[] byteStr = Base64.decodeBase64(str.getBytes()); - aesDecode = new String(cipher.doFinal(byteStr), CommonConstant.UTF8); - - } catch (UnsupportedEncodingException e) { - logger.error("** Decode UnsupportedEncodingException : {}", e); - } catch (IllegalBlockSizeException e) { - logger.error("** Decode IllegalBlockSizeException : {}", e); - } catch (BadPaddingException e) { - logger.error("** Decode BadPaddingException : {}", e); - } catch (InvalidKeyException e) { - logger.error("** Decode InvalidKeyException : {}", e); - } catch (InvalidAlgorithmParameterException e) { - logger.error("** Decode InvalidAlgorithmParameterException : {}", e); - } catch (NoSuchAlgorithmException e) { - logger.error("** Decode NoSuchAlgorithmException : {}", e); - } catch (NoSuchPaddingException e) { - logger.error("** Decode NoSuchPaddingException : {}", e); - } - - if(StringUtils.isEmpty(aesDecode)) throw new MessageException("복호화 에러"); - return aesDecode; - } - - public String shaEncode(String password) { - String generatedPassword = null; - try { - MessageDigest md = MessageDigest.getInstance("SHA-512"); - md.update(shaKey.getBytes(StandardCharsets.UTF_8)); - byte[] bytes = md.digest(password.getBytes(StandardCharsets.UTF_8)); - StringBuilder sb = new StringBuilder(); - for(int i=0; i< bytes.length ;i++){ - sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); - } - generatedPassword = sb.toString(); - } catch (NoSuchAlgorithmException e) { - logger.error("** SHA512 NoSuchAlgorithmException : {}", e); - } - - if(StringUtils.isEmpty(generatedPassword)) { - throw new MessageException("비밀번호 암호화 에러"); - } - - return generatedPassword; - } - - private SecretKeySpec getKeySpec() throws UnsupportedEncodingException { - byte[] keyBytes = new byte[16]; - byte[] defaultByte = aesKey.substring(0, 16).getBytes(CommonConstant.UTF8); - - int len = defaultByte.length; - if (len > keyBytes.length) { - len = keyBytes.length; - } - - System.arraycopy(defaultByte, 0, keyBytes, 0, len); - return new SecretKeySpec(keyBytes, "AES"); - } -} diff --git a/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java b/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java index 1cc7a26..1259ed2 100644 --- a/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java +++ b/src/test/java/me/naming/onlineshoppingmall/repository/MemberRepositoryTest.java @@ -20,6 +20,11 @@ class MemberRepositoryTest { private static final Logger logger = LogManager.getLogger(MemberRepositoryTest.class); + private static final String EMAIL = "test@test.com"; + private static final String PWD = "A1s2d3!@#232"; + private static final String NAME = "테스터"; + private static final String M_GENDER = "M"; + @Test public void 회원정보_저장() { @@ -35,10 +40,10 @@ class MemberRepositoryTest { // given Member member = Member.builder() - .email("test@test.com") - .password("A1s2d3!@#232") - .name("테스터") - .gender("M") + .email(EMAIL) + .password(PWD) + .name(NAME) + .gender(M_GENDER) .hp((long) 01022021234) .birthDate(tmpBirthDate) .build(); diff --git a/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java b/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java new file mode 100644 index 0000000..9985edb --- /dev/null +++ b/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java @@ -0,0 +1,132 @@ +package me.naming.onlineshoppingmall.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import me.naming.onlineshoppingmall.domain.Member; +import me.naming.onlineshoppingmall.repository.MemberRepository; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; + +@ExtendWith(MockitoExtension.class) +public class MemberServiceTest { + + private MemberService memberService; + + @Mock + private MemberRepository memberRepository; + @Mock + private AESServiceImpl aesService; + @Mock + private SHAServiceImpl shaService; + + private SHAServiceImpl autowiredSHAService = new SHAServiceImpl(); + + private Member testMember; + private static final String EMAIL = "test@test.com"; + private static final String PWD = "A1s2d3!@#232"; + private static final String NAME = "테스터"; + private static final String M_GENDER = "M"; + private static final Logger logger = LogManager.getLogger(MemberServiceTest.class); + + @BeforeEach + public void setUp() { + + String from = "1990/06/17"; + SimpleDateFormat transFormat = new SimpleDateFormat("yyyy/MM/dd"); + + Date tmpBirthDate = null; + try { + tmpBirthDate = transFormat.parse(from); + } catch (ParseException e) { + logger.error("Parsing Error : {}", e); + } + + // given + this.testMember = Member.builder() + .email(EMAIL) + .password(PWD) + .name(NAME) + .gender(M_GENDER) + .hp((long) 01022021234) + .birthDate(tmpBirthDate) + .build(); + } + + @Test + public void 가입된_이메일주소_확인() { + + //given + when(memberRepository.findByEmail(EMAIL)).thenReturn(testMember); + memberService = new MemberService(shaService, aesService, memberRepository); + + //when + Exception exception = assertThrows(RuntimeException.class, () -> { + memberService.signUp(testMember); + }); + + //then + assertEquals("이미 가입된 이메일 주소입니다.", exception.getMessage()); + } + + @Test + public void 로그인_실패_이메일주소_존재_X() { + + //given + MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); + when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(null); + memberService = new MemberService(shaService, aesService, memberRepository); + + //when + Exception exception = assertThrows(RuntimeException.class, () -> { + memberService.doLogin(EMAIL, PWD, mockHttpServletRequest); + }); + + //then + assertEquals("존재하지 않는 이메일 주소 입니다.", exception.getMessage()); + } + + @Test + public void 로그인_실패_비밀번호_일치_X() { + + //given + MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); + when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(testMember); + memberService = new MemberService(shaService, aesService, memberRepository); + + //when + Exception exception = assertThrows(RuntimeException.class, () -> { + memberService.doLogin(EMAIL, "testPassword", mockHttpServletRequest); + }); + + //then + assertEquals("비밀번호가 틀렸습니다", exception.getMessage()); + } + +// @Test +// public void 로그인_성공(){ +// //given +// MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); +// when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(testMember); +// memberService = new MemberService(shaService, aesService, memberRepository); +// +// //when +// memberService.doLogin(EMAIL, PWD, mockHttpServletRequest); +// +// //then +// assertNotNull(mockHttpServletRequest.getAttribute("loginInfo").toString()); +// } + +} From ac098bb41b14a811a54696800bb14eedae4c5f46 Mon Sep 17 00:00:00 2001 From: Taeyoon Date: Thu, 22 Oct 2020 01:20:37 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../RestResponseEntityExceptionHandler.java | 21 ++++---- .../constant/CommonConstant.java | 1 + .../onlineshoppingmall/domain/Member.java | 5 +- .../domain/ResponseData.java | 6 +-- .../exception/MessageException.java | 23 ++++++++ .../service/AESServiceImpl.java | 35 ++----------- .../service/MemberService.java | 22 ++++---- .../service/SHAServiceImpl.java | 8 ++- .../service/MemberServiceTest.java | 52 ++++++++++--------- 10 files changed, 87 insertions(+), 90 deletions(-) create mode 100644 src/main/java/me/naming/onlineshoppingmall/exception/MessageException.java diff --git a/build.gradle b/build.gradle index 0c5cffb..b8b73dc 100644 --- a/build.gradle +++ b/build.gradle @@ -47,6 +47,8 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.hamcrest:hamcrest-library:2.2' + + /** * (10/11) Intellij 내부적으로 JUnit을 지원해주고 있습니다. * 따라서 아래 jupiter를 설정하면, version conflict로 인해 테스트코드가 정상적으로 동작하지 않을 수 있습니다.(MemberService Test 코드 작성 중 발견) @@ -55,8 +57,6 @@ dependencies { // JUnit5의 경우 api, engine을 함께 사용해야 한다. // testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' // testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' - - testRuntimeOnly 'com.h2database:h2' } test { diff --git a/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java b/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java index 02b77b7..6a05b5d 100644 --- a/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java +++ b/src/main/java/me/naming/onlineshoppingmall/config/RestResponseEntityExceptionHandler.java @@ -2,9 +2,8 @@ import java.time.LocalDateTime; import me.naming.onlineshoppingmall.domain.ResponseData; -import org.springframework.http.HttpHeaders; +import me.naming.onlineshoppingmall.exception.MessageException; 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; @@ -17,18 +16,22 @@ @ControllerAdvice public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler { - @ExceptionHandler(value = { RuntimeException.class }) - protected ResponseEntity handleConflict(RuntimeException ex, WebRequest request) { - HttpHeaders httpHeaders = new HttpHeaders(); - - httpHeaders.setContentType(MediaType.valueOf(MediaType.APPLICATION_JSON_VALUE)); + @ExceptionHandler(value = { MessageException.class }) + protected ResponseEntity handleConflict(MessageException messageException, WebRequest request) { String path = ((ServletWebRequest)request).getRequest().getRequestURI(); + int code; + + if(messageException.getStatusCode() != 0) { + code = messageException.getStatusCode(); + } else { + code = HttpStatus.INTERNAL_SERVER_ERROR.value(); + } ResponseData responseData = ResponseData.builder() .localDateTime(LocalDateTime.now()) - .status(HttpStatus.INTERNAL_SERVER_ERROR) + .statusCode(code) .path(path) - .message(ex.getMessage()) + .message(messageException.getMessage()) .build(); return new ResponseEntity<>(responseData, HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java b/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java index ebaaf09..dd13c8d 100644 --- a/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java +++ b/src/main/java/me/naming/onlineshoppingmall/constant/CommonConstant.java @@ -4,4 +4,5 @@ public class CommonConstant { private CommonConstant() {}; + public static final String LOGIN_INFO = "loginInfo"; } diff --git a/src/main/java/me/naming/onlineshoppingmall/domain/Member.java b/src/main/java/me/naming/onlineshoppingmall/domain/Member.java index 776b016..7f2465c 100644 --- a/src/main/java/me/naming/onlineshoppingmall/domain/Member.java +++ b/src/main/java/me/naming/onlineshoppingmall/domain/Member.java @@ -34,7 +34,6 @@ public class Member { @Column(name = "EMAIL") @NotNull -// @Pattern(regexp = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$") @Email(message = "이메일 형식에 맞지 않습니다.") private String email; @@ -42,8 +41,8 @@ public class Member { @Column(name = "PASSWORD") @NotNull @Setter -// @Pattern(regexp = "^(?=.*\\d)(?=.*[~`!@#$%\\^&*()-])(?=.*[a-z])(?=.*[A-Z]).{9,12}$", -// message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 9자 ~ 12자의 비밀번호여야 합니다.") + @Pattern(regexp = "^(?=.*\\d)(?=.*[~`!@#$%\\^&*()-])(?=.*[a-z])(?=.*[A-Z]).{9,12}$", + message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 9자 ~ 12자의 비밀번호여야 합니다.") private String password; @Column(name = "NAME", length = 30) diff --git a/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java b/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java index 1b44c5f..468424c 100644 --- a/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java +++ b/src/main/java/me/naming/onlineshoppingmall/domain/ResponseData.java @@ -11,13 +11,13 @@ @Getter public class ResponseData { private LocalDateTime localDateTime; - private HttpStatus status; + private int statusCode; private String path; private String message; - public ResponseData(LocalDateTime localDateTime, HttpStatus status, String path, String message) { + public ResponseData(LocalDateTime localDateTime, int statusCode, String path, String message) { this.localDateTime = localDateTime; - this.status = status; + this.statusCode = statusCode; this.path = path; this.message = message; } diff --git a/src/main/java/me/naming/onlineshoppingmall/exception/MessageException.java b/src/main/java/me/naming/onlineshoppingmall/exception/MessageException.java new file mode 100644 index 0000000..9e42bd2 --- /dev/null +++ b/src/main/java/me/naming/onlineshoppingmall/exception/MessageException.java @@ -0,0 +1,23 @@ +package me.naming.onlineshoppingmall.exception; + +import org.springframework.http.HttpStatus; + +public class MessageException extends RuntimeException{ + + private int statusCode; + + public MessageException(String message) { + super(message); + this.statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); + } + + public MessageException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + +} diff --git a/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java b/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java index 1ec3799..640e6c8 100644 --- a/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java +++ b/src/main/java/me/naming/onlineshoppingmall/service/AESServiceImpl.java @@ -2,13 +2,8 @@ import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import javax.crypto.BadPaddingException; +import java.security.GeneralSecurityException; import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; @@ -38,20 +33,10 @@ public String encrypt(String plainText) { cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(aesKey.substring(0, 16).getBytes())); encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8.name())); - } catch (IllegalBlockSizeException e) { - logger.error("** IllegalBlockSizeException : {}", e); - } catch (BadPaddingException e) { - logger.error("** BadPaddingException : {}", e); + } catch (GeneralSecurityException e) { + logger.error("** GeneralSecurityException : {}", e); } catch (UnsupportedEncodingException e) { logger.error("** UnsupportedEncodingException : {}", e); - } catch (InvalidKeyException e) { - logger.error("** InvalidKeyException : {}", e); - } catch (InvalidAlgorithmParameterException e) { - logger.error("** InvalidAlgorithmParameterException : {}", e); - } catch (NoSuchAlgorithmException e) { - logger.error("** NoSuchAlgorithmException : {}", e); - } catch (NoSuchPaddingException e) { - logger.error("** NoSuchPaddingException : {}", e); } return new String(Base64.encodeBase64(encrypted)); } @@ -68,20 +53,8 @@ public String decrypt(String encryptText) { byte[] byteStr = Base64.decodeBase64(encryptText.getBytes()); aesDecode = new String(cipher.doFinal(byteStr), StandardCharsets.UTF_8.name()); - } catch (UnsupportedEncodingException e) { + } catch (Exception e) { logger.error("** Decode UnsupportedEncodingException : {}", e); - } catch (IllegalBlockSizeException e) { - logger.error("** Decode IllegalBlockSizeException : {}", e); - } catch (BadPaddingException e) { - logger.error("** Decode BadPaddingException : {}", e); - } catch (InvalidKeyException e) { - logger.error("** Decode InvalidKeyException : {}", e); - } catch (InvalidAlgorithmParameterException e) { - logger.error("** Decode InvalidAlgorithmParameterException : {}", e); - } catch (NoSuchAlgorithmException e) { - logger.error("** Decode NoSuchAlgorithmException : {}", e); - } catch (NoSuchPaddingException e) { - logger.error("** Decode NoSuchPaddingException : {}", e); } if(StringUtils.isEmpty(aesDecode)) throw new RuntimeException("복호화 에러"); diff --git a/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java b/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java index 3b78e01..42067e6 100644 --- a/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java +++ b/src/main/java/me/naming/onlineshoppingmall/service/MemberService.java @@ -1,15 +1,16 @@ package me.naming.onlineshoppingmall.service; +import java.util.concurrent.RejectedExecutionException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; +import lombok.extern.log4j.Log4j2; +import me.naming.onlineshoppingmall.constant.CommonConstant; import me.naming.onlineshoppingmall.domain.Member; import me.naming.onlineshoppingmall.repository.MemberRepository; 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; +@Log4j2 @Service public class MemberService { @@ -17,20 +18,16 @@ public class MemberService { private final CryptoService shaService; private final CryptoService aesService; - @Autowired - public MemberService(SHAServiceImpl shaService, AESServiceImpl aesService, - MemberRepository memberRepository) { + public MemberService(SHAServiceImpl shaService, AESServiceImpl aesService, MemberRepository memberRepository) { this.shaService = shaService; this.aesService = aesService; this.memberRepository = memberRepository; } - private final Logger logger = LogManager.getLogger(MemberService.class); - public void signUp(Member member) { if (memberRepository.findByEmail(member.getEmail()) != null) { - throw new RuntimeException("이미 가입된 이메일 주소입니다."); + throw new RejectedExecutionException("이미 가입된 이메일 주소입니다."); } member.setMemberStatus(Member.MemberStatus.DEFAULT); @@ -52,13 +49,12 @@ public void doLogin(String email, String password, HttpServletRequest request) { Member member = memberRepository.findMemnoAndPasswordByEmail(email); String encodePassword = shaService.encrypt(password); if(member == null) { - throw new RuntimeException("존재하지 않는 이메일 주소 입니다."); + throw new NullPointerException("존재하지 않는 이메일 주소 입니다."); } if(!member.getPassword().equals(encodePassword)) { - throw new RuntimeException("비밀번호가 틀렸습니다"); + throw new IllegalArgumentException("비밀번호가 틀렸습니다"); } - String encodeMemNo = aesService.encrypt(String.valueOf(member.getMemNo())); - httpSession.setAttribute("loginInfo", encodeMemNo); + httpSession.setAttribute(CommonConstant.LOGIN_INFO, member.getMemNo()); } } diff --git a/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java b/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java index 2889042..0194e8e 100644 --- a/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java +++ b/src/main/java/me/naming/onlineshoppingmall/service/SHAServiceImpl.java @@ -3,20 +3,18 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +@Log4j2 @Service public class SHAServiceImpl implements CryptoService { @Value("${encrypt.shaKey}") private String shaKey; - private static final Logger logger = LogManager.getLogger(SHAServiceImpl.class); - @Override public String encrypt(String plainText) { String generatedPassword = null; @@ -30,7 +28,7 @@ public String encrypt(String plainText) { } generatedPassword = sb.toString(); } catch (NoSuchAlgorithmException e) { - logger.error("** SHA512 NoSuchAlgorithmException : {}", e); + log.error("** SHA512 NoSuchAlgorithmException : {}", e); } if(StringUtils.isEmpty(generatedPassword)) { diff --git a/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java b/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java index 9985edb..97f1e38 100644 --- a/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java +++ b/src/test/java/me/naming/onlineshoppingmall/service/MemberServiceTest.java @@ -3,18 +3,23 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.concurrent.RejectedExecutionException; +import me.naming.onlineshoppingmall.constant.CommonConstant; import me.naming.onlineshoppingmall.domain.Member; import me.naming.onlineshoppingmall.repository.MemberRepository; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; @@ -23,16 +28,14 @@ @ExtendWith(MockitoExtension.class) public class MemberServiceTest { - private MemberService memberService; - @Mock private MemberRepository memberRepository; @Mock private AESServiceImpl aesService; @Mock private SHAServiceImpl shaService; - - private SHAServiceImpl autowiredSHAService = new SHAServiceImpl(); + @InjectMocks + private MemberService memberService; private Member testMember; private static final String EMAIL = "test@test.com"; @@ -66,14 +69,14 @@ public void setUp() { } @Test - public void 가입된_이메일주소_확인() { + @DisplayName("가입된_이메일주소_확인") + public void emailIsExist() { //given when(memberRepository.findByEmail(EMAIL)).thenReturn(testMember); - memberService = new MemberService(shaService, aesService, memberRepository); //when - Exception exception = assertThrows(RuntimeException.class, () -> { + Exception exception = assertThrows(RejectedExecutionException.class, () -> { memberService.signUp(testMember); }); @@ -82,12 +85,12 @@ public void setUp() { } @Test - public void 로그인_실패_이메일주소_존재_X() { + @DisplayName("로그인_실패_이메일주소_존재_X") + public void loginFailed_emailIsNotExist() { //given MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(null); - memberService = new MemberService(shaService, aesService, memberRepository); //when Exception exception = assertThrows(RuntimeException.class, () -> { @@ -99,12 +102,12 @@ public void setUp() { } @Test - public void 로그인_실패_비밀번호_일치_X() { + @DisplayName("로그인_실패_비밀번호_일치_X") + public void loginFailed_wrongPassword() { //given MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(testMember); - memberService = new MemberService(shaService, aesService, memberRepository); //when Exception exception = assertThrows(RuntimeException.class, () -> { @@ -115,18 +118,19 @@ public void setUp() { assertEquals("비밀번호가 틀렸습니다", exception.getMessage()); } -// @Test -// public void 로그인_성공(){ -// //given -// MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); -// when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(testMember); -// memberService = new MemberService(shaService, aesService, memberRepository); -// -// //when -// memberService.doLogin(EMAIL, PWD, mockHttpServletRequest); -// -// //then -// assertNotNull(mockHttpServletRequest.getAttribute("loginInfo").toString()); -// } + @Test + @DisplayName("로그인_성공") + public void loginSuccess(){ + //given + MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); + when(memberRepository.findMemnoAndPasswordByEmail(EMAIL)).thenReturn(testMember); + when(shaService.encrypt(PWD)).thenReturn(PWD); + + //when + memberService.doLogin(EMAIL, PWD, mockHttpServletRequest); + + //then + assertNotNull(mockHttpServletRequest.getAttribute(CommonConstant.LOGIN_INFO).toString()); + } } From 7d7c9e0ac3ecb0fc9cdf33b7e0e677c2e2d565d8 Mon Sep 17 00:00:00 2001 From: Taeyoon Date: Thu, 22 Oct 2020 01:43:57 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EB=B0=8F=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 ------ .../service/CryptoService.java | 35 ------------------- 2 files changed, 45 deletions(-) diff --git a/build.gradle b/build.gradle index b8b73dc..1c2604e 100644 --- a/build.gradle +++ b/build.gradle @@ -47,16 +47,6 @@ dependencies { testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'org.hamcrest:hamcrest-library:2.2' - - - /** - * (10/11) Intellij 내부적으로 JUnit을 지원해주고 있습니다. - * 따라서 아래 jupiter를 설정하면, version conflict로 인해 테스트코드가 정상적으로 동작하지 않을 수 있습니다.(MemberService Test 코드 작성 중 발견) - * 참고 : https://youtrack.jetbrains.com/issue/IDEA-178404 - */ - // JUnit5의 경우 api, engine을 함께 사용해야 한다. -// testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' -// testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1' } test { diff --git a/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java b/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java index 289472d..705ea19 100644 --- a/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java +++ b/src/main/java/me/naming/onlineshoppingmall/service/CryptoService.java @@ -1,40 +1,5 @@ package me.naming.onlineshoppingmall.service; -/** - * [ 단방향 암호화 방식이란?(ex. SHA ...) ] - * : 암호화된 메시지로는 원본 메시지를 구할 수 없는 방식을 '단방향성'이라고 부릅니다. - * 암호화된 메시지. 즉, 다이제스트를 통해 원본 메시지를 유추하기 어려우며 원본 메세지의 문자 한글자면 변경되고 - * 다이제스트 내용이 변경되는 것을 알 수 있습니다. - * - * - 단방향 해시 함수의 문제점은? - * 1) 인식 가능성 : 공격자가 다양한 다이제스트를 개별적으로 모으고 원본 메시지를 찾아내거나 동일한 효과의 메시지를 찾을 수 있습니다. - * 2) 속도 : 해시 함수 자체가 짧은 시간에 데이터를 검색하기 위해 설계된 것입니다. - * 이를 활용해 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 빠르게 비교할 수 있습니다. - * - * - 단방향 해시 함수 보완 - * 1) 솔팅 : 다이제스트를 생성할 때 추가되는 바이트 단위의 임의의 문자열입니다. - * 해당 방법을 사용하면 공격자가 원본 메시지를 알아내더라도 추가적으로 솔팅을 알아내야 하며 - * 위에서 언급한 '인식 가능성' 문제점을 개선 할 수 있습니다. - * 2) 키 스트레칭 : 원본 메시지를 반복적으로 다이제스트를 생성하는 것입니다. 단순 1회성으로 다이제스트를 생성하는 것이 아니라 - * 해시함수를 반복하여 시간 소요가 걸리게 만드는 것입니다. - * - * [ 양방향 암호화 방식이란?(ex. AES, RSA ..) ] - * : 양방향 알고리즘은 암호화된 다이제스트를 복호화 할 수 있는 알고리즘을 의미합니다. - * - * - 대칭키, 비대칭키 - * -> 대칭키란? 암복호화에 서로 동일한 키가 사용되는 암호화 방식 - * -> 비대칭키란? 암복호화에 서로 다른 키가 사용되는 암호화 방식이며, 하나의 키는 공개키로 사용됩니다. - * - * [ 결 론 ] - * : 사용자의 비밀번호를 선택함에 있어서는 단방향 암호화로 저장 관리되야 합니다. 왜냐하면, 사용자 본인 외에는 비밀번호 원문을 알 필요가 없기 때문입니다. - * 반면 사용자를 인증하기 위한 정보. 예를 들면, 로그인 이후 회원 번호를 통해 사용자를 인증한다면, 사용자 본인이 회원 번호를 인지 할 필요가 없습니다. - * 이런 경우 서비스 내부적으로만 암복호화 시켜 관리해주면 됨으로 양방향 암호화를 통해 관리해주면 된다고 생각합니다. - * - * [참고] - * - https://d2.naver.com/helloworld/318732 - * - https://dailyworker.github.io/AES-Algorithm-and-Chiper-mode/ - * - https://javaplant.tistory.com/26 - */ public interface CryptoService { public String encrypt(String plainText);