Skip to content

Commit

Permalink
feat: 일반 회원가입 및 로그인
Browse files Browse the repository at this point in the history
  • Loading branch information
DongGeon0908 committed Jul 6, 2024
1 parent 6dc7d53 commit 39addf5
Show file tree
Hide file tree
Showing 33 changed files with 763 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
# Aws Credentials 환경 변수 주입
cloud.aws.credentials.accessKey: ${{ secrets.AWS_ACCESS_KEY_ID }}
cloud.aws.credentials.secretKey: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
# Jwt Secret Key 환경 변수 주입
auth.jwt.secret: ${{ auth.jwt.secret }}
# Encrypt 환경 변수 주입
encrypt.key: ${{ encrypt.key }}
encrypt.algorithm: ${{ encrypt.algorithm }}

# gradlew 파일 실행권한 설정
- name: Grant execute permission for gradlew
Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ dependencies {
/** database */
runtimeOnly("com.mysql:mysql-connector-j")

/** jwt */
implementation("com.auth0:java-jwt:${DependencyVersion.AUTH_JWT}")

/** test */
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
Expand Down
3 changes: 3 additions & 0 deletions buildSrc/src/main/kotlin/DependencyVersion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ object DependencyVersion {
const val SPRINGDOC = "2.3.0"
const val JAVADOC_SCRIBE = "0.15.0"

/** auth0-jwt */
const val AUTH_JWT = "4.4.0"

/** test */
const val TEST_CONTAINER_MYSQL = "1.19.8"
const val P6SPY_LOG = "1.9.1"
Expand Down
27 changes: 27 additions & 0 deletions sql/DDL.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
-- scheme
CREATE
DATABASE hero CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

-- 유저 정보
CREATE TABLE `user_info`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'user id',
`nickname` varchar(64) NOT NULL COMMENT '닉네임',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
`modified_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=200000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='유저 정보';

CREATE UNIQUE INDEX uidx__nickname ON user_info (nickname);

-- 일반 회원가입 유저 정보
CREATE TABLE `credential_user_info`
(
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'credential_user_info id',
`uid` bigint NOT NULL COMMENT 'user id',
`username` varchar(256) NOT NULL COMMENT '로그인 id',
`password` varchar(512) NOT NULL COMMENT '로그인 pw',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
`modified_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=200000 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='유저 정보';

CREATE UNIQUE INDEX uidx__uid ON credential_user_info (uid);
CREATE UNIQUE INDEX uidx__username ON credential_user_info (username);
17 changes: 17 additions & 0 deletions src/main/kotlin/com/hero/alignlab/common/encrypt/EncryptData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.hero.alignlab.common.encrypt

data class EncryptData(
val encData: String,
) {
fun dec(encryptor: Encryptor): String {
return encryptor.decrypt(this.encData)
}

companion object {
fun enc(plainData: String, encryptor: Encryptor): EncryptData {
return EncryptData(
encData = encryptor.encrypt(plainData)
)
}
}
}
36 changes: 36 additions & 0 deletions src/main/kotlin/com/hero/alignlab/common/encrypt/Encryptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.hero.alignlab.common.encrypt

import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec

data class Encryptor(
private val key: String,
private val algorithm: String,
) {
fun encrypt(text: String): String {
val cipher = Cipher.ENCRYPT_MODE.cipher()
val encrypted = cipher.doFinal(text.toByteArray(charset(Charsets.UTF_8.name())))

return Base64.getEncoder().encodeToString(encrypted)
}

fun decrypt(text: String): String {
val cipher = Cipher.DECRYPT_MODE.cipher()
val decodedBytes = Base64.getDecoder().decode(text)
val decrypted = cipher.doFinal(decodedBytes)

return String(decrypted, Charsets.UTF_8)
}

private fun Int.cipher(): Cipher {
val cipher = Cipher.getInstance(algorithm)
val keySpec = SecretKeySpec(key.toByteArray(), "AES")
val ivParamSpec = IvParameterSpec(key.toByteArray())

cipher.init(this, keySpec, ivParamSpec)

return cipher
}
}
28 changes: 28 additions & 0 deletions src/main/kotlin/com/hero/alignlab/config/EncryptConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.hero.alignlab.config

import com.hero.alignlab.common.encrypt.Encryptor
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(EncryptConfig.EncryptProperty::class)
class EncryptConfig {
private val logger = KotlinLogging.logger {}

@ConfigurationProperties(prefix = "encrypt")
data class EncryptProperty(
val key: String,
val algorithm: String,
)

@Bean
fun encryptor(
property: EncryptProperty,
): Encryptor {
logger.info { "initialized encryptor. key: ${property.key} algorithm: ${property.algorithm}" }
return Encryptor(property.key, property.algorithm)
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/com/hero/alignlab/config/JwtConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.hero.alignlab.config

import jakarta.validation.constraints.NotBlank
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "auth.jwt")
@ConfigurationPropertiesBinding
data class JwtConfig(
@field:NotBlank
var secret: String = "",
@field:NotBlank
var accessExp: Int = 0,
@field:NotBlank
var refreshExp: Int = 0,
val issuer: String = "hero-alignlab-api",
val audience: String = "hero-alignlab-api",
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.hero.alignlab.config.swagger

import com.hero.alignlab.domain.auth.model.AUTH_TOKEN_KEY
import com.hero.alignlab.domain.auth.model.AuthUser
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import io.swagger.v3.oas.models.servers.Server
import org.springdoc.core.utils.SpringDocUtils
import org.springframework.boot.info.BuildProperties
Expand All @@ -26,19 +31,35 @@ class SpringDocConfig(
.getConfig()
.addRequestWrapperToIgnore(
WebSession::class.java,
RequestContext::class.java
RequestContext::class.java,
AuthUser::class.java
)
}

@Bean
fun openApi(): OpenAPI {
val securityRequirement = SecurityRequirement().addList(AUTH_TOKEN_KEY)
return OpenAPI()
.components(authSetting())
.security(listOf(securityRequirement))
.addServersItem(Server().url("/"))
.info(
Info()
.title(buildProperties.name)
.version(buildProperties.version)
.description("Alignlab Rest API Docs")
.description("Hero Alignlab Rest API Docs")
)
}

private fun authSetting(): Components {
return Components()
.addSecuritySchemes(
AUTH_TOKEN_KEY,
SecurityScheme()
.description("Access Token")
.type(SecurityScheme.Type.APIKEY)
.`in`(SecurityScheme.In.HEADER)
.name(AUTH_TOKEN_KEY)
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.hero.alignlab.config.web

import com.fasterxml.jackson.databind.ObjectMapper
import com.hero.alignlab.domain.auth.application.AuthFacade
import com.hero.alignlab.domain.auth.resolver.ReactiveUserResolver
import org.springframework.context.annotation.Configuration
import org.springframework.core.ReactiveAdapterRegistry
import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver
Expand All @@ -19,7 +21,8 @@ import java.nio.charset.Charset

@Configuration
class WebFluxConfig(
private val objectMapper: ObjectMapper
private val objectMapper: ObjectMapper,
private val authFacade: AuthFacade,
) : WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
Expand Down Expand Up @@ -47,6 +50,7 @@ class WebFluxConfig(
configureHttpMessageCodecs(serverCodecConfigurer)

configurer.addCustomResolver(
ReactiveUserResolver(registry, authFacade),
ReactiveSortHandlerMethodArgumentResolver(),
ReactivePageableHandlerMethodArgumentResolver()
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.hero.alignlab.domain.auth.application

import com.hero.alignlab.common.encrypt.EncryptData
import com.hero.alignlab.common.encrypt.Encryptor
import com.hero.alignlab.config.database.TransactionTemplates
import com.hero.alignlab.domain.auth.model.AuthContextImpl
import com.hero.alignlab.domain.auth.model.AuthUserImpl
import com.hero.alignlab.domain.auth.model.AuthUserToken
import com.hero.alignlab.domain.auth.model.request.SignInRequest
import com.hero.alignlab.domain.auth.model.request.SignUpRequest
import com.hero.alignlab.domain.auth.model.response.SignInResponse
import com.hero.alignlab.domain.auth.model.response.SignUpResponse
import com.hero.alignlab.domain.user.application.CredentialUserInfoService
import com.hero.alignlab.domain.user.application.UserService
import com.hero.alignlab.domain.user.domain.CredentialUserInfo
import com.hero.alignlab.domain.user.domain.UserInfo
import com.hero.alignlab.exception.ErrorCode
import com.hero.alignlab.exception.InvalidRequestException
import com.hero.alignlab.exception.InvalidTokenException
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import java.time.LocalDateTime

@Service
class AuthFacade(
private val userService: UserService,
private val credentialUserInfoService: CredentialUserInfoService,
private val jwtTokenService: JwtTokenService,
private val encryptor: Encryptor,
private val txTemplates: TransactionTemplates,
) {
companion object {
private val TOKEN_EXPIRED_DATE = LocalDateTime.of(2024, 12, 29, 0, 0, 0)
}

fun resolveAuthUser(token: Mono<AuthUserToken>): Mono<Any> {
return jwtTokenService.verifyTokenMono(token)
.handle { payload, sink ->
if (payload.type != "accessToken") {
sink.error(InvalidTokenException(ErrorCode.INVALID_ACCESS_TOKEN))
return@handle
}

val user = userService.getUserByIdOrThrowSync(payload.id)

sink.next(
AuthUserImpl(
uid = user.id,
context = AuthContextImpl(
name = user.nickname
)
)
)
}
}

suspend fun signUp(request: SignUpRequest): SignUpResponse {
if (credentialUserInfoService.existsByUsername(request.username)) {
throw InvalidRequestException(ErrorCode.DUPLICATED_USERNAME_ERROR)
}

val userInfo = userService.save(UserInfo(nickname = request.username))

credentialUserInfoService.save(
CredentialUserInfo(
uid = userInfo.id,
username = request.username,
password = EncryptData.enc(request.password, encryptor)
)
)

return SignUpResponse(
accessToken = jwtTokenService.createToken(userInfo.id, TOKEN_EXPIRED_DATE)
)
}

suspend fun signIn(request: SignInRequest): SignInResponse {
val credentialUserInfo = credentialUserInfoService.findByUsernameAndPassword(request.username, request.password)

val user = userService.getUserByIdOrThrowSync(credentialUserInfo.uid)

return SignInResponse(
accessToken = jwtTokenService.createToken(user.id, TOKEN_EXPIRED_DATE)
)
}
}
Loading

0 comments on commit 39addf5

Please sign in to comment.