Skip to content

Commit

Permalink
[feat #10] 구글 로그인 API (#12)
Browse files Browse the repository at this point in the history
* chore : gitignore yml, 파이어베이스 비공개json 추가

* rename : 예외 클래스들 모듈 위치 변경(domain으로)

* feat : 구글 통신 서비스 구현

* feat : 로그인 서비스 구현(Google만)

* feat : User 도메인 및 리포지터리 구현

* feat : 로그인 API 구현

* feat : AuthService 테스트

* feat : Auth 테스트 픽쳐

* feat : Firebase 연동 설정

* refactor : 코드 포맷 및 import 클래스 위치변경에 따른 수정

* feat : 로그인 API 필터 패스

* docs : out-web 모듈 구성 및 main 클래스 yml 파일 인식

* refactor : print문 제거

* rename : 클래스명 및 메소드명 리네이밍

* feat : FirebaseAuth Bean 등록
  • Loading branch information
dlswns2480 authored Jul 10, 2024
1 parent 7f613cb commit ef86d36
Show file tree
Hide file tree
Showing 35 changed files with 406 additions and 32 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ out/
.kotlin

### Mac OS ###
.DS_Store
.DS_Store

### Resources ###
**/src/main/resources/application-*.yml
**/src/main/resources/google-services.json
20 changes: 20 additions & 0 deletions adapters/in-web/src/main/kotlin/com/pokit/auth/AuthController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.pokit.auth

import com.pokit.auth.port.`in`.AuthUseCase
import com.pokit.token.dto.request.SignInRequest
import org.springframework.http.ResponseEntity
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

@RestController
@RequestMapping("/api/v1/auth")
class AuthController(
private val authUseCase: AuthUseCase,
) {
@PostMapping("/signin")
fun signIn(
@RequestBody request: SignInRequest,
) = ResponseEntity.ok(authUseCase.signIn(request))
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.pokit.auth.filter

import com.pokit.auth.exception.AuthErrorCode
import com.pokit.auth.port.`in`.TokenProvider
import com.pokit.common.exception.ClientValidationException
import com.pokit.token.exception.AuthErrorCode
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
Expand All @@ -19,6 +19,16 @@ import org.springframework.web.filter.OncePerRequestFilter
class CustomAuthenticationFilter(
private val tokenProvider: TokenProvider,
) : OncePerRequestFilter() {
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
val excludePath = arrayOf("/api/v1/auth/signin")
val path = request.requestURI
val shouldNotFilter =
excludePath
.any { it.equals(path) }

return shouldNotFilter
}

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
Expand All @@ -38,7 +48,7 @@ class CustomAuthenticationFilter(

private fun getAuthentication(request: HttpServletRequest): Authentication? {
val header = request.getHeader(HttpHeaders.AUTHORIZATION)
if(!StringUtils.hasText(header)) {
if (!StringUtils.hasText(header)) {
throw ClientValidationException(AuthErrorCode.TOKEN_REQUIRED)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package com.pokit.out.persistence.auth.impl

import com.pokit.auth.port.out.RefreshTokenRepository
import com.pokit.auth.port.out.RefreshTokenPort
import com.pokit.out.persistence.auth.persist.RefreshTokenJpaEntity
import com.pokit.out.persistence.auth.persist.RefreshTokenJpaRepository
import com.pokit.out.persistence.auth.persist.RefreshTokenRepository
import com.pokit.out.persistence.auth.persist.toDomain
import com.pokit.token.model.RefreshToken
import org.springframework.stereotype.Repository

@Repository
class RefreshTokenAdapter(
private val refreshTokenJpaRepository: RefreshTokenJpaRepository,
) : RefreshTokenRepository {
override fun save(refreshToken: RefreshToken): RefreshToken {
private val refreshTokenRepository: RefreshTokenRepository,
) : RefreshTokenPort {
override fun persist(refreshToken: RefreshToken): RefreshToken {
val refreshTokenEntity = RefreshTokenJpaEntity.of(refreshToken)
val savedToken = refreshTokenJpaRepository.save(refreshTokenEntity)
val savedToken = refreshTokenRepository.save(refreshTokenEntity)
return savedToken.toDomain()
}

override fun findByUserId(userId: Long): RefreshToken? {
val refreshToken = refreshTokenJpaRepository.findByUserId(userId)
override fun loadByUserId(userId: Long): RefreshToken? {
val refreshToken = refreshTokenRepository.findByUserId(userId)
return refreshToken?.toDomain()
}

override fun deleteById(id: Long) {
refreshTokenJpaRepository.deleteById(id)
refreshTokenRepository.deleteById(id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package com.pokit.out.persistence.auth.persist

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

interface RefreshTokenJpaRepository : JpaRepository<RefreshTokenJpaEntity, Long> {
interface RefreshTokenRepository : JpaRepository<RefreshTokenJpaEntity, Long> {
fun findByUserId(userId: Long): RefreshTokenJpaEntity?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.pokit.out.persistence.user.impl

import com.pokit.out.persistence.user.persist.UserJpaEntity
import com.pokit.out.persistence.user.persist.UserRepository
import com.pokit.out.persistence.user.persist.toDomain
import com.pokit.user.model.User
import com.pokit.user.port.out.UserPort
import org.springframework.stereotype.Repository

@Repository
class UserAdapter(
private val userRepository: UserRepository,
) : UserPort {
override fun persist(user: User): User {
val userJpaEntity = UserJpaEntity.of(user)
val savedUser = userRepository.save(userJpaEntity)
return savedUser.toDomain()
}

override fun loadByEmail(email: String): User? {
val userJpaEntity = userRepository.findByEmail(email)
return userJpaEntity?.toDomain()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.pokit.out.persistence.user.persist

import com.pokit.user.model.Role
import com.pokit.user.model.User
import jakarta.persistence.*

@Table(name = "USER")
@Entity
class UserJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
val id: Long = 0L,
@Column(name = "email")
val email: String,
@Column(name = "role")
val role: Role,
) {
companion object {
fun of(user: User) =
UserJpaEntity(
email = user.email,
role = user.role,
)
}
}

fun UserJpaEntity.toDomain() = User(id = this.id, email = this.email, role = this.role)

fun UserJpaEntity.toPrincipalUser() = null // Security Context에 담을 user
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pokit.out.persistence.user.persist

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

interface UserRepository : JpaRepository<UserJpaEntity, Long> {
fun findByEmail(email: String): UserJpaEntity?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.pokit.auth

import com.pokit.token.dto.request.SignInRequest
import com.pokit.token.model.AuthPlatform
import com.pokit.token.model.Token

class AuthFixture {
companion object {
fun getToken() = Token(accessToken = "at", refreshToken = "rf")

fun getGoogleSigniInRequest() = SignInRequest(AuthPlatform.GOOGLE.platform, "code")

fun getInvalidSignInRequest() = SignInRequest("구긍", "code")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pokit.user

import com.pokit.user.dto.UserInfo
import com.pokit.user.model.Role
import com.pokit.user.model.User

class UserFixture {
companion object {
fun getUser() = User(1L, "[email protected]", Role.USER)

fun getUserInfo() = UserInfo("[email protected]")
}
}
20 changes: 20 additions & 0 deletions adapters/out-web/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
kotlin("plugin.spring") version "1.9.24"
}

dependencies {
implementation(project(":application"))
implementation(project(":domain"))

implementation("org.springframework.boot:spring-boot-starter")
implementation("com.google.firebase:firebase-admin:8.1.0")
}

tasks {
withType<Jar> { enabled = true }
withType<BootJar> { enabled = false }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.pokit.auth.common.config

import com.google.auth.oauth2.GoogleCredentials
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.auth.FirebaseAuth
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource

@Configuration
class FirebaseConfig {
@Bean
fun firebaseApp(): FirebaseApp {
val resource = ClassPathResource("google-services.json")
val serviceAccount = resource.inputStream

val options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build()

return FirebaseApp.initializeApp(options)
}

@Bean
fun firebaseAuth() = FirebaseAuth.getInstance(firebaseApp())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.pokit.auth.impl

import com.google.firebase.auth.FirebaseAuth
import com.pokit.auth.port.out.GoogleApiClient
import com.pokit.user.dto.UserInfo
import org.springframework.stereotype.Component

@Component
class GoogleApiAdapter(
private val firebaseAuth: FirebaseAuth
) : GoogleApiClient{
override fun getUserInfo(authorizationCode: String): UserInfo {
val decodedToken = verifyIdToken(authorizationCode)
return UserInfo(decodedToken.email) // 로그인 한 사용자의 이메일
}

private fun verifyIdToken(idToken: String) = firebaseAuth.verifyIdToken(idToken)
}
1 change: 1 addition & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {

// 테스팅
testImplementation("io.mockk:mockk:1.13.7")
testImplementation(testFixtures(project(":adapters:out-persistence")))
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pokit.auth.port.`in`

import com.pokit.token.dto.request.SignInRequest
import com.pokit.token.model.Token

interface AuthUseCase {
fun signIn(request: SignInRequest): Token
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pokit.auth.port.out

import com.pokit.user.dto.UserInfo

interface GoogleApiClient {
fun getUserInfo(authorizationCode: String): UserInfo
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.pokit.auth.port.out

import com.pokit.token.model.RefreshToken

interface RefreshTokenPort {
fun persist(refreshToken: RefreshToken): RefreshToken

fun loadByUserId(userId: Long): RefreshToken?

fun deleteById(id: Long)
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.pokit.auth.port.service

import com.pokit.auth.port.`in`.AuthUseCase
import com.pokit.auth.port.`in`.TokenProvider
import com.pokit.auth.port.out.GoogleApiClient
import com.pokit.token.dto.request.SignInRequest
import com.pokit.token.model.AuthPlatform
import com.pokit.token.model.Token
import com.pokit.user.dto.UserInfo
import com.pokit.user.model.Role
import com.pokit.user.model.User
import com.pokit.user.port.out.UserPort
import org.springframework.stereotype.Service

@Service
class AuthService(
private val googleApiClient: GoogleApiClient,
private val tokenProvider: TokenProvider,
private val userPort: UserPort,
) : AuthUseCase {
override fun signIn(request: SignInRequest): Token {
val platformType = AuthPlatform.of(request.authPlatform)

val userInfo =
when (platformType) {
AuthPlatform.GOOGLE -> googleApiClient.getUserInfo(request.authorizationCode)
AuthPlatform.APPLE -> UserInfo("[email protected]") // TODO
}

val userEmail = userInfo.email
val user = userPort.loadByEmail(userEmail) ?: createUser(userEmail) // 없으면 저장

val token = tokenProvider.createToken(user.id)

return token
}

private fun createUser(email: String): User {
val user = User(email = email, role = Role.USER)
return userPort.persist(user)
}
}
Loading

0 comments on commit ef86d36

Please sign in to comment.