Skip to content

Commit

Permalink
Merge pull request #3 from mash-up-kr/feature/login
Browse files Browse the repository at this point in the history
feat: 카카오 로그인 구현
  • Loading branch information
210-reverof authored Jun 15, 2024
2 parents 1633496 + 4e4ae0e commit a4f1082
Show file tree
Hide file tree
Showing 33 changed files with 769 additions and 2 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id("io.spring.dependency-management") version "1.1.5"
kotlin("jvm") version "1.9.24"
kotlin("plugin.spring") version "1.9.24"
kotlin("plugin.jpa") version "1.9.24"
}

java.sourceCompatibility = JavaVersion.VERSION_21
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
jjwtVersion=0.11.5
mysqlConnectorVersion=8.0.33
15 changes: 14 additions & 1 deletion pic-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@ tasks.jar {
enabled = false
}

val jjwtVersion: String by project.extra

dependencies {
implementation(project(":pic-common"))
implementation(project(":pic-domain"))
implementation(project(":pic-external"))

// validation
implementation("org.springframework.boot:spring-boot-starter-validation")

// security
implementation("org.springframework.boot:spring-boot-starter-security")

// jwt
implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion")

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
developmentOnly("org.springframework.boot:spring-boot-devtools")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
}
4 changes: 3 additions & 1 deletion pic-api/src/main/kotlin/com/mashup/pic/PicApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.mashup.pic

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching

@EnableCaching
@SpringBootApplication
class PicApplication

fun main(args: Array<String>) {
runApplication<PicApplication>(*args)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.mashup.pic.auth.applicationService

import com.mashup.pic.auth.applicationService.dto.LoginServiceRequest
import com.mashup.pic.auth.controller.dto.LoginResponse
import com.mashup.pic.domain.user.User
import com.mashup.pic.security.jwt.JwtManager
import com.mashup.pic.domain.user.UserService
import com.mashup.pic.security.authentication.UserInfo
import com.mashup.pic.security.oidc.KakaoIdTokenValidator
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class AuthApplicationService(
private val userService: UserService,
private val jwtTokenUtil: JwtManager,
private val idTokenValidator: KakaoIdTokenValidator
) {

@Transactional
fun login(request: LoginServiceRequest): LoginResponse {
val oAuthId = idTokenValidator.validateAndGetId(request.idToken, request.nickname)
val user = userService.findUserByOAuthIdOrNull(oAuthId)?: createUser(oAuthId, request)

val authToken = jwtTokenUtil.generateAuthToken(user.toUserInfo())
return LoginResponse.from(user, authToken)
}

private fun createUser(oAuthId: Long, request: LoginServiceRequest) : User {
return userService.create(
oAuthId = oAuthId,
nickname = request.nickname,
profileImage = request.profileImage
)
}

fun User.toUserInfo(): UserInfo {
return UserInfo(
id = this.id,
nickname = this.nickname,
roles = this.roles
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mashup.pic.auth.applicationService.dto

data class LoginServiceRequest(
val idToken: String,
val provider: LoginProvider,
val nickname: String,
val profileImage: String
)

enum class LoginProvider {
KAKAO, NAVER, GOOGLE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.mashup.pic.auth.controller

import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import com.mashup.pic.auth.applicationService.AuthApplicationService
import com.mashup.pic.auth.controller.dto.LoginRequest
import com.mashup.pic.auth.controller.dto.LoginResponse
import com.mashup.pic.common.ApiResponse
import jakarta.validation.Valid

@RestController
@RequestMapping("/api/v1/auth")
class AuthController(
private val authApplicationService: AuthApplicationService,
) {

@PostMapping("/login")
fun login(
@Valid @RequestBody loginRequest: LoginRequest
): ApiResponse<LoginResponse> {
return ApiResponse.success(authApplicationService.login(loginRequest.toServiceRequest()))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.mashup.pic.auth.controller.dto

import com.mashup.pic.auth.applicationService.dto.LoginProvider
import com.mashup.pic.auth.applicationService.dto.LoginServiceRequest
import jakarta.validation.constraints.NotBlank

data class LoginRequest(
@NotBlank val idToken: String,
@NotBlank val provider: LoginProvider,
@NotBlank val nickname: String,
@NotBlank val profileImage: String
) {

fun toServiceRequest(): LoginServiceRequest {
return LoginServiceRequest(
idToken = idToken,
provider = provider,
nickname = nickname,
profileImage = profileImage
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mashup.pic.auth.controller.dto

import com.mashup.pic.domain.user.User
import com.mashup.pic.security.authentication.AuthToken


data class LoginResponse(
val userId: Long,
val nickname: String,
val accessToken: String,
val refreshToken: String
) {
companion object {
fun from(user: User, authToken: AuthToken): LoginResponse {
return LoginResponse(
userId = user.id,
nickname = user.nickname,
accessToken = authToken.accessToken,
refreshToken = authToken.refreshToken
)
}
}
}
53 changes: 53 additions & 0 deletions pic-api/src/main/kotlin/com/mashup/pic/config/SecurityConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.mashup.pic.config

import com.fasterxml.jackson.databind.ObjectMapper
import com.mashup.pic.security.handler.HttpStatusAccessDeniedHandler
import com.mashup.pic.security.handler.HttpStatusAuthenticationEntryPoint
import com.mashup.pic.security.jwt.JwtFilter
import com.mashup.pic.security.jwt.JwtManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

@Configuration
@EnableWebSecurity
class SecurityConfig(
private val jwtTokenUtil: JwtManager,
private val objectMapper: ObjectMapper,
) {

@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
return http
.cors { it.disable() }
.csrf { it.disable() }
.httpBasic { it.disable() }
.formLogin { it.disable() }
.authorizeHttpRequests { authorization ->
authorization
.requestMatchers(*WHITELIST_ENDPOINTS).permitAll()
.requestMatchers(ADMIN_ENDPOINT_PATTERN).hasRole(ADMIN_ROLE)
.anyRequest().hasRole(MEMBER_ROLE)
}
.addFilterBefore(JwtFilter(jwtTokenUtil, objectMapper), UsernamePasswordAuthenticationFilter::class.java)
.exceptionHandling {
it.authenticationEntryPoint(HttpStatusAuthenticationEntryPoint())
it.accessDeniedHandler(HttpStatusAccessDeniedHandler())
}
.build()
}

companion object {
private const val ADMIN_ENDPOINT_PATTERN = "/api/v1/admin/**"
private const val ADMIN_ROLE = "ADMIN"
private const val MEMBER_ROLE = "MEMBER"
private val WHITELIST_ENDPOINTS = arrayOf(
"/api/v1/auth/login",
"/api/v1/auth/token"
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mashup.pic.security.authentication

data class AuthToken(
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.mashup.pic.security.authentication

import com.mashup.pic.domain.user.UserRole
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority

class JwtAuthentication(private val userInfo: UserInfo) : Authentication {

private var authenticated: Boolean = false

override fun getName(): String {
return userInfo.nickname
}

override fun getAuthorities(): Collection<GrantedAuthority> {
return userInfo.roles.map(this::convertUserRoleToGrantedAuthority)
}

override fun getCredentials(): Any {
return userInfo
}

override fun getDetails(): Any {
return userInfo
}

override fun getPrincipal(): Any {
return userInfo
}

override fun isAuthenticated(): Boolean {
return authenticated
}

override fun setAuthenticated(isAuthenticated: Boolean) {
this.authenticated = isAuthenticated
}

private fun convertUserRoleToGrantedAuthority(userRole: UserRole): GrantedAuthority {
return GrantedAuthority { userRole.role }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mashup.pic.security.authentication

import com.mashup.pic.domain.user.User
import com.mashup.pic.domain.user.UserRole


data class UserInfo(
val id: Long,
val nickname: String,
val roles: Set<UserRole>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mashup.pic.security.handler

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.security.web.access.AccessDeniedHandler

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.security.access.AccessDeniedException

class HttpStatusAccessDeniedHandler : AccessDeniedHandler {
private val logger: Logger = LoggerFactory.getLogger(HttpStatusAccessDeniedHandler::class.java)

override fun handle(
request: HttpServletRequest,
response: HttpServletResponse,
accessDeniedException: AccessDeniedException
) {
logger.warn("Access denied: {}", accessDeniedException.message)
response.status = HttpStatus.FORBIDDEN.value()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mashup.pic.security.handler

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpStatus
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint

class HttpStatusAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
) {
response.status = HttpStatus.UNAUTHORIZED.value()
}
}
Loading

0 comments on commit a4f1082

Please sign in to comment.