Skip to content

Commit

Permalink
전화번호 인증
Browse files Browse the repository at this point in the history
  • Loading branch information
hhhello0507 committed Jan 12, 2025
1 parent 4bd1ad6 commit 775b4c3
Show file tree
Hide file tree
Showing 18 changed files with 158 additions and 40 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
spring.datasource.password: ${{ secrets.DB_PASSWORD }}
neis.api-key: ${{ secrets.NEIS_API_KEY }}
jwt.secret-key: ${{ secrets.JWT_SECRET_KEY }}
coolsms.api-key: ${{ secrets.COOL_SMS_API_KEY }}
coolsms.api-secret: ${{ secrets.COOL_SMS_API_SECRET }}
coolsms.sender-phone: ${{ secrets.COOL_SMS_SENDER_PHONE }}
spring.profiles.active: 'prd'
- name: Build with Gradle
run: |
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ repositories {
dependencies {
// Swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
implementation("net.nurigo:javaSDK:2.2")

// JWT
val jwtVersion = "0.12.5"
Expand Down
8 changes: 7 additions & 1 deletion src/main/kotlin/com/ohayo/moyamoya/api/user/UserApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ class UserApi(
fun signUp(req: SignUpRequest) = userService.signUp(req)

@GetMapping("exists")
fun exists(@RequestParam tel: String) = userService.exists(tel)
fun exists(@RequestParam phone: String) = userService.exists(phone)

@PostMapping("refresh")
fun refresh(@RequestBody req: RefreshReq) = userService.refresh(req)

@PostMapping("authorization-code")
fun sendAuthorizationCode(@RequestParam phone: String) = userService.sendAuthorizationCode(phone)

@PostMapping("authorize-code")
fun authorizeCode(@RequestParam phone: String, @RequestParam code: String) = userService.authorizeCode(phone, code)
}
48 changes: 35 additions & 13 deletions src/main/kotlin/com/ohayo/moyamoya/api/user/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.ohayo.moyamoya.api.user.value.SignUpRequest
import com.ohayo.moyamoya.core.*
import com.ohayo.moyamoya.core.extension.findByIdSafety
import com.ohayo.moyamoya.global.CustomException
import com.ohayo.moyamoya.infra.sms.SmsClient
import com.ohayo.moyamoya.infra.token.JwtClient
import com.ohayo.moyamoya.infra.token.JwtPayloadKey
import com.ohayo.moyamoya.infra.token.Token
Expand All @@ -16,15 +17,17 @@ class UserService(
private val userRepository: UserRepository,
private val schoolRepository: SchoolRepository,
private val jwtClient: JwtClient,
private val smsClient: SmsClient,
private val phoneCodeRepository: PhoneCodeRepository
) {
fun signUp(req: SignUpRequest): Token {
if (userRepository.existsByTel(req.tel)) throw CustomException(HttpStatus.BAD_REQUEST, "이미 가입된 계정")
if (userRepository.existsByPhone(req.phone)) throw CustomException(HttpStatus.BAD_REQUEST, "이미 가입된 계정")

val school = schoolRepository.findByIdSafety(req.schoolId)

userRepository.save(
val user = userRepository.save(
UserEntity(
tel = req.tel,
phone = req.phone,
school = school,
schoolGrade = req.schoolGrade,
schoolClass = req.schoolClass,
Expand All @@ -34,23 +37,42 @@ class UserService(
profileImageUrl = req.profileImageUrl,
)
)

// TODO: JWT
return Token(
"", ""

return jwtClient.generate(user)
}

fun sendAuthorizationCode(phone: String) {
val code = smsClient.sendAuthorizationCode(phone)
phoneCodeRepository.save(
PhoneCodeEntity(
phone = phone,
code = code
)
)
}

fun authorizeCode(phone: String, code: String) {
val codes = phoneCodeRepository.findByIsActive(phone = phone, code = code)
if (codes.isEmpty()) {
throw CustomException(HttpStatus.BAD_REQUEST, "인증 실패")
}

val disabledCodes = codes.map {
it.disable()
it
}

phoneCodeRepository.saveAll(disabledCodes)
}

fun exists(tel: String) = userRepository.existsByTel(tel)



fun exists(phone: String) = userRepository.existsByPhone(phone)

fun refresh(req: RefreshReq): Token {
jwtClient.parseToken(req.refreshToken)

val user = run {
val tel = jwtClient.payload(JwtPayloadKey.TEL, req.refreshToken)
userRepository.findByTelSafety(tel)
val phone = jwtClient.payload(JwtPayloadKey.PHONE, req.refreshToken)
userRepository.findByPhoneSafety(phone)
}

return jwtClient.generate(user)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.ohayo.moyamoya.api.user.value
import com.ohayo.moyamoya.core.Gender

data class SignUpRequest(
val tel: String,
val phone: String,
val schoolId: Int,
val schoolGrade: Int,
val schoolClass: Int,
Expand Down
22 changes: 22 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/core/PhoneCodeEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ohayo.moyamoya.core

import jakarta.persistence.*

@Entity
@Table(name = "tbl_phone_code")
class PhoneCodeEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Int = 0,
@Column(nullable = false)
val phone: String,
@Column(nullable = false)
val code: String,
@Column(nullable = false)
var isActive: Boolean = true
) {

fun disable() {
this.isActive = false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ohayo.moyamoya.core

import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface PhoneCodeRepository: JpaRepository<PhoneCodeEntity, Int> {
@Query("SELECT m FROM PhoneCodeEntity m WHERE m.isActive = true AND m.phone = :phone AND m.code = :code")
fun findByIsActive(phone: String, code: String): List<PhoneCodeEntity>
}
14 changes: 7 additions & 7 deletions src/main/kotlin/com/ohayo/moyamoya/core/UserEntity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,30 @@ class UserEntity(
val id: Int = 0,

@Column(unique = true, nullable = false)
val tel: String,
val phone: String,

@JoinColumn(name = "school_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
val school: SchoolEntity,

@Column(nullable = false)
val schoolGrade: Int,

@Column(nullable = false)
val schoolClass: Int,

@Column(nullable = false)
val name: String,

@Column(nullable = false)
val gender: Gender,

@Column(nullable = false)
val password: String,

@Column(nullable = false)
val profileImageUrl: String,

@Column(nullable = false)
val userRole: UserRole = UserRole.NORMAL
)
8 changes: 4 additions & 4 deletions src/main/kotlin/com/ohayo/moyamoya/core/UserRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import org.springframework.stereotype.Repository

@Repository
interface UserRepository : JpaRepository<UserEntity, Int> {
fun existsByTel(tel: String): Boolean
fun existsByPhone(phone: String): Boolean

fun findByTel(tel: String): UserEntity?
fun findByPhone(phone: String): UserEntity?
}

fun UserRepository.findByTelSafety(tel: String) =
findByTel(tel) ?: throw CustomException(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다")
fun UserRepository.findByPhoneSafety(phone: String) =
findByPhone(phone) ?: throw CustomException(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다")
9 changes: 7 additions & 2 deletions src/main/kotlin/com/ohayo/moyamoya/global/SecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@ class SecurityConfig(
.authorizeHttpRequests {
it.requestMatchers(
// Features
"auth/refresh",
"schools",
"/user/refresh",
"/user/sign-up",
"/user/exists",
"/user/authorize-code",
"/user/authorization-code",

"/schools",

// ETC
"test/**",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ class JwtAuthenticationFilter(
}

private fun setAuthentication(token: String) {
val tel = jwtUtils.payload(JwtPayloadKey.TEL, token)
val details = userDetailsService.loadUserByUsername(tel)
val phone = jwtUtils.payload(JwtPayloadKey.PHONE, token)
val details = userDetailsService.loadUserByUsername(phone)
SecurityContextHolder.getContext().authentication =
UsernamePasswordAuthenticationToken(details, null, details.authorities)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class JwtUserDetails(
) : UserDetails {
override fun getAuthorities() = listOf(GrantedAuthority { user.userRole.name })
override fun getPassword() = user.password
override fun getUsername() = user.tel
override fun getUsername() = user.phone
override fun isAccountNonExpired() = true // 계정이 만료되지 않았는지
override fun isAccountNonLocked() = true // 계정이 잠기지 않았는지
override fun isCredentialsNonExpired() = true // 비밀번호가 만료되지 않았는지
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.ohayo.moyamoya.global.jwt
import com.ohayo.moyamoya.core.UserRepository
import com.ohayo.moyamoya.core.findByTelSafety
import com.ohayo.moyamoya.core.findByPhoneSafety
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.stereotype.Service

Expand All @@ -9,5 +9,5 @@ class JwtUserDetailsService(
private val userRepository: UserRepository,
) : UserDetailsService {
override fun loadUserByUsername(username: String) =
JwtUserDetails(userRepository.findByTelSafety(username))
JwtUserDetails(userRepository.findByPhoneSafety(username))
}
12 changes: 12 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/infra/sms/CoolSMSProperties.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ohayo.moyamoya.infra.sms

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
import org.springframework.boot.context.properties.bind.ConstructorBinding

@ConfigurationProperties("coolsms")
class CoolSMSProperties @ConstructorBinding constructor(
val apiKey: String,
val apiSecret: String,
val senderPhone: String
)
34 changes: 34 additions & 0 deletions src/main/kotlin/com/ohayo/moyamoya/infra/sms/SmsClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.ohayo.moyamoya.infra.sms

import net.nurigo.java_sdk.api.Message
import net.nurigo.java_sdk.exceptions.CoolsmsException
import org.springframework.stereotype.Component
@Component
class SmsClient(
private val coolSMSProperties: CoolSMSProperties
) {
fun sendAuthorizationCode(phone: String): String {
val authorizationCode = ((Math.random() * (999999 - 100000 + 1)).toInt() + 100000).toString()
val text = "[모야모야]\n인증번호: $authorizationCode\n인증 번호를 입력해주세요."

sendText(phone = phone, text = text)

return authorizationCode
}

private fun sendText(phone: String, text: String) {
val coolsms = Message(coolSMSProperties.apiKey, coolSMSProperties.apiSecret)
try {
coolsms.send(
hashMapOf(
"to" to phone,
"from" to coolSMSProperties.senderPhone,
"type" to "SMS",
"text" to text
)
)
} catch (e: CoolsmsException) {
throw RuntimeException(e)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class JwtClient(
private fun createToken(user: UserEntity, tokenExpired: Long) =
Jwts.builder()
.claim(JwtPayloadKey.ID.key, user.id)
.claim(JwtPayloadKey.TEL.key, user.tel)
.claim(JwtPayloadKey.PHONE.key, user.phone)
.claim(JwtPayloadKey.ROLE.key, user.userRole)
.issuedAt(Date())
.expiration(Date(Date().time + tokenExpired))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ enum class JwtPayloadKey(
val key: String
) {
ID("id"),
TEL("email"),
PHONE("phone"),
ROLE("role");
}
14 changes: 9 additions & 5 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ jwt:
expired:
access: 3_600_000 # 1h
refresh: 7_884_000_000 # 2M
#springdoc:
# swagger-ui:
# groups-order: DESC # path, query, body, response 순으로 출력
# tags-sorter: alpha # 태그를 알파벳 순으로 정렬
# operations-sorter: method # delete - get - patch - post - put 순으로 정렬, alpha를 사용하면 알파벳 순으로 정렬 가능
coolsms:
api-key: ${COOL_SMS_API_KEY}
api-secret: ${COOL_SMS_API_SECRET}
sender-phone: ${COOL_SMS_SENDER_PHONE}
springdoc:
swagger-ui:
groups-order: DESC # path, query, body, response 순으로 출력
tags-sorter: alpha # 태그를 알파벳 순으로 정렬
operations-sorter: method # delete - get - patch - post - put 순으로 정렬, alpha를 사용하면 알파벳 순으로 정렬 가능

0 comments on commit 775b4c3

Please sign in to comment.