diff --git a/src/main/java/com/brainpix/api/code/error/AuthorityErrorCode.java b/src/main/java/com/brainpix/api/code/error/AuthorityErrorCode.java index f3e72b35..a8e2d952 100644 --- a/src/main/java/com/brainpix/api/code/error/AuthorityErrorCode.java +++ b/src/main/java/com/brainpix/api/code/error/AuthorityErrorCode.java @@ -10,6 +10,11 @@ public enum AuthorityErrorCode implements ErrorCode { PASSWORD_NOT_MATCH(HttpStatus.BAD_REQUEST, "AUTHORITY400", "비밀번호가 일치하지 않습니다."), AUTHORITY_ERROR_CODE(HttpStatus.FORBIDDEN, "AUTHORITY403", "권한이 없습니다."), + EMAIL_AUTH_CODE_NOT_FOUND(HttpStatus.BAD_REQUEST, "AUTHORITY400", "인증 코드를 먼저 요청해주세요."), + EMAIL_AUTH_CODE_NOT_MATCH(HttpStatus.BAD_REQUEST, "AUTHORITY400", "인증 코드가 일치하지 않습니다."), + EMAIL_AUTH_CODE_EXPIRED(HttpStatus.BAD_REQUEST, "AUTHORITY400", "인증 코드가 만료되었습니다."), + EMAIL_NOT_MATCHED(HttpStatus.BAD_REQUEST, "AUTHORITY400", "인증받은 이메일이 아닙니다"), + EMAIL_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "AUTHORITY400", "다른 이메일을 사용해주세요."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/brainpix/config/MailConfig.java b/src/main/java/com/brainpix/config/MailConfig.java index 9f97dcd4..bb789aad 100644 --- a/src/main/java/com/brainpix/config/MailConfig.java +++ b/src/main/java/com/brainpix/config/MailConfig.java @@ -15,9 +15,10 @@ public class MailConfig { private final String USERNAME; private final String PASSWORD; - public MailConfig(@Value("${spring.mail.password}") String HOST, + public MailConfig( + @Value("${spring.mail.host}") String HOST, @Value("${spring.mail.username}") String USERNAME, - @Value("${spring.mail.host}") String PASSWORD) { + @Value("${spring.mail.password}") String PASSWORD) { this.HOST = HOST; this.USERNAME = USERNAME; this.PASSWORD = PASSWORD; diff --git a/src/main/java/com/brainpix/security/controller/EmailController.java b/src/main/java/com/brainpix/security/controller/EmailController.java new file mode 100644 index 00000000..47287401 --- /dev/null +++ b/src/main/java/com/brainpix/security/controller/EmailController.java @@ -0,0 +1,39 @@ +package com.brainpix.security.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.brainpix.api.ApiResponse; +import com.brainpix.security.dto.EmailAuthCode; +import com.brainpix.security.dto.request.SendEmailNumberRequest; +import com.brainpix.security.service.EmailAuthService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "회원가입시 사용되는 이메일 인증 API", description = "회원가입시 사용되는 이메일 인증 API입니다.") +@RestController +@RequestMapping("/users/login/email") +@RequiredArgsConstructor +public class EmailController { + private final EmailAuthService emailAuthService; + + @Operation(summary = "입력 이메일로 인증 번호 전송", description = "입력한 이메일로 인증번호를 전송합니다.") + @PostMapping + public ResponseEntity> postEmail(@RequestBody SendEmailNumberRequest sendEmailNumberRequest) { + emailAuthService.sendEmailAuthCode(sendEmailNumberRequest); + return ResponseEntity.ok(ApiResponse.successWithNoData()); + } + + @Operation(summary = "인증번호 확인", description = "입력한 인증번호가 맞는지 확인합니다.") + @PostMapping("/auth") + public ResponseEntity> checkAuthCode( + @RequestBody EmailAuthCode.Request request) { + EmailAuthCode.Response response = emailAuthService.checkEmailAuthCode(request); + return ResponseEntity.ok(ApiResponse.success(response)); + } +} diff --git a/src/main/java/com/brainpix/security/controller/SignInController.java b/src/main/java/com/brainpix/security/controller/SignInController.java index 8d700ff7..6d7c8719 100644 --- a/src/main/java/com/brainpix/security/controller/SignInController.java +++ b/src/main/java/com/brainpix/security/controller/SignInController.java @@ -31,7 +31,7 @@ public class SignInController { public ResponseEntity> singIn(@RequestBody SignInRequest signInRequest) { Authentication authentication = authenticationManager.authenticate( SignInConverter.toAuthenticationToken(signInRequest)); - String jwt = tokenManager.writeToken(authentication); + String jwt = tokenManager.writeAuthenticationToken(authentication); return ResponseEntity.ok(ApiResponse.success(new SignInResponse("Bearer " + jwt))); } } diff --git a/src/main/java/com/brainpix/security/converter/EmailAuthCodeConverter.java b/src/main/java/com/brainpix/security/converter/EmailAuthCodeConverter.java new file mode 100644 index 00000000..8428fdf7 --- /dev/null +++ b/src/main/java/com/brainpix/security/converter/EmailAuthCodeConverter.java @@ -0,0 +1,11 @@ +package com.brainpix.security.converter; + +import com.brainpix.security.dto.EmailAuthCode; + +public class EmailAuthCodeConverter { + public static EmailAuthCode.Response toResponse(String token) { + return EmailAuthCode.Response.builder() + .token(token) + .build(); + } +} diff --git a/src/main/java/com/brainpix/security/dto/EmailAuthCode.java b/src/main/java/com/brainpix/security/dto/EmailAuthCode.java new file mode 100644 index 00000000..e46c9e3e --- /dev/null +++ b/src/main/java/com/brainpix/security/dto/EmailAuthCode.java @@ -0,0 +1,22 @@ +package com.brainpix.security.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class EmailAuthCode { + + @Getter + @NoArgsConstructor + public static class Request { + private String email; + private String authCode; + } + + @Getter + @Builder + public static class Response { + private String token; + } + +} diff --git a/src/main/java/com/brainpix/security/dto/request/SendEmailNumberRequest.java b/src/main/java/com/brainpix/security/dto/request/SendEmailNumberRequest.java new file mode 100644 index 00000000..fe750d8c --- /dev/null +++ b/src/main/java/com/brainpix/security/dto/request/SendEmailNumberRequest.java @@ -0,0 +1,10 @@ +package com.brainpix.security.dto.request; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SendEmailNumberRequest { + private String email; +} diff --git a/src/main/java/com/brainpix/security/dto/request/SignUpRequest.java b/src/main/java/com/brainpix/security/dto/request/SignUpRequest.java index a7283c81..f515334f 100644 --- a/src/main/java/com/brainpix/security/dto/request/SignUpRequest.java +++ b/src/main/java/com/brainpix/security/dto/request/SignUpRequest.java @@ -19,6 +19,7 @@ public abstract static class CommonSignUpRequest { protected String name; protected LocalDate birthday; protected String email; + protected String emailToken; public abstract User toEntity(String encodedPassword); diff --git a/src/main/java/com/brainpix/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/brainpix/security/filter/JwtAuthenticationFilter.java index 99c890bc..2c8beff7 100644 --- a/src/main/java/com/brainpix/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/brainpix/security/filter/JwtAuthenticationFilter.java @@ -38,7 +38,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } else { jwt = parseJwt(jwt); try { - BrainpixAuthenticationToken authenticationToken = tokenManager.readToken(jwt); + BrainpixAuthenticationToken authenticationToken = tokenManager.readAuthenticationToken(jwt); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } diff --git a/src/main/java/com/brainpix/security/service/CompanySignUpService.java b/src/main/java/com/brainpix/security/service/CompanySignUpService.java index 3b7dd6bc..7a3939ec 100644 --- a/src/main/java/com/brainpix/security/service/CompanySignUpService.java +++ b/src/main/java/com/brainpix/security/service/CompanySignUpService.java @@ -14,9 +14,8 @@ public class CompanySignUpService extends SignUpService { private final ProfileRepository profileRepository; public CompanySignUpService(UserRepository userRepository, - PasswordEncoder passwordEncoder, - ProfileRepository profileRepository) { - super(userRepository, passwordEncoder); + PasswordEncoder passwordEncoder, ProfileRepository profileRepository, EmailAuthService emailAuthService) { + super(userRepository, passwordEncoder, emailAuthService); this.profileRepository = profileRepository; } diff --git a/src/main/java/com/brainpix/security/service/EmailAuthService.java b/src/main/java/com/brainpix/security/service/EmailAuthService.java new file mode 100644 index 00000000..ba3dc517 --- /dev/null +++ b/src/main/java/com/brainpix/security/service/EmailAuthService.java @@ -0,0 +1,81 @@ +package com.brainpix.security.service; + +import java.security.SecureRandom; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.brainpix.api.code.error.AuthorityErrorCode; +import com.brainpix.api.exception.BrainPixException; +import com.brainpix.kafka.service.MailEventService; +import com.brainpix.mail.dto.SendMailDto; +import com.brainpix.security.converter.EmailAuthCodeConverter; +import com.brainpix.security.dto.EmailAuthCode; +import com.brainpix.security.dto.request.SendEmailNumberRequest; +import com.brainpix.security.tokenManger.TokenManager; +import com.brainpix.user.entity.EmailAuth; +import com.brainpix.user.repository.EmailAuthRepository; +import com.brainpix.user.repository.UserRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class EmailAuthService { + + private final EmailAuthRepository emailAuthRepository; + private final MailEventService mailEventService; + private final TokenManager tokenManager; + private final UserRepository userRepository; + + @Transactional + public void sendEmailAuthCode(SendEmailNumberRequest sendEmailNumberRequest) { + SendMailDto sendMailDto = SendMailDto.builder() + .receiverEmail(sendEmailNumberRequest.getEmail()) + .title("BrainPix 회원가입 인증 코드") + .signupCode(String.valueOf(generateRandomNumber())) + .build(); + EmailAuth emailAuth = EmailAuth.builder() + .email(sendEmailNumberRequest.getEmail()) + .authCode(sendMailDto.getSignupCode()) + .build(); + userRepository.findByEmail(sendEmailNumberRequest.getEmail()) + .ifPresent(user -> { + throw new BrainPixException(AuthorityErrorCode.EMAIL_ALREADY_EXIST); + }); + emailAuthRepository.save(emailAuth); + mailEventService.sendMailEvent(sendMailDto); + } + + /* + * 이메일 인증 코드를 확인하고 토큰을 발급합니다. + */ + @Transactional + public EmailAuthCode.Response checkEmailAuthCode(EmailAuthCode.Request emailAuthCode) { + EmailAuth emailAuth = emailAuthRepository.findFirstByEmailOrderByCreatedAtDesc(emailAuthCode.getEmail()) + .orElseThrow(() -> new BrainPixException(AuthorityErrorCode.EMAIL_AUTH_CODE_NOT_FOUND)); + + emailAuth.checkAuthTime(); + if (emailAuth.getAuthCode().equals(emailAuthCode.getAuthCode())) { + emailAuthRepository.delete(emailAuth); + String token = tokenManager.writeEmailAuthCodeToken(emailAuthCode.getEmail(), emailAuthCode.getAuthCode()); + return EmailAuthCodeConverter.toResponse(token); + } else { + throw new BrainPixException(AuthorityErrorCode.EMAIL_AUTH_CODE_NOT_MATCH); + } + } + + /* + * 이메일과 토큰을 확인합니다. + */ + public void checkEmailToken(String email, String jwt) { + if (!email.equals(tokenManager.readEmail(jwt))) { + throw new BrainPixException(AuthorityErrorCode.EMAIL_NOT_MATCHED); + } + } + + private int generateRandomNumber() { + SecureRandom random = new SecureRandom(); + return 100000 + random.nextInt(900000); + } +} diff --git a/src/main/java/com/brainpix/security/service/IndividualSignUpService.java b/src/main/java/com/brainpix/security/service/IndividualSignUpService.java index ba598768..2fc62c36 100644 --- a/src/main/java/com/brainpix/security/service/IndividualSignUpService.java +++ b/src/main/java/com/brainpix/security/service/IndividualSignUpService.java @@ -14,9 +14,8 @@ public class IndividualSignUpService extends SignUpService { public final ProfileRepository profileRepository; public IndividualSignUpService(UserRepository userRepository, - PasswordEncoder passwordEncoder, - ProfileRepository profileRepository) { - super(userRepository, passwordEncoder); + PasswordEncoder passwordEncoder, ProfileRepository profileRepository, EmailAuthService emailAuthService) { + super(userRepository, passwordEncoder, emailAuthService); this.profileRepository = profileRepository; } diff --git a/src/main/java/com/brainpix/security/service/SignUpService.java b/src/main/java/com/brainpix/security/service/SignUpService.java index f9c86822..c4639887 100644 --- a/src/main/java/com/brainpix/security/service/SignUpService.java +++ b/src/main/java/com/brainpix/security/service/SignUpService.java @@ -17,10 +17,12 @@ public abstract class SignUpService { private final UserRepository userRepository; public final PasswordEncoder passwordEncoder; + private final EmailAuthService emailAuthService; public void signUpUser(SignUpRequest.CommonSignUpRequest commonSignUpRequest) { checkDuplicated(commonSignUpRequest.getId()); checkDuplicatedNickName(commonSignUpRequest.myNickname()); + emailAuthService.checkEmailToken(commonSignUpRequest.getEmail(), commonSignUpRequest.getEmailToken()); User user = commonSignUpRequest.toEntity(passwordEncoder.encode(commonSignUpRequest.getPassword())); userRepository.save(user); @@ -29,7 +31,7 @@ public void signUpUser(SignUpRequest.CommonSignUpRequest commonSignUpRequest) { public void checkDuplicated(String identifier) { if (userRepository.findByIdentifier(identifier).isPresent()) { - throw new BrainPixException(SecurityErrorCode.NICKNAME_DUPLICATED); + throw new BrainPixException(SecurityErrorCode.IDENTIFIER_DUPLICATED); } } diff --git a/src/main/java/com/brainpix/security/tokenManger/JwtTokenManager.java b/src/main/java/com/brainpix/security/tokenManger/JwtTokenManager.java index 5c3f6557..fd6eade9 100644 --- a/src/main/java/com/brainpix/security/tokenManger/JwtTokenManager.java +++ b/src/main/java/com/brainpix/security/tokenManger/JwtTokenManager.java @@ -38,7 +38,7 @@ public JwtTokenManager(@Value("${jwt.secret}") String secretKey) { } @Override - public BrainpixAuthenticationToken readToken(String token) throws + public BrainpixAuthenticationToken readAuthenticationToken(String token) throws SignatureException, UnsupportedJwtException, MalformedJwtException, ExpiredJwtException { JwtParser jwtParser = Jwts.parserBuilder() @@ -71,7 +71,7 @@ public BrainpixAuthenticationToken readToken(String token) throws } @Override - public String writeToken(Authentication authentication) { + public String writeAuthenticationToken(Authentication authentication) { return Jwts.builder() .setHeader(Map.of( "provider", "brainpix", @@ -87,4 +87,33 @@ public String writeToken(Authentication authentication) { .signWith(secretKey) .compact(); } + + @Override + public String writeEmailAuthCodeToken(String email, String authCode) { + return Jwts.builder() + .setHeader(Map.of( + "provider", "brainpix", + "type", "emailAuthCode" + )) + .setClaims(Map.of( + "email", email, + "authCode", authCode + )) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) + .signWith(secretKey) + .compact(); + } + + @Override + public String readEmail(String token) { + + JwtParser jwtParser = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build(); + Claims claims = jwtParser.parseClaimsJws(token) + .getBody(); + + return claims.get("email", String.class); + } } diff --git a/src/main/java/com/brainpix/security/tokenManger/TokenManager.java b/src/main/java/com/brainpix/security/tokenManger/TokenManager.java index 75f73d65..d3199b89 100644 --- a/src/main/java/com/brainpix/security/tokenManger/TokenManager.java +++ b/src/main/java/com/brainpix/security/tokenManger/TokenManager.java @@ -5,7 +5,11 @@ import com.brainpix.security.authenticationToken.BrainpixAuthenticationToken; public interface TokenManager { - BrainpixAuthenticationToken readToken(String token); + BrainpixAuthenticationToken readAuthenticationToken(String token); - String writeToken(Authentication authentication); + String writeAuthenticationToken(Authentication authentication); + + String writeEmailAuthCodeToken(String email, String authCode); + + String readEmail(String token); } diff --git a/src/main/java/com/brainpix/user/entity/EmailAuth.java b/src/main/java/com/brainpix/user/entity/EmailAuth.java new file mode 100644 index 00000000..827297e3 --- /dev/null +++ b/src/main/java/com/brainpix/user/entity/EmailAuth.java @@ -0,0 +1,43 @@ +package com.brainpix.user.entity; + +import java.time.LocalDateTime; + +import com.brainpix.api.code.error.AuthorityErrorCode; +import com.brainpix.api.exception.BrainPixException; +import com.brainpix.jpa.BaseTimeEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor +public class EmailAuth extends BaseTimeEntity { + + private final static int authExpireMinute = 10; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String email; + + private String authCode; + + @Builder + public EmailAuth(String email, String authCode) { + this.email = email; + this.authCode = authCode; + } + + public void checkAuthTime() { + if (this.getCreatedAt().plusMinutes(authExpireMinute).isBefore(LocalDateTime.now())) { + throw new BrainPixException(AuthorityErrorCode.EMAIL_AUTH_CODE_EXPIRED); + } + } +} diff --git a/src/main/java/com/brainpix/user/repository/EmailAuthRepository.java b/src/main/java/com/brainpix/user/repository/EmailAuthRepository.java new file mode 100644 index 00000000..dc3b381e --- /dev/null +++ b/src/main/java/com/brainpix/user/repository/EmailAuthRepository.java @@ -0,0 +1,13 @@ +package com.brainpix.user.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.brainpix.user.entity.EmailAuth; + +@Repository +public interface EmailAuthRepository extends JpaRepository { + Optional findFirstByEmailOrderByCreatedAtDesc(String email); +} diff --git a/src/main/java/com/brainpix/user/repository/UserRepository.java b/src/main/java/com/brainpix/user/repository/UserRepository.java index fa43d336..56bb5ffc 100644 --- a/src/main/java/com/brainpix/user/repository/UserRepository.java +++ b/src/main/java/com/brainpix/user/repository/UserRepository.java @@ -18,4 +18,6 @@ public interface UserRepository extends JpaRepository { Optional findByNickName(String nickName); boolean existsByIdentifier(String identifier); + + Optional findByEmail(String email); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 205b011e..2679512d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -20,9 +20,9 @@ spring: mongodb: uri: ENC(OyBqzmMmtLqk4kAhF/+3rGjN/7ZzE3gIHHBE5T7sfRK2fgj3L+xq26lIUmkJG4Aqa0gQ5i83l43yGpxOIuT56aQ098QukuiFAPYNVpSerxY=) mail: - host: smtp.gmail.com - username: example12345@gmail.com - password: example12345 + host: ENC(5WpWHIIQtBVAYDvxlhS3jjaEZXLMSQjIqO5kXPrLze/PM29RbHV3U8Z6ihG501qp) + username: ENC(prCp5Ol/nxisuAB+sy7D1brjRQDnk1sArHFMZ1J310Q7O/DOed09i9OXAiZBSrh2Lae9pI7soUI6dxdpgcx3KA==) + password: ENC(15oeubKhgXAzVQFYZJlBH+zhMn9nO9baDacvG0xfZVKCALibswPooowYZbQHja6X/kgi0gSJ5/kVx6yuQhgz+Q==) jasypt: encryptor: password: jasyptKey