Skip to content

Commit

Permalink
Merge pull request #68 from ClothingStoreService/feature/signup-email…
Browse files Browse the repository at this point in the history
…-verify

Feature/signup email verify
  • Loading branch information
hjj4060 authored Jul 5, 2024
2 parents 401e59f + b2c06c1 commit 25fb021
Show file tree
Hide file tree
Showing 40 changed files with 620 additions and 161 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ out/

### Custom Private Keys ###
src/main/resources/app.key
src/main/resources/app.pub
src/main/resources/app.pub

/src/main/generated/
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ dependencies {
//yml 암호화, 공식문서: https://github.com/ulisesbocchio/jasypt-spring-boot
//참고 블로그: https://velog.io/@bey1548/SpringBoot-application.yml-%EA%B0%92-%EC%95%94%ED%98%B8%ED%99%94%ED%95%98%EA%B8%B0jasypt
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5'

//mail 전송 의존성
implementation 'org.springframework.boot:spring-boot-starter-mail'

//Redis 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
"/v1/categories/**", "/v1/products/**", "/v1/productLines/**", "/v2/productLines/**",
"/v1/orderdetails", "/v1/orders",
"/v1/seller/orders/**", "/v1/seller/orders", "/v1/orders/**",
"/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**"
"/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/v1/members/auth/**"
).permitAll()
.requestMatchers(HttpMethod.POST, "/v1/members").permitAll()
.requestMatchers(HttpMethod.POST, "/v1/sellers/**").authenticated()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
Expand Down Expand Up @@ -93,13 +95,26 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
log.info("로그인 실패");

response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");

MessageDTO messageDTO = MessageDTOBuilder.buildMessage(HttpServletResponse.SC_UNAUTHORIZED, "이메일 또는 비밀번호가 올바르지 않습니다. 다시 확인해주세요.");
MessageDTO messageDTO = MessageDTOBuilder.buildMessage(HttpServletResponse.SC_UNAUTHORIZED, errorMessage(failed));
ObjectMapper om = new ObjectMapper();

response.getWriter().print(om.writeValueAsString(messageDTO));
}

private String errorMessage(AuthenticationException failed) {
String errorMessage = null;

if (failed instanceof BadCredentialsException) {
errorMessage = "이메일 또는 비밀번호가 올바르지 않습니다. 다시 확인해주세요.";
} else if (failed instanceof DisabledException) {
errorMessage = "계정이 비활성화 되어있습니다. 이메일 인증을 완료해주세요";
}

return errorMessage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
@Getter
public enum ErrorCode {
NOT_FOUND_REFRESH_TOKEN(HttpStatus.NOT_FOUND, "refresh 토큰이 없습니다."),
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "refresh 토큰이 만료되었거나 유효하지 않습니다.");
INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "refresh 토큰이 만료되었거나 유효하지 않습니다."),
INVALID_AUTH_CERTIFY_NUM(HttpStatus.BAD_REQUEST, "인증번호가 잘못 되었습니다.");

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.store.clothstar.common.error.exception;

import org.store.clothstar.common.error.ErrorCode;

public class SignupCertifyNumAuthFailedException extends RuntimeException {
private final ErrorCode errorCode;

public SignupCertifyNumAuthFailedException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,5 @@
public interface ExceptionType {
HttpStatus status();

int exceptionCode();

String message();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.MailException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand Down Expand Up @@ -35,6 +36,16 @@ protected ResponseEntity<ValidErrorResponseDTO> handleMethodArgumentNotValidExce
return new ResponseEntity<>(validErrorResponseDTO, HttpStatus.BAD_REQUEST);
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
private ResponseEntity<ErrorResponseDTO> mailException(MailException ex) {
log.error("[MailException Handler] {}", ex.getMessage());
ex.fillInStackTrace();
ErrorResponseDTO errorResponseDTO = new ErrorResponseDTO(HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage());

return new ResponseEntity<>(errorResponseDTO, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler
private ResponseEntity<ErrorResponseDTO> illegalArgumentHandler(IllegalArgumentException ex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.store.clothstar.common.mail;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;


@Service
@RequiredArgsConstructor
public class MailContentBuilder {
private final TemplateEngine templateEngine;

public String build(String certifyNum) {
Context context = new Context();
context.setVariable("certifyNum", certifyNum);
return templateEngine.process("mailTemplate", context);
}
}
14 changes: 14 additions & 0 deletions src/main/java/org/store/clothstar/common/mail/MailSendDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.store.clothstar.common.mail;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MailSendDTO {
private String address;
private String subject;
private String text;
}
34 changes: 34 additions & 0 deletions src/main/java/org/store/clothstar/common/mail/MailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.store.clothstar.common.mail;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Service;

@Service
@Slf4j
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender mailSender;

@Value("${email.send}")
private String fromAddress;

public boolean sendMail(MailSendDTO mailSendDTO) {
MimeMessagePreparator messagePreparator = mimeMessage -> {
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
messageHelper.setFrom(fromAddress);
messageHelper.setTo(mailSendDTO.getAddress());
messageHelper.setSubject(mailSendDTO.getSubject());
messageHelper.setText(mailSendDTO.getText(), true);
};

log.info("전송 메일주소 : {} -> {}", fromAddress, mailSendDTO.getAddress());
mailSender.send(messagePreparator);

return true;
}
}
21 changes: 21 additions & 0 deletions src/main/java/org/store/clothstar/common/redis/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.store.clothstar.common.redis;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
}
59 changes: 59 additions & 0 deletions src/main/java/org/store/clothstar/common/redis/RedisUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.store.clothstar.common.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Random;

@RequiredArgsConstructor
@Service
public class RedisUtil {
private final StringRedisTemplate template;

@Value("${spring.data.redis.duration}")
private int duration;

public String getData(String key) {
ValueOperations<String, String> valueOperations = template.opsForValue();
return valueOperations.get(key);
}

public boolean existData(String key) {
return Boolean.TRUE.equals(template.hasKey(key));
}

public void setDataExpire(String key, String value) {
ValueOperations<String, String> valueOperations = template.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}

public void deleteData(String key) {
template.delete(key);
}

public void createRedisData(String toEmail, String code) {
if (existData(toEmail)) {
deleteData(toEmail);
}

setDataExpire(toEmail, code);
}

public String createdCertifyNum() {
int leftLimit = 48; // number '0'
int rightLimit = 122; // alphabet 'z'
int targetStringLength = 6;
Random random = new Random();

return random.ints(leftLimit, rightLimit + 1)
.filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97))
.limit(targetStringLength)
.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ public void updateDeleteAt(Long memberId) {
public Long signup(CreateMemberRequest createMemberDTO) {
return memberService.signUp(createMemberDTO);
}

public void signupCertifyNumEmailSend(String email) {
memberService.signupCertifyNumEmailSend(email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.web.bind.annotation.RestController;
import org.store.clothstar.common.dto.SaveResponseDTO;
import org.store.clothstar.member.application.MemberServiceApplication;
import org.store.clothstar.member.dto.request.CertifyNumRequest;
import org.store.clothstar.member.dto.request.CreateMemberRequest;
import org.store.clothstar.member.dto.request.MemberLoginRequest;

Expand All @@ -32,7 +33,7 @@ public ResponseEntity<SaveResponseDTO> signup(@Validated @RequestBody CreateMemb
SaveResponseDTO saveResponseDTO = SaveResponseDTO.builder()
.id(memberId)
.statusCode(HttpStatus.OK.value())
.message("memberId : " + memberId + " 가 정상적으로 회원가입 되었습니다.")
.message(createMemberDTO.getEmail() + " 아이디로 회원가입이 완료 되었습니다.")
.build();

return new ResponseEntity<>(saveResponseDTO, HttpStatus.CREATED);
Expand All @@ -43,4 +44,10 @@ public ResponseEntity<SaveResponseDTO> signup(@Validated @RequestBody CreateMemb
public void login(@RequestBody MemberLoginRequest memberLoginRequest) {
// 실제 로그인 로직은 Spring Security에서 처리
}

@Operation(summary = "이메일로 인증번호 전송", description = "기입한 이메일로 인증번호를 전송합니다.")
@PostMapping("/v1/members/auth")
public void signupEmailAuthentication(@Validated @RequestBody CertifyNumRequest certifyNumRequest) {
memberServiceApplication.signupCertifyNumEmailSend(certifyNumRequest.getEmail());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.store.clothstar.member.dto.request;

import jakarta.validation.constraints.NotNull;
import lombok.*;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class CertifyNumRequest {
@NotNull(message = "이메일을 입력해 주세요")
private String email;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
package org.store.clothstar.member.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.*;
import lombok.*;
import org.store.clothstar.member.domain.Member;
import org.store.clothstar.member.domain.MemberGrade;
import org.store.clothstar.member.domain.MemberRole;
import org.store.clothstar.member.entity.MemberEntity;

import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -30,8 +24,11 @@ public class CreateMemberRequest {
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "유효하지 않은 전화번호 형식입니다.")
private String telNo;

public Member toMember(String encryptedPassword) {
return Member.builder()
@NotNull(message = "인증번호를 입력해 주세요")
private String certifyNum;

public MemberEntity toMemberEntity(String encryptedPassword) {
return MemberEntity.builder()
.email(email)
.password(encryptedPassword)
.name(name)
Expand All @@ -40,14 +37,13 @@ public Member toMember(String encryptedPassword) {
.point(0)
.role(MemberRole.USER)
.grade(MemberGrade.BRONZE)
.createdAt(LocalDateTime.now())
.build();
}

public MemberEntity toMemberEntity(String encryptedPassword) {
public MemberEntity toMemberEntity() {
return MemberEntity.builder()
.email(email)
.password(encryptedPassword)
.password(password)
.name(name)
.telNo(telNo)
.totalPaymentPrice(0)
Expand Down
Loading

0 comments on commit 25fb021

Please sign in to comment.