Skip to content

Commit

Permalink
메일 인증 시 redis 활용하도록 수정 (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
kih00 authored Mar 10, 2025
1 parent 5640596 commit e92252d
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 67 deletions.
19 changes: 10 additions & 9 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y docker-compose
# # 2. Docker Compose로 DB 시작
# - name: Start Database with Docker Compose
# run: |
# docker-compose up -d mysql
# working-directory: .
# 2. Docker Compose로 DB 시작
- name: Start Database with Docker Compose
run: |
docker-compose up -d mysql
working-directory: .

# 2. Gradle 빌드
- name: Build project
Expand Down Expand Up @@ -103,6 +103,7 @@ jobs:
DB_ENDPOINT=${{ secrets.DB_ENDPOINT }}
DB_USERNAME=${{ secrets.DB_USERNAME }}
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
REDIS_ENDPOINT=${{ secrets.REDIS_ENDPOINT }}
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
KAKAO_CLIENT_ID=${{ secrets.KAKAO_CLIENT_ID }}
Expand All @@ -122,7 +123,7 @@ jobs:
# EC2에서 스크립트 실행
ssh -i private_key.pem -o StrictHostKeyChecking=no ubuntu@${{ secrets.EC2_PUBLIC_IP }} "bash /home/ubuntu/deploy.sh"
# - name: Stop Database
# run: |
# docker-compose down
# working-directory: .
- name: Stop Database
run: |
docker-compose down
working-directory: .
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-mail")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-logging")
implementation("org.springframework.boot:spring-boot-starter-data-redis")
testImplementation("org.springframework.boot:spring-boot-starter-test")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0")
testImplementation("com.ninja-squad:springmockk:4.0.2")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.wafflestudio.toyproject.memoWithTags.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
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer

@Configuration
class RedisConfig(
@Value("\${spring.data.redis.host}") private val host: String,
@Value("\${spring.data.redis.port}") private val port: Int
) {
@Bean
fun redisConnectionFactory(): RedisConnectionFactory = LettuceConnectionFactory(host, port)

@Bean
fun redisTemplate(factory: RedisConnectionFactory): RedisTemplate<String, Any> {
val template = RedisTemplate<String, Any>()
template.connectionFactory = factory
template.keySerializer = StringRedisSerializer()
template.valueSerializer = GenericJackson2JsonRedisSerializer()
return template
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
package com.wafflestudio.toyproject.memoWithTags.mail

import com.wafflestudio.toyproject.memoWithTags.mail.persistence.EmailVerificationEntity
import java.time.LocalDateTime

class EmailVerification(
val id: Long,
val email: String,
val code: String,
val expiryTime: LocalDateTime
val code: String
) {
companion object {
fun fromEntity(entity: EmailVerificationEntity): EmailVerification {
return EmailVerification(
id = entity.id!!,
email = entity.email,
code = entity.code,
expiryTime = entity.expiryTime
email = entity.id,
code = entity.code
)
}
}

override fun toString(): String {
return "{id: $id, email: $email, code: $code, expiryTime: $expiryTime}"
return "{email: $email, code: $code}"
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
package com.wafflestudio.toyproject.memoWithTags.mail.persistence

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import java.time.LocalDateTime
import org.springframework.data.redis.core.RedisHash
import org.springframework.data.redis.core.TimeToLive
import org.springframework.data.redis.core.index.Indexed

@Entity(name = "emails")
class EmailVerificationEntity(
@RedisHash(value = "email_verification")
data class EmailVerificationEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(name = "email", nullable = false)
val email: String,
@Column(name = "code", nullable = false)
val code: String,
@Column(name = "verified", nullable = false)
var verified: Boolean = false,
@Column(name = "expiryTime", nullable = false)
val expiryTime: LocalDateTime
)
val id: String, // email ([email protected])

@Indexed
val code: String, // verification code (000000)

var verified: Boolean // default: false, verified user: true
) {
@TimeToLive
fun getTimeToLive(): Long {
return if (verified) {
86400 // 메일 인증이 확인된 인증 정보는 TTL 24시간으로 설정
} else {
300 // 메일 인증을 완료하지 않은 인증 정보는 TTL 5분으로 설정
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package com.wafflestudio.toyproject.memoWithTags.mail.persistence

import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalDateTime
import org.springframework.data.repository.CrudRepository

interface EmailVerificationRepository : JpaRepository<EmailVerificationEntity, Long> {
fun deleteByExpiryTimeBefore(expiryTime: LocalDateTime)
fun deleteAllByEmail(email: String)
fun findByEmail(email: String): EmailVerificationEntity?
fun findByEmailAndCode(email: String, code: String): EmailVerificationEntity?
interface EmailVerificationRepository : CrudRepository<EmailVerificationEntity, String> {
fun deleteAllById(id: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Profile("!prod")
@Service
Expand All @@ -30,9 +29,9 @@ class NoOpMailService(
override fun createVerificationCode(email: String): EmailVerification {
val randomCode = "000000"
val codeEntity = EmailVerificationEntity(
email = email,
id = email,
code = randomCode,
expiryTime = LocalDateTime.now().plusDays(1)
verified = false
)
return EmailVerification.fromEntity(emailVerificationRepository.save(codeEntity))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import org.springframework.mail.javamail.JavaMailSender
import org.springframework.mail.javamail.MimeMessageHelper
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Profile("prod")
@Service
Expand Down Expand Up @@ -48,9 +47,9 @@ class SmtpMailService(
}

val codeEntity = EmailVerificationEntity(
email = email,
id = email,
code = randomCode,
expiryTime = LocalDateTime.now().plusDays(1)
verified = false
)
return EmailVerification.fromEntity(emailVerificationRepository.save(codeEntity))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.wafflestudio.toyproject.memoWithTags.exception.SignInInvalidException
import com.wafflestudio.toyproject.memoWithTags.exception.UpdatePasswordInvalidException
import com.wafflestudio.toyproject.memoWithTags.exception.UserNotFoundException
import com.wafflestudio.toyproject.memoWithTags.mail.EmailVerification
import com.wafflestudio.toyproject.memoWithTags.mail.persistence.EmailVerificationEntity
import com.wafflestudio.toyproject.memoWithTags.mail.persistence.EmailVerificationRepository
import com.wafflestudio.toyproject.memoWithTags.mail.service.MailService
import com.wafflestudio.toyproject.memoWithTags.user.JwtUtil
Expand All @@ -20,11 +21,11 @@ import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserEntity
import com.wafflestudio.toyproject.memoWithTags.user.persistence.UserRepository
import org.mindrot.jbcrypt.BCrypt
import org.slf4j.LoggerFactory
import org.springframework.data.repository.findByIdOrNull
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
import java.time.LocalDateTime

@Service
class UserService(
Expand All @@ -46,10 +47,12 @@ class UserService(
// 소셜 로그인 사용 여부와 무관하게 동일 이메일이 존재하기만 하면 예외 처리한다.
if (userRepository.findByEmail(email) != null) throw EmailAlreadyExistsException()

logger.info(emailVerificationRepository.findAll().toString())

// 메일 인증 과정을 거치지 않고 바로 회원가입 시도 시 예외 처리한다. 검증 후 인증 데이터는 삭제한다.
val verification = emailVerificationRepository.findByEmail(email) ?: throw EmailNotVerifiedException()
val verification = emailVerificationRepository.findByIdOrNull(email) ?: throw EmailNotVerifiedException()
if (!verification.verified) throw EmailNotVerifiedException()
emailVerificationRepository.deleteById(verification.id!!)
emailVerificationRepository.deleteById(email)

val encryptedPassword = BCrypt.hashpw(password, BCrypt.gensalt())
// 클라이언트에서 쓸 유저 식별 번호인 userNumber는 해당 유저가 서비스에 가입한 순서 + 1로 한다.
Expand Down Expand Up @@ -77,7 +80,7 @@ class UserService(
email: String
) {
// 이미 인증 메일을 보낸 주소로 또 시도하는 경우에는 해당 이메일로 발송된 인증번호 데이터를 삭제한다.
emailVerificationRepository.deleteAllByEmail(email)
emailVerificationRepository.deleteAllById(email)

val verification: EmailVerification = mailService.createVerificationCode(email)
val title = "Memo with tags 이메일 인증 번호"
Expand Down Expand Up @@ -107,10 +110,11 @@ class UserService(
email: String,
code: String
): Boolean {
val verification = emailVerificationRepository.findByEmailAndCode(email, code) ?: throw MailVerificationException()
if (verification.expiryTime.isBefore(LocalDateTime.now())) throw AuthenticationFailedException()
logger.info(emailVerificationRepository.findAll().toString())
val verification = emailVerificationRepository.findByIdOrNull(email) ?: throw MailVerificationException()
if (verification.code != code) throw MailVerificationException()
// 인증 성공 시, verification의 Verified 필드가 true로 바뀌어 회원가입의 검증 절차를 통과한다.
verification.verified = true
emailVerificationRepository.save(EmailVerificationEntity(email, code, true))
logger.info("verified email code")
return true
}
Expand Down Expand Up @@ -144,9 +148,9 @@ class UserService(
newPassword: String
) {
// 인증된 이메일인지 확인하고, 검증 후 인증 데이터를 삭제한다.
val verification = emailVerificationRepository.findByEmail(email) ?: throw EmailNotVerifiedException()
val verification = emailVerificationRepository.findByIdOrNull(email) ?: throw EmailNotVerifiedException()
if (!verification.verified) throw EmailNotVerifiedException()
emailVerificationRepository.deleteById(verification.id!!)
emailVerificationRepository.deleteById(email)

// 해당하는 유저가 없으면 예외를 발생시킨다.
val userEntity = userRepository.findByEmail(email) ?: throw UserNotFoundException()
Expand Down Expand Up @@ -237,15 +241,6 @@ class UserService(
return userRepository.findByEmail(email) ?: throw UserNotFoundException()
}

/**
* 메일 정오에 만료된 인증 코드 엔티티를 삭제하는 함수
*/
@Transactional
@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Seoul") // 매일 정오에 만료 코드 삭제
fun deleteExpiredVerificationCode() {
emailVerificationRepository.deleteByExpiryTimeBefore(LocalDateTime.now())
}

/**
* 매일 정오에 메일 인증이 되지 않은 유저를 삭제하는 함수
*/
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ spring:
config:
import: "optional:file:.env[.properties]"

data:
redis:
host: localhost
port: 6379

logging:
level:
root: info
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ spring:
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
show-sql: false
data:
redis:
host: ${REDIS_ENDPOINT}
port: 6379
logging:
level:
org.hibernate.SQL: WARN
Expand Down

0 comments on commit e92252d

Please sign in to comment.