✅ 이전 포스팅
2024.12.02 - [Java/쇼핑몰] - 쇼핑몰 - 6 [Spring Security + JWT + OAuth2 + Redis]
이전 포스팅에서 OAuth2 로그인까지 구현하여 회원가입과 로그인 부분에 대해서 다뤄보았다.
이번에는 비밀번호를 까먹은 사용자를 위해 사용자의 이메일로 비밀번호 변경 URL을 전송하여
비밀번호를 변경할 수 있도록 해보자
[참고사항]
기존에는 import 패키지도 작성했지만 코드가 길어져 제외했습니다.
이메일 전송에 필요한 naver, gmail 등 메일 설정은 제외했습니다.
✅ 컨트롤러
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/members")
public class MemberController implements MemberControllerDocs {
private final MemberService memberService;
@GetMapping("/password-reset-email")
public ResponseEntity<Response<?>> sendResetPasswordEmail(Authentication authentication) {
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
PasswordRestResponseDTO result = memberService.sendResetEmail(userDetails);
return buildResponse(HttpStatus.OK, "이메일 전송 성공", result);
}
@PutMapping("/password")
public ResponseEntity<Response<?>> resetPassword(@RequestBody @Valid PasswordResetRequestDTO parameter) {
boolean result = memberService.resetPassword(parameter);
return buildResponse(HttpStatus.OK, "비밀번호 변경 성공", result);
}
}
컨트롤러에서는 이메일에 URL을 전송하는 부분과 실제 비밀번호 변경을 담당하는 것 두 부분으로 나누었다.
✅ 서비스
🟨 MemberService
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final RedisService redisService;
private final MailService mailService;
/**
* 비밀번호 재설정 이메일을 발송하는 메서드입니다.
* 주어진 사용자 세부 정보를 기반으로 사용자의 이메일을 확인한 후, 비밀번호 재설정 이메일을 생성하여 발송합니다.
* 생성된 고유 토큰과 사용자 이메일을 포함한 응답 DTO를 반환합니다.
*
* @param userDetails 사용자 정보를 담고 있는 {@link UserDetailsImpl} 객체.
* 이 객체는 사용자의 이메일(사용자 이름) 정보를 포함합니다.
* @return 비밀번호 재설정 이메일 발송에 대한 정보를 담은 {@link PasswordRestResponseDTO} 객체.
* 이 DTO는 사용자 이메일과 재설정에 사용되는 UUID 토큰을 포함합니다.
* @throws UsernameNotFoundException 사용자의 이메일이 존재하지 않을 경우 발생합니다.
*/
@Transactional
public PasswordRestResponseDTO sendResetEmail(UserDetailsImpl userDetails) {
if (memberRepository.existsByEmail(userDetails.getUsername())) {
String uuid = mailService.generateResetEmail(userDetails.getUsername());
return PasswordRestResponseDTO.builder()
.userEmail(userDetails.getUsername())
.token(uuid)
.build();
} else {
throw new UsernameNotFoundException("존재하지 않는 이메일입니다.");
}
}
/**
* 비밀번호 재설정을 수행하는 메서드입니다.
* 요청된 토큰의 유효성을 검증하고, 유효한 경우 해당 사용자의 비밀번호를 재설정합니다.
* 이후 사용된 토큰은 무효화됩니다.
*
* @param parameter 비밀번호 재설정을 위한 요청 정보가 담긴 {@link PasswordResetRequestDTO} 객체.
* 이 객체는 사용자의 이메일, 재설정할 비밀번호, 그리고 재설정 토큰을 포함합니다.
* @return 비밀번호 재설정 성공 여부. 유효한 토큰을 사용하여 비밀번호가 성공적으로 변경되면 true를 반환합니다.
* @throws BadCredentialsException 유효하지 않은 토큰이 제공될 경우 발생합니다.
*/
@Transactional
public boolean resetPassword(PasswordResetRequestDTO parameter) {
String token = parameter.getToken();
if (!redisService.isValidPwResetToken(token)) {
throw new BadCredentialsException("유효하지 않은 토큰입니다.");
}
// 비밀번호 재설정
Optional<Member> memberOptional = memberRepository.findByEmail(parameter.getUserEmail());
memberOptional.ifPresent(member -> {
member.setPassword(passwordEncoder.encode(parameter.getPassword()));
memberRepository.save(member);
});
// 토큰 무효화
redisService.invalidatePwResetToken(token);
return true;
}
}
MemberService에서는 메일을 전송한 뒤 전송 된 값들에 대한 DTO 값을 전달하는 메서드와
전달받은 데이터를 통해 비밀번호를 변경해 주는 메서드가 존재한다.
🟨 MailService
@Service
@RequiredArgsConstructor
public class MailService {
private final RedisService redisService;
private final JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String fromEmail;
@Value("${props.reset-password-url}")
private String resetPwUrl;
/**
* 비밀번호 재설정 이메일을 생성하여 발송하는 메서드입니다.
* 비밀번호 재설정에 필요한 토큰을 생성하고, 해당 토큰이 포함된 이메일을 지정된 사용자에게 보냅니다.
*
* @param userEmail 비밀번호 재설정 이메일을 받을 사용자의 이메일 주소
* @return 비밀번호 재설정에 사용되는 고유 토큰 (UUID)
*/
@Transactional
public String generateResetEmail(String userEmail) {
String uuid = redisService.generatePwResetToken(userEmail);
String title = "[쇼핑몰] 비밀번호 재설정 링크입니다.";
String content = "아래 링크에 접속하여 비밀번호를 재설정 해주세요.<br><br>"
+ "<a href=\"" + resetPwUrl + "/" + uuid + "\">"
+ resetPwUrl + "/" + uuid + "</a>"
+ "<br><br>해당 링크는 24시간 동안 유효하며, 1회 변경 가능합니다.<br>";
sendMail(userEmail, title, content);
return uuid;
}
/**
* 이메일을 발송하는 메서드입니다. 지정된 제목과 내용을 사용하여 사용자에게 이메일을 전송합니다.
*
* @param userEmail 수신자 이메일 주소
* @param title 이메일 제목
* @param content 이메일 본문 (HTML 형식 지원)
* @throws RuntimeException 이메일 발송 중 문제가 발생할 경우 발생합니다.
*/
public void sendMail(String userEmail, String title, String content) {
MimeMessage message = mailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(message, true, "utf-8");
helper.setFrom(new InternetAddress(fromEmail, "쇼핑몰"));
helper.setTo(userEmail);
helper.setSubject(title);
helper.setText(content, true);
mailSender.send(message);
} catch (MessagingException | UnsupportedEncodingException exception) {
throw new RuntimeException(exception);
}
}
}
mailService의 경우 사용자의 이메일에 지정해 둔 url을 전송하는 방식으로 구현하였다.
🟨 RedisService
@Service
@RequiredArgsConstructor
public class RedisService {
private static final String TOKEN_PREFIX = "password-reset-token: ";
private final RedisTemplate<String, String> redisTemplate;
/**
* 비밀번호 재설정 토큰을 생성하는 메서드입니다.
* UUID 기반으로 고유한 토큰을 생성하며, 생성된 토큰을 Redis에 저장하여 유효성을 유지합니다.
*
* @param userEmail 비밀번호 재설정을 요청한 사용자의 Email
* @return 생성된 비밀번호 재설정 토큰
*/
@Transactional
public String generatePwResetToken(String userEmail) {
String token = UUID.randomUUID().toString();
String key = TOKEN_PREFIX + token;
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(key, userEmail, Duration.ofHours(24));
return token;
}
/**
* 비밀번호 재설정 토큰이 유효한지 확인하는 메서드입니다.
* Redis에서 해당 토큰의 존재 여부를 통해 유효성을 검사합니다.
*
* @param token 확인할 비밀번호 재설정 토큰
* @return 토큰이 유효한 경우 true, 유효하지 않은 경우 false
*/
public boolean isValidPwResetToken(String token) {
String key = TOKEN_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 비밀번호 재설정 토큰을 무효화하는 메서드입니다.
* Redis에서 해당 토큰을 삭제하여 더 이상 사용할 수 없도록 만듭니다.
*
* @param token 무효화할 비밀번호 재설정 토큰
*/
@Transactional
public void invalidatePwResetToken(String token) {
String key = TOKEN_PREFIX + token;
redisTemplate.delete(key);
}
}
redisService의 경우 이전 포스팅에서 작성했던 것 같은데 생각이 안 나서 같이 넣었다.
위 서비스에서는 이메일에 같이 전송할 토큰 값을 생성하고 해당 토큰이 유효한지 확인하기 위한 로직을
넣어두었다.
✅ 테스트
위 이미지와 같이 이메일 요청 전송을 했을 때 사용자의 이메일로 url이 전송된다.
그럼 위와 같은 이메일이 전송되고, 해당 url을 클릭하면 프론트엔드의 비밀번호 변경 폼이 나타나도록 했다.
마지막으로 비밀번호 변경 컨트롤러에 사용자 이메일과, 변경할 비밀번호 그리고 토큰값을 넣어주면
지정한 비밀번호로 변경되는 것을 볼 수 있다.
✅ 마무리
처음에는 기존 비밀번호와 신규 비밀번호를 입력해서 바꾸는 방식이라던지 아니면 인증번호를 보내서 인증하는 방식이라던지 고민을 많이 했었는데 통상적으로 많이 보이는 사이트의 변경 방식이 위와 같아서
url로 비밀번호 변경 폼에 접근하도록 만들었다. 직접 구현해보니 어렵지는 않아서 생각보다 쉽게 구현했던 것 같다.
https://github.com/sideProject-org/shop-back
'Java > 쇼핑몰' 카테고리의 다른 글
쇼핑몰 - 6 [Spring Security + JWT + OAuth2 + Redis] (2) | 2024.12.02 |
---|---|
쇼핑몰 - 5 [spring security + JWT + Redis 로그인] (0) | 2024.11.19 |
쇼핑몰 - 4 [회원 가입 구현] (0) | 2024.11.12 |
쇼핑몰 - 3 [GitHub Actions를 통한 CI/CD] (3) | 2024.11.06 |
쇼핑몰 - 2 [Docker-compose 사용하기] (0) | 2024.11.03 |