โ๊ฐ์
Spring Security ๋ฅผ ์ด์ฉํ์ฌ ์ธ์ฆ ๋ฐ ์ธ๊ฐ๋ฅผ ๊ตฌํํ๋ฉด์ ๋์น๊ณ ์๋์ ์ด ์์๋ค. ํ์๊ฐ์
์ ์ํํ ํ ํ์์ด ์
๋ ฅํ ์ ๋ณด๊ฐ ๋ง๋์ง ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ๋ ๋ก์ง์ ๊ตฌํํ์ ์ด ์์๋ค. ๋ค๋ฅธ ๋ง์ ์ ํ๋ฆฌ์ผ์ด์
์ ์ด์ฉํ๋ฉด ํ์๊ฐ์
ํ ์ด๋ฉ์ผ ์ธ์ฆ์ ์ํํ๋ผ๊ณ ํ๊ฑฐ๋ ํ์ด์ค ์์ด๋๋ฅผ ํตํด ํ๋ฒ ๋ ์ธ์ฆ์ ์ํํ๋ค. ์ด๊ฒ์ 2FA(Two-Factor-Authentication) ์ด๋ผ๊ณ ํ๋ค. ์ด๋ถ๋ถ์ ํ์๊ฐ์
์ ์ ์ฉํด ๋ณด๋ ค๊ณ ํ๋ค.
๐ก 2FA ์ ๋ํด
์ฝ๋๋ก ๊ตฌํํ๊ธฐ ์ ์ 2FA ์ ๋ํด ๊ฐ๋ ๋จผ์ ์ง๊ณ ๋์ด๊ฐ๊ฒ ๋ค. 2FA ๋ ์ธ์ฆ ์ํ์ 2๊ฐ์ง ๋ฐฉ๋ฒ์ผ๋ก ์ธ์ฆ์ ์ํํ๋ ๊ฒ์ ๋งํ๋ค. 2๊ฐ์ง ๋ฐฉ์์ผ๋ก ์ธ์ฆ์ ์ํํ์ฌ ๋ณด์์ ๊ฐํํ๋ ๋ฐฉ์์ผ๋ก MFA(๋ค์ค์ธ์ฆ๋ฐฉ์)์ด๋ผ๊ณ ๋ ๋ถ๋ฆฐ๋ค.
TOTP)SMS๋ก ์ ์ก๋ ์ธ์ฆ ์ฝ๋Email๋ก ์ ์ก๋ ์ธ์ฆ ์ฝ๋์ด๋ฌํ ๋ค์ค ์ธ์ฆ ๋ฐฉ์์ ๋จ์ผ ์ธ์ฆ๋ฐฉ์์ ๋นํด ํด์ปค๊ฐ ๋๊ฐ์ง ์ธ์ฆ ์ ๋ณด๋ฅผ ํ์ณ์ผ ํ๊ณ , ๋๋ฒ์งธ ์์๋ ์ง๋ฌธ์ด๋ ์๊ฐ ์ ํ ์์๋๋ฌธ์ ํดํนํ๊ธฐ์ด๋ ต๊ธฐ์ ๋ณด์์ฑ์ด ๋ ๋ฐ์ด๋๊ณ ๋ฌด์ฐจ๋ณ ๋์ ๊ณต๊ฒฉ๊ณผ ํผ์ฑ ๊ณต๊ฒฉ์ ๋ํ ๋ณด์์ ๊ฐํํ ์ ์๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ด๊ฐ ๊ตฌํ์ ํ ๋ถ๋ถ์ ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ์ง ์์ ์ด๋ฉ์ผ ๋ฐฉ์์ผ๋ก 2FA ๋ฅผ ๊ตฌํํด๋ณผ ์๊ฐ์ด๋ค. ํ์๊ฐ์
์์ ์ ์ฉ์ํฌ ๊ฒ์ด๋ค. ์ฌ๋ฌ ๋ค๋ฅธ ์ธ์ฆ ๋ฐ ์ธ๊ฐ๋ฅผ ๊ณ ๋ คํ ์ ์์ง๋ง ํ๋ก ํธ๊ฐ์๊ณ ํ
์คํธํด๋ณผ๋งํ ๋๋ฐ์ด์ค๊ฐ ์๊ธฐ์ ํ์๊ฐ์
์ ์ด๋ฉ์ผ ์ธ์ฆ์ผ๋ก๋ผ๋ ๊ตฌํํ๋ ค๊ณ ํ๋ค.
๐งโ๐ป ์ฝ๋๋ก ๊ตฌํํ๊ธฐ
@Slf4j
@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;
private final EncryptionUtil encryptionUtil;
private static final String MAIL_SUBJECT = "์ธ์ฆ ์ฝ๋ ์๋ด";
@Value("${spring.mail.username}")
String host;
public String sendMail(String to) throws MessagingException {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(message, true, StandardCharsets.UTF_8.name());
Context context = new Context();
String code = generateVerificationCode(); // ์ธ์ฆ ์ฝ๋ ์์ฑ
context.setVariable("code", code); // Email ์ธ์ฆ Form ์ ๋ฃ์ Code
context.setVariable("email", encryptionUtil.encrypt(to)); // Email ์ธ์ฆ ํผ์ ๋ค์ด๊ฐ ์ํธํ๋ Email
String htmlContent = templateEngine.process("email/verification", context); // Email ์ธ์ฆํผ String ๋ณํ
mimeMessageHelper.setFrom(host);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(MAIL_SUBJECT);
mimeMessageHelper.setText(htmlContent, true);
javaMailSender.send(message);
return code;
}
private String generateVerificationCode() {
Random random = new Random();
StringBuilder code = new StringBuilder();
IntStream.range(0, 6).forEach(e -> code.append(random.nextInt(10)));
return code.toString();
}
Email ์ ๋ด์๋ณด๋ธ๋ค.Email ์ hidden ํ๋์ ๋ฐ์ธ๋ฉ ์ํฌ๊ฒ์ด๋ค.@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final MailService mailService;
@Transactional
public UserCreateRespDto register(UserCreateRepDto dto) {
String encodedPassword = passwordEncoder.encode(dto.getPassword());
boolean isExistsNickname = userRepository.existsByNickname(dto.getNickname());
if (isExistsNickname) { // ๋๋ค์ ์ค๋ณต ๊ฒ์ฆ
throw new BusinessException(ErrorCode.EXISTS_ALREADY_USER);
}
boolean isExistsEmail = userRepository.existsByEmail(dto.getEmail());
if (isExistsEmail) { // ์ด๋ฉ์ผ ์ค๋ณต ๊ฒ์ฆ
throw new BusinessException(ErrorCode.EXISTS_ALREADY_EMAIL);
}
User user = UserCreateRepDto.from(dto, encodedPassword);
User savedUser = userRepository.save(user);
try {
mailService.sendMail(savedUser.getEmail()); // ์ด๋ฉ์ผ ์ ์ก
} catch (MessagingException e) {
throw new BusinessException(ErrorCode.SERVER_ERROR);
}
return new UserCreateRespDto(savedUser);
}
}
Domain-Service ์ธ UserService ์์ ํ์๊ฐ์
์ ์ด๋ฉ์ผ์ ๋ณด๋ผ ์ ์๋๋ก ์ฒ๋ฆฌํ๋ค.try-catch ๋ก ์์ธ์ฒ๋ฆฌ๊น์ง ํด์ผํ๋ ๋น์ฆ๋์ค๋ก์ง์ ์ฝ๋๋ฐ ๋ฐฉํด๋๋ค.
๐จ ๋ฌธ์ ์ ๋ฐ์

API ์๋๊ฐ 5์ด ๊ฐ๊น์ด ๋์ค๋๊ฒ์ ๋ณผ ์ ์๋ค.Email ๋ฐ์ก ๋ก์ง์ด ๋ฌธ์ ๋๊ฒ์ ์ ์ ์๋ค.๊ทธ๋ ๋ค๋ฉด ์ด๋ค์์ผ๋ก ํด๊ฒฐํด์ผํ๋์ง ํ๋ฆ์ ์ด์ผ๊ธฐํด๋ณด๋ฉฐ ์ ๋ฆฌํ๊ณ ๊ตฌํํ๋๋ก ํด๋ณด์
DB์ ์ ์ฅ๋๊ณ ๋ฐ์ก๋์ด์ผ ํ๋ค. @Slf4j
@Component
@RequiredArgsConstructor
public class UserRegisterEventListener {
private final MailService mailService;
private final RedisTemplate<String, String> redisTemplate;
private static final long VERIFICATION_CODE_EXPIRATION = 10 * 60;
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserRegistration(UserRegisterEvent event) {
try {
String code = mailService.sendMail(event.email());
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(event.email(), code, Duration.ofSeconds(VERIFICATION_CODE_EXPIRATION));
} catch (MessagingException e) {
log.warn("Mail ์ ์ก ์คํจ : {}", e.getMessage());
}
}
}
@Transactional
public UserCreateRespDto register(UserCreateRepDto dto) {
String encodedPassword = passwordEncoder.encode(dto.getPassword());
boolean isExistsNickname = userRepository.existsByNickname(dto.getNickname());
if (isExistsNickname) {
throw new BusinessException(ErrorCode.EXISTS_ALREADY_USER);
}
boolean isExistsEmail = userRepository.existsByEmail(dto.getEmail());
if (isExistsEmail) {
throw new BusinessException(ErrorCode.EXISTS_ALREADY_EMAIL);
}
User user = UserCreateRepDto.from(dto, encodedPassword);
User savedUser = userRepository.save(user);
// ์ด๋ฒคํธ ๋ฐํ
applicationEventPublisher.publishEvent(new UserRegisterEvent(dto.getEmail()));
return new UserCreateRespDto(savedUser);
}
ApplicationEventPublisher ๋ฅผ ํตํด ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋ค.
200ms ๋๋ก ํ์คํ ์ฑ๋ฅ์ด ๊ฐ์ ๋์๋ค.โ ์ด๋ฉ์ผ ์ ํจ์ฑ ๊ฒ์ฆ
@Transactional
public void verificationEmail(String email, String code) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
String decryptedEmail = encryptionUtil.decrypt(email);
String storedCode = ops.get(decryptedEmail);
if (!storedCode.equals(code)) {
throw new BusinessException(ErrorCode.INVALID_EMAIL_CODE);
}
redisTemplate.delete(decryptedEmail);
User findUser = userRepository.findByEmail(decryptedEmail)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
findUser.addAuthorizationRole();
userRepository.save(findUser);
}
Redis ์ ์ ์ฅ๋์๋ ์ธ์ฆ์ฝ๋๋ฅผ ๊บผ๋ด์ ๊ฒ์ฆํ๊ณ Redis ์ ๊ฐ์ ์ง์ด๋ค.DB ์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฐพ๊ณ ๊ถํ์ ์ถ๊ฐํ ํ ์ ์ฅํ๋ค.
AuthService ์์ ๊ถํ์ถ๊ฐํ์๋ค๋ฉด ๊ถํ์ ๊ฒ์ฆํ๊ฒ์ ํํฐ์๊ฒ ์์ํ๋๋กํ๋ค. JWT ํ ํฐ์๋ ๊ถํ์ ์ถ๊ฐํ๋๋ก ํด์ผ ํํฐ๋จ์์ ๊ฒ์ฆํ ์ ์๋ค. JWT ํ ํฐ ๊ฒ์ฆ ๋ฌธ์ ๋ ์์ ๋ฌธ์ ๊ธฐ์ ์ฌ๊ธฐ์ ๋ค๋ฃจ์ง ์๊ฒ ๋ค.
๐ ํบ์๋ณด๊ธฐ
ํ์๊ฐ์ ์ ์ด๋ฉ์ผ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ํํ๋๋ก ํ๋ 2FA ๋ฅผ ๊ตฌํํด๋ณด์๋ค. ํด๋ผ์ด์ธํธ๊ฐ ์์๋ค๋ฉด ์ข๋ ๋ค๋ฅธ ์ธ์ฆ ๋ฐฉ์์ ๊ณ ๋ คํด๋ณผ ์ ์๊ฒ ๋๋ฐ ํด๋ผ์ด์ธํธ๊ฐ ์๊ธฐ์ ๊ตฌํ์กฐ๊ฑด์ด ๊น๋ค๋ก์ด์ ์ด ์์ฝ๋ค. ์ถํ ์ธ์ฆ ๋ฐ ์ธ๊ฐ๋ฅผ ํ์ต์ ํ์ฌ SSO ๊ตฌ์ถ๋ ๋ค๋ค๋ณผ ์์ ์ด๋ค. ์ด๋ฉ์ผ ์ธ์ฆ์ ๊ตฌํํ๋ฉด์ ์ฌ๋ฌ ํค์๋๋ค์ ์ป๊ณ ๊ฐ์ผ๋ฏ๋ก ๊ฐ์น์๋ ํ์ต์ด์๋ค. ์ด๋ฒคํธ ๋ฐํ์ ๋ํด์๋ ์ข์ ๊ณต๋ถ๊ฐ ๋ ๊ฒ๊ฐ์ ์ถํ ๊ณต๋ถํด๋ด์ผ๊ฒ ๋ค.