[Ktor] Ktor를 이용하여 JWT 인증 시스템 구현하기(PostgreSQL) + Postman으로 테스트

밤새·2024년 8월 16일
1

Back/Front

목록 보기
12/12
post-thumbnail

Ktor를 이용하여 JWT 인증 시스템을 구현 과정에 대한 글입니다!
프로젝트 생성 방법DB 연동 이후에 이어지는 내용입니다.

1. 서론

1-1. JWT란?

JWT(Json Web Token)는 JSON 객체를 사용해 정보를 안전하게 전달하는 웹 표준입니다. 주로 사용자 인증에 사용되며, 세션 관리가 필요 없는 상태 비저장(stateless) 방식의 인증 메커니즘으로 많이 사용됩니다. JWT는 클라이언트와 서버 간의 인증 정보를 효율적으로 관리할 수 있어, 특히 RESTful API에서 많이 사용됩니다.

JWT에 더 자세히 알고 싶다면? 이 글을 참고해주세요.

Ktor와 PostgreSQL을 선택한 이유

  • Ktor는 Kotlin 기반의 웹 프레임워크로, 경량성과 높은 성능을 자랑하며 다양한 애플리케이션 개발에 적합합니다. PostgreSQL은 신뢰할 수 있는 오픈 소스 데이터베이스로, 대규모 트랜잭션 처리와 안정성이 요구되는 애플리케이션에서 많이 사용됩니다. 이번 프로젝트에서는 Ktor와 PostgreSQL을 활용하여 JWT 인증 시스템을 구현해 보겠습니다.

2. 프로젝트 설정

먼저, Ktor 프로젝트를 생성하고 필요한 의존성을 추가합니다. 이 과정에서 JWT 인증, 데이터베이스 연동, 직렬화 기능 등을 설정합니다.

2-1. 라이브러리 의존성 설정

build.gradle.kts 파일은 프로젝트에 필요한 라이브러리 의존성을 관리하는 곳입니다.

Ktor, Exposed ORM, PostgreSQL, HikariCP, JWT 라이브러리 등 다양한 라이브러리를 추가했습니다!

dependencies {
    implementation("org.jetbrains.exposed:exposed-core:0.41.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.41.1")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")

    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-server-content-negotiation-jvm")

    // Exposed ORM 라이브러리
    implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
    implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")

    // PostgreSQL JDBC 드라이버
    implementation("org.postgresql:postgresql:42.6.0")

    // HikariCP (커넥션 풀)
    implementation("com.zaxxer:HikariCP:5.0.1")

    // 환경 변수 로드
    implementation("io.github.cdimascio:dotenv-kotlin:6.4.0")

    implementation("io.ktor:ktor-server-auth:$logback_version")
    implementation("io.ktor:ktor-server-auth-jwt:$logback_version")
    implementation("com.auth0:java-jwt:4.2.1")

    implementation("com.h2database:h2:$h2_version")
    implementation("io.ktor:ktor-server-openapi")
    implementation("io.ktor:ktor-server-netty-jvm")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-server-config-yaml")
    testImplementation("io.ktor:ktor-server-test-host-jvm")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

이 부분에서 주요 라이브러리를 간단히 소개하자면

  • Exposed ORM : Kotlin을 위한 ORM 라이브러리로, 데이터베이스와의 상호작용을 쉽게 만들어 줍니다.
  • HikariCP : 고성능 JDBC 커넥션 풀을 제공하는 라이브러리입니다.
  • dotenv : .env 파일에서 환경 변수를 쉽게 로드할 수 있도록 도와줍니다.
  • JWT : JSON Web Token을 생성하고 검증할 수 있는 라이브러리입니다.

2-2. 애플리케이션 시작 및 모듈 설정

애플리케이션의 메인 함수와 모듈 설정은 Application.module()을 사용해 설정합니다. 여기서는 데이터베이스, 직렬화, HTTP 설정, 보안 및 라우팅을 설정합니다.

fun main(args: Array<String>) {
    EngineMain.main(args)
}

fun Application.module() {
    configureDatabase()     // 데이터베이스 설정
    configureSerialization()  // 직렬화 설정
    configureHTTP()         // HTTP 관련 설정
    configureSecurity()     // 보안 설정
    configureRouting()      // 라우팅 설정
}

이 함수는 각 기능별 설정 함수를 호출하여 애플리케이션 전반의 구성을 담당합니다.


2-3. 데이터베이스 설정

데이터베이스 연결은 HikariCPExposed ORM을 사용하여 설정합니다. 환경 변수는 dotenv 라이브러리를 통해 불러옵니다.

fun Application.configureDatabase() {
    val dotenv = dotenv()  // .env 파일 로드

    val dbUrl = dotenv["DB_URL"] ?: "jdbc:postgresql://localhost:5432/defaultdb"
    val dbUser = dotenv["DB_USER"] ?: "defaultuser"
    val dbPassword = dotenv["DB_PASSWORD"] ?: "defaultpassword"

    val hikariConfig = HikariConfig().apply {
        jdbcUrl = dbUrl
        driverClassName = "org.postgresql.Driver"
        username = dbUser
        password = dbPassword
        maximumPoolSize = 10
    }

    try {
        val dataSource = HikariDataSource(hikariConfig)
        Database.connect(dataSource)
        environment.log.info("Database connected successfully! : $dbUrl")
    } catch (e: Exception) {
        environment.log.error("Database connection failed: ${e.message}")
    }

    transaction {
        SchemaUtils.drop(Users)  // 데이터베이스 초기화 (개발 중에만 사용)
        SchemaUtils.create(Users)  // Users 테이블 생성
    }
}

이 함수는 PostgreSQL 데이터베이스에 연결한 후 Users 테이블을 생성합니다. 여기서는 Exposed ORM을 통해 스키마를 정의하고, 데이터베이스 초기화 및 테이블 생성을 처리합니다.

ENV 파일에는 이러한 형식으로 작성하면 됩니다.

DB_URL=jdbc:postgresql://localhost:5432/test
DB_USER=mic050r
DB_PASSWORD=123456

2-4. 테이블 정의

Users 테이블은 Exposed ORM을 사용해 정의하였습니다. 이 테이블은 사용자의 기본 정보(이름, 이메일, 비밀번호)를 저장합니다.

object Users : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val email = varchar("email", 50)
    val password = varchar("password", 64)  

    override val primaryKey = PrimaryKey(id)
}

Exposed ORM을 사용하면 데이터베이스 테이블과 필드를 쉽게 정의할 수 있습니다. 여기서는 id, name, email, password 필드를 정의했습니다.


2-5. JWT 인증 설정

JWT 인증은 io.ktor.auth.jwt 패키지를 사용하여 설정됩니다. configureSecurity() 함수에서 JWT 인증을 처리하는 로직을 작성합니다.

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.auth.jwt.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.http.*
import io.ktor.server.auth.*

fun Application.configureSecurity() {
    install(Authentication) {
        jwt("auth-jwt") {
            realm = "ktor-sample"
            verifier(
                JWT
                    .require(Algorithm.HMAC256("secret"))
                    .withAudience("ktor-audience")
                    .withIssuer("ktor-issuer")
                    .build()
            )
            validate { credential ->
                if (credential.payload.getClaim("name").asString().isNotEmpty()) {
                    JWTPrincipal(credential.payload)
                } else null
            }
            challenge { _, _ ->
                call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
            }
        }
    }
}

이 설정은 JWT 토큰을 검증하고, 유효한 경우 인증된 요청으로 처리합니다. 인증된 사용자는 보호된 라우트에 접근할 수 있습니다.


2-6. 라우팅 설정

라우팅 설정에서는 사용자 인증이 필요한 userRoutes와 로그인, 회원가입을 처리하는 authRoutes를 정의합니다.

fun Application.configureRouting() {
    routing {
        authenticate("auth-jwt") {
            userRoutes()  // 유저 관련 라우팅 추가
        }
        authRoutes() // 로그인, 회원가입 라우팅 추가
    }
}

이 코드에서는 authenticate("auth-jwt") 블록 안에 JWT 인증이 필요한 라우트를 정의했습니다. 인증이 필요한 경로에서는 사용자 정보 수정과 삭제를 처리하는 라우팅을 추가했습니다.

2-7. 사용자 관련 라우트

authRoutes() 함수에서는 회원가입/로그인 API를 정의합니다.

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import example.com.dto.Credentials
import example.com.dto.UserResponse
import example.com.models.User
import example.com.models.Users
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

fun Route.authRoutes() {
    route("/users") {
        // POST - 유저 생성
        post {
            val user = call.receive<User>()
            transaction {
                Users.insert {
                    it[name] = user.name
                    it[email] = user.email
                    it[password] = user.password  // 비밀번호 저장
                }
            }
            call.respond(HttpStatusCode.Created, "User added successfully")
        }

        // POST - 로그인
        post("/login") {
            val credentials = call.receive<Credentials>()

            // 데이터베이스에서 유저 조회
            val user = transaction {
                Users.select { Users.name eq credentials.name }
                    .map { User(it[Users.id], it[Users.name], it[Users.email], it[Users.password]) }
                    .singleOrNull()
            }

            if (user != null && user.password == credentials.password) {  // 비밀번호 검증
                val token = JWT.create()
                    .withAudience("ktor-audience")
                    .withIssuer("ktor-issuer")
                    .withClaim("name", credentials.name)
                    .sign(Algorithm.HMAC256("secret"))

                call.respond(hashMapOf("token" to token))
            } else {
                call.respond(HttpStatusCode.Unauthorized, "Invalid credentials")
            }
        }
        // GET - 모든 유저 조회
        get {
            val users = transaction {
                Users.selectAll().map {
                    UserResponse(it[Users.id], it[Users.name], it[Users.email])  // 비밀번호를 제외하고 응답
                }
            }
            call.respond(users)
        }

        // GET - 특정 유저 조회
        get("/{id}") {
            val id = call.parameters["id"]?.toIntOrNull()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
                return@get
            }

            val user = transaction {
                Users.select { Users.id eq id }
                    .map { UserResponse(it[Users.id], it[Users.name], it[Users.email]) }
                    .singleOrNull()
            }

            if (user == null) {
                call.respond(HttpStatusCode.NotFound, "User not found")
            } else {
                call.respond(user)
            }
        }
    }

    // GET - 모든 유저 조회
    get {
        val users = transaction {
            Users.selectAll().map {
                UserResponse(it[Users.id], it[Users.name], it[Users.email])  // 비밀번호를 제외하고 응답
            }
        }
        call.respond(users)
    }

    // GET - 특정 유저 조회
    get("/{id}") {
        val id = call.parameters["id"]?.toIntOrNull()
        if (id == null) {
            call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
            return@get
        }

        val user = transaction {
            Users.select { Users.id eq id }
                .map { UserResponse(it[Users.id], it[Users.name], it[Users.email]) }
                .singleOrNull()
        }

        if (user == null) {
            call.respond(HttpStatusCode.NotFound, "User not found")
        } else {
            call.respond(user)
        }
    }

}

userRoutes() 함수에서는 사용자의 정보를 수정하거나 삭제하는 API를 정의합니다.

package example.com.routes

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import example.com.dto.Credentials
import example.com.dto.UserResponse
import example.com.models.User
import example.com.models.Users
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction

fun Route.userRoutes() {

    route("/users") {

        // PUT - 유저 정보 수정
        put("/{id}") {
            val id = call.parameters["id"]?.toIntOrNull()
            val user = call.receive<User>()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
                return@put
            }

            val updated = transaction {
                Users.update({ Users.id eq id }) {
                    it[name] = user.name
                    it[email] = user.email
                    it[password] = user.password  // 비밀번호 업데이트
                }
            }

            if (updated == 0) {
                call.respond(HttpStatusCode.NotFound, "User not found")
            } else {
                call.respond(HttpStatusCode.OK, "User updated successfully")
            }
        }

        // DELETE - 유저 삭제
        delete("/{id}") {
            val id = call.parameters["id"]?.toIntOrNull()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
                return@delete
            }

            val deleted = transaction {
                Users.deleteWhere { Users.id eq id }
            }

            if (deleted == 0) {
                call.respond(HttpStatusCode.NotFound, "User not found")
            } else {
                call.respond(HttpStatusCode.OK, "User deleted successfully")
            }
        }
    }
}

이 라우트에서는 사용자 ID를 기준으로 정보를 수정하거나 삭제하는 API를 제공합니다.

3. Postman으로 테스트

3-1. 회원가입 (Sign Up)

  • URL : /users
  • Method : POST
  • 설명: 새로운 사용자를 등록합니다.
Request Body
{
  "name": "John Doe",
  "email": "john.doe@example.com",
  "password": "password123"
}
Response
  • Status: 201 Created
User added successfully

3-2. 로그인 (Login)

  • URL: /auth/login
  • Method: POST
  • 설명: 사용자 인증을 수행하고, JWT 토큰을 발급합니다.
Request Body
{
  "email": "john.doe@example.com",
  "password": "password123"
}
Response
  • Status: 200 OK
{
  "token": "eyJhbGciOiJIUzI1NiIsInR..."
}
Error Response
  • Status: 401 Unauthorized
  • 내용: 잘못된 이메일 또는 비밀번호

3. 사용자 정보 수정 (Update User)

  • URL: /users/{id}
  • Method: PUT
  • 설명: 사용자 정보를 수정합니다. JWT 토큰이 필요합니다.
Headers
  • Authorization: Bearer {JWT_TOKEN}
Request Body
  • Content-Type: application/json
Response
  • Status: 200 OK
  • 내용: User updated successfully
Error Response
  • Status: 404 Not Found
  • 내용: User not found

로그인 했을 때 발급 된 token을 header에 넣어줍니다!

  • Key : Authorization
  • Value : Bearer <token>

    get으로 불러왔더니 잘 바뀐것을 확인했습니다!

4. 디렉토리 구조

5. 전체 코드

build.gradle.kts


val kotlin_version: String by project
val logback_version: String by project
val exposed_version: String by project
val h2_version: String by project

plugins {
    kotlin("jvm") version "2.0.10"
    id("io.ktor.plugin") version "2.3.12"
    id("org.jetbrains.kotlin.plugin.serialization") version "2.0.10"
}

group = "example.com"
version = "0.0.1"

application {
    mainClass.set("io.ktor.server.netty.EngineMain")

    val isDevelopment: Boolean = project.ext.has("development")
    applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.exposed:exposed-core:0.41.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.41.1")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.41.1")

    implementation("io.ktor:ktor-server-core-jvm")
    implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
    implementation("io.ktor:ktor-server-content-negotiation-jvm")

    // Exposed ORM 라이브러리
    implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
    implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")

    // PostgreSQL JDBC 드라이버
    implementation("org.postgresql:postgresql:42.6.0")

    // HikariCP (커넥션 풀)
    implementation("com.zaxxer:HikariCP:5.0.1")

    // env
    implementation("io.github.cdimascio:dotenv-kotlin:6.4.0")

    implementation("io.ktor:ktor-server-auth:$logback_version") // Ktor 버전에 맞게 조정
    implementation("io.ktor:ktor-server-auth-jwt:$logback_version")
    implementation("com.auth0:java-jwt:4.2.1") // 최신 버전으로 조정

    implementation("com.h2database:h2:$h2_version")
    implementation("io.ktor:ktor-server-openapi")
    implementation("io.ktor:ktor-server-auth-jvm")
    implementation("io.ktor:ktor-server-netty-jvm")
    implementation("ch.qos.logback:logback-classic:$logback_version")
    implementation("io.ktor:ktor-server-config-yaml")
    testImplementation("io.ktor:ktor-server-test-host-jvm")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

src/main/kotlin/example/com/

Application.kt

package example.com

import example.com.plugins.*
import io.ktor.server.application.*
import io.ktor.server.netty.*
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import example.com.models.Users
import example.com.routes.authRoutes
import example.com.routes.userRoutes
import io.github.cdimascio.dotenv.dotenv
import io.ktor.server.auth.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction

fun main(args: Array<String>) {
    EngineMain.main(args)
}

fun Application.module() {
    // 설정 함수 호출
    configureDatabase()     // 데이터베이스 설정
    configureSerialization()  // 직렬화 설정
    configureHTTP()         // HTTP 관련 설정
    configureSecurity()     // 보안 설정
    configureRouting()      // 라우팅 설정
}

fun Application.configureDatabase() {
    val dotenv = dotenv()  // .env 파일 로드

    val dbUrl = dotenv["DB_URL"] ?: "jdbc:postgresql://localhost:5432/defaultdb"
    val dbUser = dotenv["DB_USER"] ?: "defaultuser"
    val dbPassword = dotenv["DB_PASSWORD"] ?: "defaultpassword"

    val hikariConfig = HikariConfig().apply {
        jdbcUrl = dbUrl
        driverClassName = "org.postgresql.Driver"
        username = dbUser
        password = dbPassword
        maximumPoolSize = 10
    }

    try {
        val dataSource = HikariDataSource(hikariConfig)
        Database.connect(dataSource)
        // 연결 성공 시 로그 출력
        environment.log.info("Database connected successfully! : $dbUrl")
    } catch (e: Exception) {
        // 연결 실패 시 오류 로그 출력
        environment.log.error("Database connection failed: ${e.message}")
    }

    // 데이터베이스 테이블 생성
    transaction {
        SchemaUtils.drop(Users)
        SchemaUtils.create(Users)  // Users 테이블 생성
    }
}

fun Application.configureRouting() {
    routing {
        authenticate("auth-jwt") { // jwt 인증이 필요한 라우팅
            userRoutes()  // 유저 관련 라우팅 추가
        }
        authRoutes() // 로그인, 회원가입 라우팅 추가
    }
}

dto/Credentials.kt

package example.com.dto

import kotlinx.serialization.Serializable

@Serializable
data class Credentials(val name: String, val password: String)

dto/UserResponse.kt


package example.com.dto

import kotlinx.serialization.Serializable

@Serializable
data class UserResponse(val id: Int, val name: String, val email: String)

models/Users.kt

package example.com.models

import org.jetbrains.exposed.sql.Table
import kotlinx.serialization.Serializable

// Exposed ORM 테이블 정의
object Users : Table() {
    val id = integer("id").autoIncrement()
    val name = varchar("name", 50)
    val email = varchar("email", 50)
    val password = varchar("password", 64)  // 비밀번호 필드 추가

    override val primaryKey = PrimaryKey(id)
}

// 데이터 클래스 정의 (직렬화를 위해 Serializable 사용)
@Serializable
data class User(val id: Int? = null, val name: String, val email: String, val password: String)

plugins/Security.kt

package example.com.plugins

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import io.ktor.server.auth.jwt.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.http.*
import io.ktor.server.auth.*

fun Application.configureSecurity() {
    install(Authentication) {
        jwt("auth-jwt") {
            realm = "ktor-sample"
            verifier(
                JWT
                    .require(Algorithm.HMAC256("secret"))
                    .withAudience("ktor-audience")
                    .withIssuer("ktor-issuer")
                    .build()
            )
            validate { credential ->
                if (credential.payload.getClaim("name").asString().isNotEmpty()) {
                    JWTPrincipal(credential.payload)
                } else null
            }
            challenge { _, _ ->
                call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
            }
        }
    }
}

routes/AuthRoutes.kt

package example.com.routes

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import example.com.dto.Credentials
import example.com.dto.UserResponse
import example.com.models.User
import example.com.models.Users
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

fun Route.authRoutes() {
    route("/users") {
        // POST - 유저 생성
        post {
            val user = call.receive<User>()
            transaction {
                Users.insert {
                    it[name] = user.name
                    it[email] = user.email
                    it[password] = user.password  // 비밀번호 저장
                }
            }
            call.respond(HttpStatusCode.Created, "User added successfully")
        }

        // POST - 로그인
        post("/login") {
            val credentials = call.receive<Credentials>()

            // 데이터베이스에서 유저 조회
            val user = transaction {
                Users.select { Users.name eq credentials.name }
                    .map { User(it[Users.id], it[Users.name], it[Users.email], it[Users.password]) }
                    .singleOrNull()
            }

            if (user != null && user.password == credentials.password) {  // 비밀번호 검증
                val token = JWT.create()
                    .withAudience("ktor-audience")
                    .withIssuer("ktor-issuer")
                    .withClaim("name", credentials.name)
                    .sign(Algorithm.HMAC256("secret"))

                call.respond(hashMapOf("token" to token))
            } else {
                call.respond(HttpStatusCode.Unauthorized, "Invalid credentials")
            }
        }
        // GET - 모든 유저 조회
        get {
            val users = transaction {
                Users.selectAll().map {
                    UserResponse(it[Users.id], it[Users.name], it[Users.email])  // 비밀번호를 제외하고 응답
                }
            }
            call.respond(users)
        }

        // GET - 특정 유저 조회
        get("/{id}") {
            val id = call.parameters["id"]?.toIntOrNull()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
                return@get
            }

            val user = transaction {
                Users.select { Users.id eq id }
                    .map { UserResponse(it[Users.id], it[Users.name], it[Users.email]) }
                    .singleOrNull()
            }

            if (user == null) {
                call.respond(HttpStatusCode.NotFound, "User not found")
            } else {
                call.respond(user)
            }
        }
    }

    // GET - 모든 유저 조회
    get {
        val users = transaction {
            Users.selectAll().map {
                UserResponse(it[Users.id], it[Users.name], it[Users.email])  // 비밀번호를 제외하고 응답
            }
        }
        call.respond(users)
    }

    // GET - 특정 유저 조회
    get("/{id}") {
        val id = call.parameters["id"]?.toIntOrNull()
        if (id == null) {
            call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
            return@get
        }

        val user = transaction {
            Users.select { Users.id eq id }
                .map { UserResponse(it[Users.id], it[Users.name], it[Users.email]) }
                .singleOrNull()
        }

        if (user == null) {
            call.respond(HttpStatusCode.NotFound, "User not found")
        } else {
            call.respond(user)
        }
    }

}

routes/UserRoutes.kt

package example.com.routes

import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import example.com.dto.Credentials
import example.com.dto.UserResponse
import example.com.models.User
import example.com.models.Users
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction

fun Route.userRoutes() {

    route("/users") {

        // PUT - 유저 정보 수정
        put("/{id}") {
            val id = call.parameters["id"]?.toIntOrNull()
            val user = call.receive<User>()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
                return@put
            }

            val updated = transaction {
                Users.update({ Users.id eq id }) {
                    it[name] = user.name
                    it[email] = user.email
                    it[password] = user.password  // 비밀번호 업데이트
                }
            }

            if (updated == 0) {
                call.respond(HttpStatusCode.NotFound, "User not found")
            } else {
                call.respond(HttpStatusCode.OK, "User updated successfully")
            }
        }

        // DELETE - 유저 삭제
        delete("/{id}") {
            val id = call.parameters["id"]?.toIntOrNull()
            if (id == null) {
                call.respond(HttpStatusCode.BadRequest, "Invalid user ID")
                return@delete
            }

            val deleted = transaction {
                Users.deleteWhere { Users.id eq id }
            }

            if (deleted == 0) {
                call.respond(HttpStatusCode.NotFound, "User not found")
            } else {
                call.respond(HttpStatusCode.OK, "User deleted successfully")
            }
        }
    }
}

env

DB_URL=jdbc:postgresql://localhost:5432/test
DB_USER=mic050r
DB_PASSWORD=123456

6. 정리

  1. 회원가입 : 클라이언트는 /auth/signup 엔드포인트로 POST 요청을 보내고, 새로운 사용자를 등록하기
  2. 로그인 : 클라이언트는 /auth/login 엔드포인트로 POST 요청을 보내 JWT 토큰을 발급받기
  3. 사용자 정보 수정 : 인증된 사용자는 JWT 토큰을 헤더에 포함해 /users/{id}PUT 요청을 보내 사용자 정보를 수정하기
  4. 사용자 삭제 : 인증된 사용자는 JWT 토큰을 헤더에 포함해 /users/{id}DELETE 요청을 보내 사용자 계정을 삭제할 수 있다.

7. 참고 자료

추가 학습 자료

profile
프로젝트를 통해 배운 개념이나 겪은 문제점들을 정리하고, 회고록을 작성하며 성장해나가는 곳입니다 😊

0개의 댓글

관련 채용 정보