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

@ -32,9 +32,9 @@ public class UserController {
private final JWTUtil jwtUtil; private final JWTUtil jwtUtil;
@PostMapping("/join") @PostMapping("/join")
public ResponseEntity<String> join(@RequestBody RequestJoinDto requestJoinDto){ public ResponseEntity<String> join(@RequestBody RequestJoinDto requestJoinDto) {
if(userService.isUserIdExist(requestJoinDto.getUserId())) if (userService.isUserIdExist(requestJoinDto.getUserId()))
return new ResponseEntity<>("아이디가 중복 됐습니다.", HttpStatus.CONFLICT); return new ResponseEntity<>("아이디가 중복 됐습니다.", HttpStatus.CONFLICT);
userService.join(requestJoinDto); userService.join(requestJoinDto);
@ -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(
@ -83,8 +91,8 @@ public class UserController {
userService.saveRefreshToken(loginUser.getId(), refreshToken); userService.saveRefreshToken(loginUser.getId(), refreshToken);
resultMap.put("name",loginUser.getName()); resultMap.put("name", loginUser.getName());
resultMap.put("role",loginUser.getRole()); resultMap.put("role", loginUser.getRole());
resultMap.put("access-token", accessToken); resultMap.put("access-token", accessToken);
setCookies(response, refreshToken); setCookies(response, refreshToken);
@ -108,7 +116,7 @@ public class UserController {
@Operation(summary = "Access Token 재발급", description = "만료된 access token 을 재발급 받는다.") @Operation(summary = "Access Token 재발급", description = "만료된 access token 을 재발급 받는다.")
@PostMapping("/refresh") @PostMapping("/refresh")
public ResponseEntity<?> refreshToken(HttpServletRequest request,HttpServletResponse response) { public ResponseEntity<?> refreshToken(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies(); Cookie[] cookies = request.getCookies();
String token = null; String token = null;
if (cookies != null) { if (cookies != null) {
@ -120,9 +128,9 @@ public class UserController {
} }
} }
try{ try {
jwtUtil.checkToken(token); jwtUtil.checkToken(token);
}catch (Exception e){ } catch (Exception e) {
throw new InvalidTokenException(); throw new InvalidTokenException();
} }
@ -140,7 +148,7 @@ public class UserController {
Map<String, Object> resultMap = new HashMap<>(); Map<String, Object> resultMap = new HashMap<>();
resultMap.put("access-token", accessToken); resultMap.put("access-token", accessToken);
userService.saveRefreshToken(userId,refreshToken); userService.saveRefreshToken(userId, refreshToken);
setCookies(response, refreshToken); setCookies(response, refreshToken);
@ -175,7 +183,7 @@ public class UserController {
} }
private void setCookies(HttpServletResponse response, String refreshToken){ private void setCookies(HttpServletResponse response, String refreshToken) {
Cookie refreshCookie = new Cookie("refresh-token", refreshToken); Cookie refreshCookie = new Cookie("refresh-token", refreshToken);
refreshCookie.setPath("/"); refreshCookie.setPath("/");
refreshCookie.setHttpOnly(true); refreshCookie.setHttpOnly(true);

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

@ -20,9 +20,13 @@ public interface UserService {
String getUserName(Long id); String getUserName(Long id);
void changeUserInfo(InfoDto infoDto,Long id); void changeUserInfo(InfoDto infoDto, Long id);
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())
@ -36,7 +35,7 @@ public class UserServiceImpl implements UserService {
} }
public User login(User user){ public User login(User user) {
Optional<User> findUser = userRepository.findByUserId(user.getUserId()); Optional<User> findUser = userRepository.findByUserId(user.getUserId());
if (findUser.isEmpty()) { if (findUser.isEmpty()) {
@ -63,32 +62,29 @@ 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();
} }
@Override @Override
public void changeUserInfo(InfoDto infoDto, Long id){ public void changeUserInfo(InfoDto infoDto, Long id) {
User user = userRepository.findById(id).orElseThrow(IllegalArgumentException::new); User user = userRepository.findById(id).orElseThrow(IllegalArgumentException::new);
if (infoDto.getName() != null) if (infoDto.getName() != null)
user.setName(infoDto.getName()); user.setName(infoDto.getName());
if(infoDto.getEmail()!=null) if (infoDto.getEmail() != null)
user.setEmail(infoDto.getEmail()); user.setEmail(infoDto.getEmail());
userRepository.save(user); userRepository.save(user);
} }
@Override @Override
public void changePassword(PasswordDto passwordDto, Long id){ public void changePassword(PasswordDto passwordDto, Long id) {
User user = userRepository.findById(id).orElse(null); User user = userRepository.findById(id).orElse(null);
if (user == null) { if (user == null) {
@ -114,30 +110,52 @@ public class UserServiceImpl implements UserService {
} }
public String getTempPassword() { public String getTempPassword() {
char[] charSet = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', char[] charSet = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
String str = ""; String str = "";
int idx = 0; int idx = 0;
for (int i=0; i<10; i++) { for (int i = 0; i < 10; i++) {
idx = (int) (charSet.length * Math.random()); idx = (int) (charSet.length * Math.random());
str += charSet[idx]; str += charSet[idx];
} }
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);
} }
@Override @Override
public String getRefreshToken(Long id){ public String getRefreshToken(Long id) {
return userRepository.getRefreshToken(id); return userRepository.getRefreshToken(id);
} }
@Override @Override
public void deleteRefreshToken(Long id){ public void deleteRefreshToken(Long id) {
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}