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
@@ -1,13 +1,16 @@
package com.example.reservation_solution;

import com.example.reservation_solution.global.config.VerificationProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@SpringBootApplication
@EnableJpaAuditing
@EnableConfigurationProperties(VerificationProperties.class)
public class ReservationSolutionApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.reservation_solution.api.auth.domain;

import java.time.LocalDateTime;

public record VerificationInfo(
String code,
LocalDateTime expiryTime,
boolean verified
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.reservation_solution.api.auth.repository;

import com.example.reservation_solution.api.auth.domain.VerificationInfo;
import com.example.reservation_solution.global.config.VerificationProperties;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class CaffeineVerificationStore implements VerificationStore {

private final Cache<String, VerificationInfo> cache;

public CaffeineVerificationStore(VerificationProperties properties) {
this.cache = Caffeine.newBuilder()
.expireAfterWrite(properties.getCodeExpirationMinutes(), TimeUnit.MINUTES)
.maximumSize(properties.getCacheMaxSize())
.build();
}

@Override
public void save(String email, VerificationInfo info) {
cache.put(email, info);
}

@Override
public VerificationInfo find(String email) {
return cache.getIfPresent(email);
}

@Override
public void invalidate(String email) {
cache.invalidate(email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.reservation_solution.api.auth.repository;

import com.example.reservation_solution.api.auth.domain.VerificationInfo;

public interface VerificationStore {

void save(String email, VerificationInfo info);

VerificationInfo find(String email);

void invalidate(String email);
}
Original file line number Diff line number Diff line change
@@ -1,53 +1,50 @@
package com.example.reservation_solution.api.auth.service;

import com.example.reservation_solution.api.auth.domain.VerificationInfo;
import com.example.reservation_solution.api.auth.repository.VerificationStore;
import com.example.reservation_solution.global.config.VerificationProperties;
import com.example.reservation_solution.global.mail.MailService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@RequiredArgsConstructor
public class EmailVerificationService {

private final MailService mailService;
private final Cache<String, VerificationInfo> storage = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000)
.build();

private static final int CODE_EXPIRATION_MINUTES = 5;
private static final int SIGNUP_WINDOW_MINUTES = 1; // 인증 후 회원가입까지 허용 시간
private final VerificationStore verificationStore;
private final VerificationProperties verificationProperties;

public void sendCode(String email) {
String authCode = generateAuthCode();
storage.put(email, new VerificationInfo(authCode, LocalDateTime.now().plusMinutes(CODE_EXPIRATION_MINUTES), false));
verificationStore.save(email, new VerificationInfo(authCode,
LocalDateTime.now().plusMinutes(verificationProperties.getCodeExpirationMinutes()), false));
mailService.sendAuthEmail(email, authCode);
}

public boolean verifyCode(String email, String inputCode) {
VerificationInfo info = storage.getIfPresent(email);
if (info == null){
VerificationInfo info = verificationStore.find(email);
if (info == null) {
return false;
}
if (info.code().equals(inputCode)) {
storage.put(email, new VerificationInfo(info.code(), LocalDateTime.now().plusMinutes(SIGNUP_WINDOW_MINUTES), true));
verificationStore.save(email, new VerificationInfo(info.code(),
LocalDateTime.now().plusMinutes(verificationProperties.getSignupWindowMinutes()), true));
return true;
}
return false;
}

public boolean isEmailVerified(String email) {
VerificationInfo info = storage.getIfPresent(email);
if (info != null && info.verified && LocalDateTime.now().isBefore(info.expiryTime)) {
log.info("인증 코드 삭제됨 : {}", info.code);
storage.invalidate(email);
VerificationInfo info = verificationStore.find(email);
if (info != null && info.verified() && LocalDateTime.now().isBefore(info.expiryTime())) {
log.info("인증 코드 삭제됨 : {}", info.code());
verificationStore.invalidate(email);
return true;
}
return false;
Expand All @@ -58,5 +55,4 @@ private String generateAuthCode() {
int num = random.nextInt(900000) + 100000;
return String.valueOf(num);
}
private record VerificationInfo(String code, LocalDateTime expiryTime, boolean verified) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.reservation_solution.global.config;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "verification")
public class VerificationProperties {

private final int codeExpirationMinutes;
private final int signupWindowMinutes;
private final int cacheMaxSize;
}
7 changes: 6 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ spring:
active: dev

jwt:
expiration: 1800000 # 30분
expiration: 1800000 # 30분

verification:
code-expiration-minutes: 5
signup-window-minutes: 1
cache-max-size: 1000
5 changes: 5 additions & 0 deletions src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ jwt:
secret: "testsecretkeyforunittestingonly1234567890"
expiration: 3600000

verification:
code-expiration-minutes: 5
signup-window-minutes: 1
cache-max-size: 100

logging:
level:
root: INFO
Expand Down