feat: email 비밀번호 찾기 이메일 인증 추가

This commit is contained in:
kgc91747 2024-08-07 10:39:16 +09:00
parent 7201e0aa5c
commit 509a41fd0a
19 changed files with 277 additions and 40 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -37,7 +37,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.mindrot:jbcrypt:0.4' implementation 'org.mindrot:jbcrypt:0.4'
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

View File

@ -2,6 +2,7 @@ package com.edufocus.edufocus.global.config;
import com.edufocus.edufocus.global.properties.ImagePathProperties; import com.edufocus.edufocus.global.properties.ImagePathProperties;
import com.edufocus.edufocus.global.properties.MailProperties;
import com.edufocus.edufocus.global.properties.RabbitMQProperties; import com.edufocus.edufocus.global.properties.RabbitMQProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -9,7 +10,8 @@ import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@EnableConfigurationProperties({ @EnableConfigurationProperties({
RabbitMQProperties.class, RabbitMQProperties.class,
ImagePathProperties.class ImagePathProperties.class,
MailProperties.class
}) })
public class PropertiesConfig { public class PropertiesConfig {
} }

View File

@ -0,0 +1,22 @@
package com.edufocus.edufocus.global.config;
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);
}
}

View File

@ -0,0 +1,15 @@
package com.edufocus.edufocus.global.properties;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "spring.mail")
public class MailProperties {
private final String host;
private final Integer port;
private final String name;
private final String password;
}

View File

@ -0,0 +1,39 @@
package com.edufocus.edufocus.mail.controller;
import com.edufocus.edufocus.mail.service.MailService;
import com.edufocus.edufocus.user.model.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/mail")
@RequiredArgsConstructor
public class MailController {
private final MailService mailService;
private final UserService userService;
@PostMapping("/sendCode")
public ResponseEntity<?> sendMail(@RequestParam String email) {
if (!userService.isEmailExist(email)) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
mailService.sendMail(email);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/verify")
public ResponseEntity<?> verifyCode(@RequestParam String code, @RequestParam String email) {
if (!mailService.verifyCode(code, email)) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<>(HttpStatus.OK);
}
}

View File

@ -0,0 +1,13 @@
package com.edufocus.edufocus.mail.service;
import org.springframework.stereotype.Service;
@Service
public interface MailService {
void sendMail(String to);
String createRandomCode();
boolean verifyCode(String code, String email);
}

View File

@ -0,0 +1,69 @@
package com.edufocus.edufocus.mail.service;
import com.edufocus.edufocus.redis.util.RedisUtil;
import com.edufocus.edufocus.user.model.entity.vo.User;
import com.edufocus.edufocus.user.model.repository.UserRepository;
import com.edufocus.edufocus.user.model.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import java.util.NoSuchElementException;
import java.util.Random;
@Service
@Slf4j
@ToString
@RequiredArgsConstructor
public class MailServiceImpl implements MailService {
private final JavaMailSender mailSender;
private final UserRepository userRepository;
private final UserService userService;
private final RedisUtil redisUtil;
@Override
public void sendMail(String email) {
String code = createRandomCode();
redisUtil.setDataExpire(code, email, 60 * 5L);
SimpleMailMessage mail = createEmail(email, "[EDUFOCUS] 비밀번호 찾기 안내", code);
mailSender.send(mail);
}
@Override
public boolean verifyCode(String code, String email) {
String registedEmail = redisUtil.getData(code);
return registedEmail != null && registedEmail.equals(email);
}
private SimpleMailMessage createEmail(String to, String title, String code) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(title);
message.setText("인증번호 6자리입니다 : " + code);
return message;
}
@Override
public String createRandomCode() {
StringBuilder sb = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 3; i++) {
sb.append((char) (random.nextInt(26) + 65));
}
for (int i = 0; i < 3; i++) {
sb.append(random.nextInt(10));
}
return sb.toString();
}
}

View File

@ -0,0 +1,36 @@
package com.edufocus.edufocus.redis.util;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final StringRedisTemplate stringRedisTemplate;
public String getData(String key) {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
return valueOperations.get(key);
}
public void setData(String key, String value) {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set(key, value);
}
public void setDataExpire(String key, String value, long duration) {
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, value, expireDuration);
}
public void deleteData(String key) {
stringRedisTemplate.delete(key);
}
}

View File

@ -47,6 +47,6 @@ public class WebConfiguration implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor) registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 모든 경로에 대해 인터셉터 적용 .addPathPatterns("/**") // 모든 경로에 대해 인터셉터 적용
.excludePathPatterns("/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/swagger-ui/**", "/auth/**", "/board/**", "/user/**", "/lecture/**", "/qna/**", "/quiz/**", "/video/**", "/registration/**", "/report/**"); // 인증 없이 접근 가능한 경로 설정 .excludePathPatterns("/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/swagger-ui/**", "/auth/**", "/board/**", "/user/**", "/lecture/**", "/qna/**", "/quiz/**", "/video/**", "/registration/**", "/report/**", "/mail/**"); // 인증 없이 접근 가능한 경로 설정
} }
} }

View File

@ -66,6 +66,14 @@ public class UserController {
} }
} }
// 비밀번호 찾기를 통한 변경
@PutMapping("/updateforgottenpassword")
public ResponseEntity<String> updatePassword(@RequestParam long userId,
@RequestParam String newPassword) {
userService.changeForgottenPassword(userId, newPassword);
return new ResponseEntity<>(HttpStatus.OK);
}
@Operation(summary = "로그인", description = "아이디와 비밀번호를 이용하여 로그인 처리.") @Operation(summary = "로그인", description = "아이디와 비밀번호를 이용하여 로그인 처리.")
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<Map<String, Object>> login( public ResponseEntity<Map<String, Object>> login(

View File

@ -31,4 +31,6 @@ public interface UserRepository extends JpaRepository<User,Long> {
Optional<User> findByUserId(String userId); Optional<User> findByUserId(String userId);
Optional<User> findByEmail(String email);
} }

View File

@ -25,4 +25,8 @@ public interface UserService {
void changePassword(PasswordDto passwordDto, Long id); void changePassword(PasswordDto passwordDto, Long id);
boolean isUserIdExist(String userId); boolean isUserIdExist(String userId);
boolean isEmailExist(String email);
void changeForgottenPassword(Long id, String newPassword);
} }

View File

@ -3,11 +3,11 @@ package com.edufocus.edufocus.user.model.service;
import com.edufocus.edufocus.user.model.entity.dto.InfoDto; import com.edufocus.edufocus.user.model.entity.dto.InfoDto;
import com.edufocus.edufocus.user.model.entity.dto.PasswordDto; import com.edufocus.edufocus.user.model.entity.dto.PasswordDto;
import com.edufocus.edufocus.user.util.PasswordUtils;
import com.edufocus.edufocus.user.model.entity.dto.RequestJoinDto; import com.edufocus.edufocus.user.model.entity.dto.RequestJoinDto;
import com.edufocus.edufocus.user.model.entity.vo.User; import com.edufocus.edufocus.user.model.entity.vo.User;
import com.edufocus.edufocus.user.model.exception.UserException; import com.edufocus.edufocus.user.model.exception.UserException;
import com.edufocus.edufocus.user.model.repository.UserRepository; import com.edufocus.edufocus.user.model.repository.UserRepository;
import com.edufocus.edufocus.user.util.PasswordUtils;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -23,8 +23,7 @@ public class UserServiceImpl implements UserService {
private final UserRepository userRepository; private final UserRepository userRepository;
public void join(RequestJoinDto requestJoinDto) public void join(RequestJoinDto requestJoinDto) {
{
User user = User.builder() User user = User.builder()
.userId(requestJoinDto.getUserId()) .userId(requestJoinDto.getUserId())
.email(requestJoinDto.getEmail()) .email(requestJoinDto.getEmail())
@ -63,9 +62,6 @@ public class UserServiceImpl implements UserService {
} }
@Override @Override
public String getUserName(Long id) { public String getUserName(Long id) {
return userRepository.findById(id).get().getName(); return userRepository.findById(id).get().getName();
@ -125,6 +121,7 @@ public class UserServiceImpl implements UserService {
} }
return str; return str;
} }
@Override @Override
public void saveRefreshToken(Long id, String refreshToken) { public void saveRefreshToken(Long id, String refreshToken) {
userRepository.saveRefreshToken(id, refreshToken); userRepository.saveRefreshToken(id, refreshToken);
@ -140,4 +137,25 @@ public class UserServiceImpl implements UserService {
userRepository.deleteRefreshToken(id); userRepository.deleteRefreshToken(id);
} }
@Override
public boolean isEmailExist(String email) {
Optional<User> user = userRepository.findByEmail(email);
return user.isPresent();
}
@Override
public void changeForgottenPassword(Long id, String newPassword) {
User user = userRepository.findById(id).orElse(null);
if (user == null) {
throw new UserException("User not found");
}
// Hash the new password before saving
user.setPassword(PasswordUtils.hashPassword(newPassword));
userRepository.save(user);
}
} }

View File

@ -30,3 +30,12 @@ spring.rabbitmq.port=${RABBITMQ_PORT}
spring.rabbitmq.username=${RABBITMQ_USERNAME} spring.rabbitmq.username=${RABBITMQ_USERNAME}
spring.rabbitmq.password=${RABBITMQ_PASSWORD} spring.rabbitmq.password=${RABBITMQ_PASSWORD}
image.path=${IMAGE_PATH} image.path=${IMAGE_PATH}
spring.mail.host=smtp.gmail.com
spring.mail.port=${MAIL_PORT}
spring.mail.username=${MAIL_NAME}
spring.mail.password=${MAIL_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.starttls.enable=true
spring.data.redis.host=${REDIS_HOST}
spring.data.redis.port=${REDIS_PORT}