Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/brainpix/config/MailConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ApiResponse<Void>> postEmail(@RequestBody SendEmailNumberRequest sendEmailNumberRequest) {
emailAuthService.sendEmailAuthCode(sendEmailNumberRequest);
return ResponseEntity.ok(ApiResponse.successWithNoData());
}

@Operation(summary = "인증번호 확인", description = "입력한 인증번호가 맞는지 확인합니다.")
@PostMapping("/auth")
public ResponseEntity<ApiResponse<EmailAuthCode.Response>> checkAuthCode(
@RequestBody EmailAuthCode.Request request) {
EmailAuthCode.Response response = emailAuthService.checkEmailAuthCode(request);
return ResponseEntity.ok(ApiResponse.success(response));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class SignInController {
public ResponseEntity<ApiResponse<SignInResponse>> 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)));
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/brainpix/security/dto/EmailAuthCode.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.brainpix.security.dto.request;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class SendEmailNumberRequest {
private String email;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
81 changes: 81 additions & 0 deletions src/main/java/com/brainpix/security/service/EmailAuthService.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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",
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
43 changes: 43 additions & 0 deletions src/main/java/com/brainpix/user/entity/EmailAuth.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<EmailAuth, Long> {
Optional<EmailAuth> findFirstByEmailOrderByCreatedAtDesc(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByNickName(String nickName);

boolean existsByIdentifier(String identifier);

Optional<User> findByEmail(String email);
}
Loading