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
@@ -0,0 +1,11 @@
package com.example.LifeMaster_BE.UserManager.Email.Password.Dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class VerifyCodeRequest {
private String email;
private String code;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.PasswordResetDto;
import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.PasswordResponseDto;
import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.TokenVerifyResponse;
import com.example.LifeMaster_BE.UserManager.Email.Password.Dto.VerifyCodeRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -16,24 +17,24 @@ public class PasswordController {
private final PasswordService passwordService;

@PostMapping("/confirm-email")
public ResponseEntity<PasswordResponseDto> sendEmail(@RequestBody ConfirmEmailRequest request){
public ResponseEntity<PasswordResponseDto> sendEmail(@RequestBody ConfirmEmailRequest request) {
passwordService.sendPasswordResetEmail(request.getEmail());
return ResponseEntity.ok(new PasswordResponseDto(true, "Password reset link has been sent to your email."));
return ResponseEntity.ok(new PasswordResponseDto(true, "인증 코드가 이메일로 발송되었습니다."));
}

@GetMapping("/verify")
public ResponseEntity<TokenVerifyResponse> verifyToken(@RequestParam String token) {
passwordService.verifyToken(token);
return ResponseEntity.ok(new TokenVerifyResponse(true, "Token verified.", token));
@PostMapping("/verify-code")
public ResponseEntity<TokenVerifyResponse> verifyCode(@RequestBody VerifyCodeRequest request) {
String token = passwordService.verifyCode(request.getEmail(), request.getCode());
return ResponseEntity.ok(new TokenVerifyResponse(true, "인증이 완료되었습니다.", token));
}

@PostMapping
public ResponseEntity<PasswordResponseDto> resetPassword(@RequestBody PasswordResetDto passwordResetDto){
public ResponseEntity<PasswordResponseDto> resetPassword(@RequestBody PasswordResetDto passwordResetDto) {
String token = passwordResetDto.getToken();
String newPassword = passwordResetDto.getNewPassword();
String checkPassword = passwordResetDto.getCheckPassword();

passwordService.resetPassword(token, newPassword, checkPassword);
return ResponseEntity.ok(new PasswordResponseDto(true, "password reset successful."));
return ResponseEntity.ok(new PasswordResponseDto(true, "비밀번호가 성공적으로 재설정되었습니다."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,33 +23,32 @@ public void sendPasswordResetEmail(String email) {
MemberEntity memberEntity = memberRepository.findByEmail(email)
.orElseThrow(() -> new EntityNotFoundException("존재하지 않는 이메일 정보입니다."));

String token = tokenService.createToken(memberEntity.getId());
String resetLink = "https://api.lifemaster.harvester.kr/auth/password/reset/verify?token=" + token;
// String resetLink = "http://localhost:8080/auth/password/reset/verify?token=" + token;
// HTML 형식으로 작성
String code = tokenService.createVerificationCode(email, memberEntity.getId());

String htmlContent = String.format(
"<html>" +
"<body>" +
"<h2>비밀번호 재설정</h2>" +
"<p>아래 링크를 클릭하여 비밀번호를 재설정하세요:</p>" +
"<a href='%s' style='display:inline-block; padding:10px 20px; background-color:#4CAF50; color:white; text-decoration:none; border-radius:5px;'>비밀번호 재설정</a>" +
"<p>링크가 작동하지 않으면 다음 URL을 복사하여 브라우저에 붙여넣으세요:</p>" +
"<p>%s</p>" +
"<p>아래 인증 코드를 입력하여 비밀번호를 재설정하세요:</p>" +
"<div style='background-color:#f4f4f4; padding:20px; text-align:center; margin:20px 0;'>" +
"<span style='font-size:32px; font-weight:bold; letter-spacing:8px; color:#333;'>%s</span>" +
"</div>" +
"<p>이 코드는 10분 동안 유효합니다.</p>" +
"<p>본인이 요청하지 않았다면 이 이메일을 무시해주세요.</p>" +
"</body>" +
"</html>",
resetLink, resetLink
code
);

emailService.sendEmail(email, "비밀번호 재설정", htmlContent);
emailService.sendEmail(email, "비밀번호 재설정 인증 코드", htmlContent);
}

public void verifyToken(String token){
tokenService.validateToken(token);
public String verifyCode(String email, String code) {
return tokenService.verifyCodeAndCreateToken(email, code);
}

public void resetPassword(String token, String newPassword, String checkPassword){

if(!confirmPassword(newPassword, checkPassword)){
public void resetPassword(String token, String newPassword, String checkPassword) {
if (!confirmPassword(newPassword, checkPassword)) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다");
}

Expand All @@ -64,8 +63,7 @@ public void resetPassword(String token, String newPassword, String checkPassword
memberRepository.save(memberEntity);
}

private boolean confirmPassword(String password, String confirmPassword){
private boolean confirmPassword(String password, String confirmPassword) {
return password.equals(confirmPassword);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,102 @@
import org.springframework.stereotype.Service;

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

@Service
@RequiredArgsConstructor
public class TokenService {

private final StringRedisTemplate redisTemplate;
private static final Duration CODE_EXPIRATION = Duration.ofMinutes(10);
private static final Duration TOKEN_EXPIRATION = Duration.ofMinutes(10);
private static final int MAX_ATTEMPTS = 5;
private static final String CODE_KEY_PREFIX = "password_reset:code:";
private static final String ATTEMPTS_KEY_PREFIX = "password_reset:attempts:";
private static final String USERID_KEY_PREFIX = "password_reset:userid:";

public String createToken(Long userId){
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(token, String.valueOf(userId), TOKEN_EXPIRATION);
return token;
// 6자리 인증 코드 생성 및 저장
public String createVerificationCode(String email, Long userId) {
String code = generateSixDigitCode();
String codeKey = CODE_KEY_PREFIX + email;
String attemptsKey = ATTEMPTS_KEY_PREFIX + email;
String userIdKey = USERID_KEY_PREFIX + email;

redisTemplate.opsForValue().set(codeKey, code, CODE_EXPIRATION);
redisTemplate.opsForValue().set(attemptsKey, "0", CODE_EXPIRATION);
redisTemplate.opsForValue().set(userIdKey, String.valueOf(userId), CODE_EXPIRATION);

return code;
}

// 6자리 숫자 코드 생성
private String generateSixDigitCode() {
Random random = new Random();
int code = 100000 + random.nextInt(900000);
return String.valueOf(code);
}

public void validateToken(String token){
Boolean hasKey = redisTemplate.hasKey(token);
if (hasKey == null || !hasKey){
throw new IllegalArgumentException("Invalid token");
// 인증 코드 검증 및 토큰 발급
public String verifyCodeAndCreateToken(String email, String inputCode) {
String codeKey = CODE_KEY_PREFIX + email;
String attemptsKey = ATTEMPTS_KEY_PREFIX + email;
String userIdKey = USERID_KEY_PREFIX + email;

// 저장된 코드 조회
String storedCode = redisTemplate.opsForValue().get(codeKey);
if (storedCode == null) {
throw new IllegalArgumentException("인증 코드가 만료되었거나 존재하지 않습니다.");
}

// 시도 횟수 확인
String attemptsStr = redisTemplate.opsForValue().get(attemptsKey);
int attempts = attemptsStr != null ? Integer.parseInt(attemptsStr) : 0;

if (attempts >= MAX_ATTEMPTS) {
deleteVerificationData(email);
throw new IllegalArgumentException("인증 시도 횟수를 초과했습니다. 다시 인증 코드를 요청해주세요.");
}

// 코드 검증
if (!storedCode.equals(inputCode)) {
redisTemplate.opsForValue().set(attemptsKey, String.valueOf(attempts + 1), CODE_EXPIRATION);
int remainingAttempts = MAX_ATTEMPTS - attempts - 1;
throw new IllegalArgumentException("인증 코드가 일치하지 않습니다. 남은 시도 횟수: " + remainingAttempts);
}

// 검증 성공 - userId 조회 후 토큰 발급
String userIdStr = redisTemplate.opsForValue().get(userIdKey);
if (userIdStr == null) {
throw new IllegalArgumentException("사용자 정보를 찾을 수 없습니다.");
}

// 인증 데이터 삭제
deleteVerificationData(email);

// 토큰 발급
return createToken(Long.valueOf(userIdStr));
}

// 인증 데이터 삭제
private void deleteVerificationData(String email) {
redisTemplate.delete(CODE_KEY_PREFIX + email);
redisTemplate.delete(ATTEMPTS_KEY_PREFIX + email);
redisTemplate.delete(USERID_KEY_PREFIX + email);
}

// 토큰 생성 (비밀번호 재설정용)
public String createToken(Long userId) {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(token, String.valueOf(userId), TOKEN_EXPIRATION);
return token;
}

public Long validateAndConsumeToken(String token){
// 토큰 검증 및 소비
public Long validateAndConsumeToken(String token) {
String userIdStr = redisTemplate.opsForValue().get(token);
if(userIdStr == null){
throw new IllegalArgumentException("Invalid token");
if (userIdStr == null) {
throw new IllegalArgumentException("유효하지 않거나 만료된 토큰입니다.");
}
redisTemplate.delete(token);
return Long.valueOf(userIdStr);
Expand Down