Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BG-137]: jwt를 통한 인증 및 인가 로직 개발 (5h / 3h) #7

Closed
wants to merge 11 commits into from
2 changes: 2 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ repositories {
dependencies {
implementation(project(":domain"))
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("com.auth0:java-jwt:3.18.3")

testImplementation(kotlin("test"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.backgu.amaker.auth.controller

import com.backgu.amaker.auth.config.AuthConfig
import com.backgu.amaker.auth.dto.JwtTokenResponse
import com.backgu.amaker.auth.service.AuthFacade
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/auth")
class AuthApiController(
val authConfig: AuthConfig,
val authFacade: AuthFacade,
) {
@GetMapping("/oauth/google")
fun googleAuth(): String = authConfig.oauthUrl()

@GetMapping("/code/google")
fun login(
@RequestParam(name = "code") authorizationCode: String,
@RequestParam(name = "scope", required = false) scope: String,
@RequestParam(name = "authuser", required = false) authUser: String,
@RequestParam(name = "prompt", required = false) prompt: String,
): ResponseEntity<JwtTokenResponse> {
val token: JwtTokenResponse = authFacade.googleLogin(authorizationCode)
return ResponseEntity.ok(token)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import jakarta.servlet.http.HttpServletResponse
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam

@Controller
@RequestMapping("/auth")
Expand All @@ -18,15 +17,4 @@ class AuthController(
fun googleAuth(response: HttpServletResponse) {
response.sendRedirect(authConfig.oauthUrl())
}

@GetMapping("/code/google")
fun login(
@RequestParam(name = "code") authorizationCode: String,
@RequestParam(name = "scope", required = false) scope: String,
@RequestParam(name = "authuser", required = false) authUser: String,
@RequestParam(name = "prompt", required = false) prompt: String,
): String {
authService.googleLogin(authorizationCode)
return "redirect:/"
}
}
12 changes: 12 additions & 0 deletions api/src/main/kotlin/com/backgu/amaker/auth/dto/JwtTokenResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.backgu.amaker.auth.dto

import com.backgu.amaker.user.dto.UserDto

class JwtTokenResponse(
val token: String,
user: UserDto,
) {
val name: String = user.name
val email: String = user.email
val picture: String = user.picture
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.backgu.amaker.auth.dto
package com.backgu.amaker.auth.dto.oauth.google

import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.backgu.amaker.auth.dto
package com.backgu.amaker.auth.dto.oauth.google

import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.backgu.amaker.auth.infra

import com.backgu.amaker.auth.dto.GoogleUserInfoDto
import com.backgu.amaker.auth.dto.oauth.google.GoogleUserInfoDto
import com.backgu.amaker.config.CaughtHttpExchange
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.service.annotation.GetExchange
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.backgu.amaker.auth.infra

import com.backgu.amaker.auth.dto.GoogleOAuth2AccessTokenDto
import com.backgu.amaker.auth.dto.oauth.google.GoogleOAuth2AccessTokenDto
import com.backgu.amaker.config.CaughtHttpExchange
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.RequestParam
Expand Down
28 changes: 28 additions & 0 deletions api/src/main/kotlin/com/backgu/amaker/auth/service/AuthFacade.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.backgu.amaker.auth.service

import com.backgu.amaker.auth.dto.JwtTokenResponse
import com.backgu.amaker.auth.dto.oauth.google.GoogleUserInfoDto
import com.backgu.amaker.security.jwt.service.JwtService
import com.backgu.amaker.user.dto.UserCreateDto
import com.backgu.amaker.user.dto.UserDto
import com.backgu.amaker.user.service.UserService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional(readOnly = true)
class AuthFacade(
val authService: AuthService,
val jwtService: JwtService,
val userService: UserService,
) {
@Transactional
fun googleLogin(authorizationCode: String): JwtTokenResponse {
val userInfo: GoogleUserInfoDto = authService.googleLogin(authorizationCode)
val savedUser: UserDto =
userService.saveOrGetUser(UserCreateDto(userInfo.name, userInfo.email, userInfo.picture))
val token: String = jwtService.create(savedUser.id, savedUser.userRole.key)

return JwtTokenResponse(token, savedUser)
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package com.backgu.amaker.auth.service

import com.backgu.amaker.auth.config.AuthConfig
import com.backgu.amaker.auth.dto.GoogleOAuth2AccessTokenDto
import com.backgu.amaker.auth.dto.GoogleUserInfoDto
import com.backgu.amaker.auth.dto.oauth.google.GoogleOAuth2AccessTokenDto
import com.backgu.amaker.auth.dto.oauth.google.GoogleUserInfoDto
import com.backgu.amaker.auth.infra.GoogleApiClient
import com.backgu.amaker.auth.infra.GoogleOAuthClient
import org.springframework.stereotype.Service
import java.lang.IllegalArgumentException

@Service
class AuthService(
val googleOAuthClient: GoogleOAuthClient,
val googleApiClient: GoogleApiClient,
val authConfig: AuthConfig,
private val googleOAuthClient: GoogleOAuthClient,
private val googleApiClient: GoogleApiClient,
private val authConfig: AuthConfig,
) {
fun googleLogin(authorizationCode: String): String? {
fun googleLogin(authorizationCode: String): GoogleUserInfoDto {
val accessTokenDto: GoogleOAuth2AccessTokenDto =
googleOAuthClient.getGoogleOAuth2(
authorizationCode,
Expand All @@ -28,6 +28,6 @@ class AuthService(
googleApiClient.getUserInfo(accessTokenDto.getBearerToken())
?: throw IllegalArgumentException("Failed to get user information")

return userInfo.email
return userInfo
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.backgu.amaker.security

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component

@Component
class JwtAccessDeniedHandler : AccessDeniedHandler {
override fun handle(
request: HttpServletRequest?,
response: HttpServletResponse,
accessDeniedException: AccessDeniedException?,
) {
// TODO 후에 에러 폼이 수정되면 다시 작성
response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException?.message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.backgu.amaker.security

import java.util.UUID

class JwtAuthentication(
val id: UUID,
val token: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.backgu.amaker.security

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

@Component
class JwtAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest?,
response: HttpServletResponse?,
authException: AuthenticationException?,
) {
// TODO 후에 에러 폼이 수정되면 다시 작성
response?.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException?.message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.backgu.amaker.security

import com.backgu.amaker.user.domain.UserRole
import com.backgu.amaker.user.dto.UserDto
import com.backgu.amaker.user.service.UserService
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.AuthorityUtils.createAuthorityList
import org.springframework.util.ClassUtils

class JwtAuthenticationProvider(
private val userService: UserService,
) : AuthenticationProvider {
override fun supports(authentication: Class<*>?): Boolean =
ClassUtils.isAssignable(JwtAuthenticationToken::class.java, authentication!!)

override fun authenticate(authentication: Authentication): Authentication =
processOAuthAuthentication(
authentication.principal.toString(),
)

private fun processOAuthAuthentication(email: String): Authentication {
val user: UserDto = userService.getByEmail(email)
val authenticated: JwtAuthenticationToken =
JwtAuthenticationToken(
JwtAuthentication(user.id, user.name),
createAuthorityList(UserRole.USER.key),
)
authenticated.details = user
return authenticated
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.backgu.amaker.security

import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.GrantedAuthority

class JwtAuthenticationToken : AbstractAuthenticationToken {
private val principal: JwtAuthentication

constructor(principal: JwtAuthentication) : super(null) {
super.setAuthenticated(false)
this.principal = principal
}

internal constructor(principal: JwtAuthentication, authorities: Collection<GrantedAuthority?>?) : super(
authorities,
) {
super.setAuthenticated(true)
this.principal = principal
}

override fun getPrincipal(): JwtAuthentication = principal

override fun getCredentials(): String = ""

override fun setAuthenticated(isAuthenticated: Boolean) {
super.setAuthenticated(false)
}

override fun eraseCredentials() {
super.eraseCredentials()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.backgu.amaker.security

import com.backgu.amaker.security.jwt.service.JwtService
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.web.filter.OncePerRequestFilter
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.util.Arrays
import java.util.Objects
import java.util.UUID
import java.util.regex.Pattern
import java.util.stream.Collectors

class JwtAuthenticationTokenFilter(
private val jwtService: JwtService,
) : OncePerRequestFilter() {
private val bearerRegex: Pattern = Pattern.compile("^Bearer$", Pattern.CASE_INSENSITIVE)
private val headerKey: String = "Authorization"

override fun doFilterInternal(
req: HttpServletRequest,
res: HttpServletResponse,
chain: FilterChain,
) {
if (SecurityContextHolder.getContext().authentication == null) {
val authorizationToken: String? = obtainAuthorizationToken(req)
try {
if (authorizationToken != null) {
println(authorizationToken)
val claims: JwtService.Claims = jwtService.verify(authorizationToken)

val id: UUID = UUID.fromString(claims.id.replace("\"", ""))
val authorities: List<GrantedAuthority> = obtainAuthorities(claims)

if (Objects.nonNull(id) && authorities.isNotEmpty()) {
val authentication: JwtAuthenticationToken =
JwtAuthenticationToken(
JwtAuthentication(id, authorizationToken),
authorities,
)
authentication.details = WebAuthenticationDetailsSource().buildDetails(req)
SecurityContextHolder.getContext().authentication = authentication
}
}
} catch (e: Exception) {
// TODO exception handling
throw IllegalArgumentException("Invalid token")
}
}
chain.doFilter(req, res)
}

private fun obtainAuthorizationToken(request: HttpServletRequest): String? {
var token: String? = request.getHeader(headerKey)
if (token != null) {
try {
token = URLDecoder.decode(token, "UTF-8")
val parts: Array<String> = token.split(" ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (parts.size == 2) {
val scheme = parts[0]
val credentials = parts[1]
return if (bearerRegex.matcher(scheme).matches()) credentials else null
}
} catch (_: UnsupportedEncodingException) {
// TODO exception handling
}
}

return null
}

private fun obtainAuthorities(claims: JwtService.Claims): List<GrantedAuthority> {
val roles: Array<String> = claims.roles
return if (roles.isEmpty()) {
emptyList()
} else {
Arrays
.stream(roles)
.map { role: String? ->
SimpleGrantedAuthority(
role,
)
}.collect(Collectors.toList())
}
}
}
Loading
Loading