Skip to content

Commit de2ee35

Browse files
committed
✨ feat(auth): OAuth2Client를 사용한 로그인 구현
1 parent e01729b commit de2ee35

File tree

19 files changed

+318
-20
lines changed

19 files changed

+318
-20
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.app.server.auth.application.service
2+
3+
import com.app.server.auth.exception.AuthException
4+
import com.app.server.common.constant.Constants
5+
import com.app.server.common.exception.BadRequestException
6+
import com.app.server.core.security.dto.UserInfoResponseDto
7+
import com.app.server.core.security.util.JwtUtil
8+
import com.app.server.user.application.service.UserService
9+
import jakarta.transaction.Transactional
10+
import org.springframework.stereotype.Service
11+
12+
@Service
13+
@Transactional
14+
class AuthService(
15+
private val jwtUtil: JwtUtil,
16+
private val userService: UserService
17+
) {
18+
fun refreshToken(token: String): UserInfoResponseDto {
19+
20+
val refreshToken = refineToken(token)
21+
if (jwtUtil.validateToken(refreshToken)) {
22+
throw BadRequestException(AuthException.INVALID_REFRESH_TOKEN)
23+
}
24+
val userId = jwtUtil.getUserIdFromToken(refreshToken)
25+
26+
val user = userService.findById(userId)
27+
28+
if (!user.refreshToken.equals(refreshToken)) {
29+
throw BadRequestException(AuthException.INVALID_REFRESH_TOKEN)
30+
}
31+
32+
val jwtTokenDto = jwtUtil.generateToken(user.id!!, user.email, "ROLE_USER")
33+
userService.updateRefreshToken(userId, jwtTokenDto.refreshToken)
34+
35+
return UserInfoResponseDto.fromUserEntity(
36+
user = user,
37+
jwtTokenDto = jwtTokenDto
38+
)
39+
}
40+
41+
private fun refineToken(accessToken: String): String {
42+
return if (accessToken.startsWith(Constants.BEARER_PREFIX)) {
43+
accessToken.substring(Constants.BEARER_PREFIX.length)
44+
} else {
45+
accessToken
46+
}
47+
}
48+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.app.server.auth.exception
2+
3+
import com.app.server.common.enums.ResultCode
4+
5+
enum class AuthException(
6+
override val code: String,
7+
override val message: String
8+
) : ResultCode{
9+
10+
INVALID_REFRESH_TOKEN("AUT000", "유효하지 않은 리프레시 토큰입니다."),
11+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.app.server.auth.ui.controller
2+
3+
import com.app.server.auth.application.service.AuthService
4+
import com.app.server.common.constant.Constants
5+
import com.app.server.common.response.ApiResponse
6+
import com.app.server.core.security.dto.UserInfoResponseDto
7+
import jakarta.validation.Valid
8+
import org.springframework.web.bind.annotation.GetMapping
9+
import org.springframework.web.bind.annotation.RequestHeader
10+
import org.springframework.web.bind.annotation.RequestMapping
11+
import org.springframework.web.bind.annotation.RestController
12+
13+
@RestController
14+
@RequestMapping("/api/v1/auth")
15+
class AuthController (
16+
private val authService: AuthService
17+
){
18+
19+
@GetMapping("/refresh")
20+
fun refreshToken(
21+
@Valid @RequestHeader(Constants.AUTHORIZATION_HEADER) refreshToken: String
22+
) : ApiResponse<UserInfoResponseDto>{
23+
return ApiResponse.success(authService.refreshToken(refreshToken))
24+
}
25+
}

server/src/main/kotlin/com/app/server/common/constant/Constants.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,6 @@ object Constants {
1010
const val USER_ID_CLAIM_NAME: String = "uid"
1111
const val USER_EMAIL_CLAIM_NAME: String = "email"
1212

13-
// 소셜 로그인 관련 상수
14-
const val APPLE_PUBLIC_KEYS_URL: String = "https://appleid.apple.com/auth/keys"
15-
const val KAKAO_RESOURCE_SERVER_URL: String = "https://kapi.kakao.com/v2/user/me"
16-
const val APPLE_TOKEN_URL: String = "https://appleid.apple.com/auth/token"
17-
const val APPLE_REVOKE_URL: String = "https://appleid.apple.com/auth/revoke"
18-
1913
/**
2014
* Urls which don't need authentication
2115
* but need to be filtered
@@ -30,7 +24,10 @@ object Constants {
3024
"/api/auth/login/kakao",
3125
"/api/auth/login/naver",
3226
"/api/auth/login/google",
33-
"/api/auth/login/apple"
27+
"/api/auth/login/apple",
28+
"/oauth2/authorization/google",
29+
"/login/oauth2/code/google",
30+
"/favicon.ico",
3431
)
3532

3633
/**
@@ -44,7 +41,6 @@ object Constants {
4441
val BYPASS_URLS: List<String> = listOf( //
4542
"/hello", // 모니터링
4643
"/actuator/**", // 피드백 데이터 조회
47-
"/api/v1/**", //TODO: Auth 구현 후 제거
4844
)
4945

5046
const val CONTENT_TYPE: String = "Content-Type"

server/src/main/kotlin/com/app/server/common/enums/CommonResultCode.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum class CommonResultCode(
1919

2020
// domain
2121
AUTH_EXCEPTION("AUT000", "Auth Exception"),
22+
OAUTH2_EXCEPTION("OAU000", "OAuth2 Exception"),
2223
USER_EXCEPTION("USR000", "User Exception"),
2324
NOTIFICATION_EXCEPTION("NOT000", "Notification Exception"),
2425
;

server/src/main/kotlin/com/app/server/core/security/config/SecurityConfig.kt

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import com.app.server.core.security.JwtAuthEntryPoint
55
import com.app.server.core.security.filter.CustomLogoutFilter
66
import com.app.server.core.security.filter.JwtAuthenticationFilter
77
import com.app.server.core.security.filter.JwtExceptionFilter
8+
import com.app.server.core.security.handler.CustomOAuth2FailureHandler
9+
import com.app.server.core.security.handler.CustomOAuth2SuccessHandler
10+
import com.app.server.core.security.service.CustomOAuth2UserService
811
import com.app.server.core.security.handler.CustomSignOutProcessHandler
912
import com.app.server.core.security.handler.CustomSignOutResultHandler
1013
import com.app.server.core.security.handler.JwtAccessDeniedHandler
1114
import com.app.server.core.security.provider.JwtAuthenticationProvider
1215
import com.app.server.core.security.service.CustomUserDetailsService
1316
import com.app.server.core.security.util.JwtUtil
17+
import org.springframework.aot.generate.ValueCodeGenerator.withDefaults
1418
import org.springframework.context.annotation.Bean
1519
import org.springframework.context.annotation.Configuration
20+
import org.springframework.security.config.Customizer
1621
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
1722
import org.springframework.security.config.annotation.web.builders.HttpSecurity
1823
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@@ -30,7 +35,10 @@ class SecurityConfig(
3035
private val customSignOutProcessHandler: CustomSignOutProcessHandler,
3136
private val customSignOutResultHandler: CustomSignOutResultHandler,
3237
private val jwtAuthEntryPoint: JwtAuthEntryPoint,
33-
private val jwtAccessDeniedHandler: JwtAccessDeniedHandler
38+
private val jwtAccessDeniedHandler: JwtAccessDeniedHandler,
39+
private val customOAuth2UserService: CustomOAuth2UserService,
40+
private val customOAuth2SuccessHandler: CustomOAuth2SuccessHandler,
41+
private val customOAuth2failureHandler: CustomOAuth2FailureHandler,
3442
) {
3543

3644
@Bean
@@ -47,6 +55,18 @@ class SecurityConfig(
4755
.anyRequest().authenticated()
4856
}
4957
.formLogin { it.disable() }
58+
.oauth2Client { Customizer.withDefaults<Any>() }
59+
.oauth2Login { oauth ->
60+
oauth.userInfoEndpoint { userInfo ->
61+
userInfo.userService(customOAuth2UserService)
62+
}
63+
.successHandler{ request, response, authentication ->
64+
customOAuth2SuccessHandler.onAuthenticationSuccess(request, response, authentication)
65+
}
66+
.failureHandler { request, response, exception ->
67+
customOAuth2failureHandler.onAuthenticationFailure(request, response, exception)
68+
}
69+
}
5070
.exceptionHandling { exceptions ->
5171
exceptions.authenticationEntryPoint(jwtAuthEntryPoint)
5272
.accessDeniedHandler(jwtAccessDeniedHandler)
@@ -57,12 +77,12 @@ class SecurityConfig(
5777
.logoutSuccessHandler(customSignOutResultHandler)
5878
.deleteCookies(Constants.AUTHORIZATION_HEADER, Constants.REAUTHORIZATION)
5979
}
60-
.addFilterBefore(CustomLogoutFilter(), LogoutFilter::class.java)
6180
.addFilterBefore(
6281
JwtAuthenticationFilter(jwtUtil, JwtAuthenticationProvider(customUserDetailsService)),
63-
CustomLogoutFilter::class.java
82+
LogoutFilter::class.java
6483
)
65-
.addFilterBefore(JwtExceptionFilter(), JwtAuthenticationFilter::class.java)
84+
.addFilterBefore(CustomLogoutFilter(), JwtAuthenticationFilter::class.java)
85+
.addFilterBefore(JwtExceptionFilter(), CustomLogoutFilter::class.java)
6686

6787
return http.build()
6888
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GET http://localhost:8080/api/v1/challenges
2+
Authorization : Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1aWQiOjMsImVtYWlsIjoib2hoczEwMTBAZ21haWwuY29tIiwicm9sZSI6IlVTRVIiLCJpYXQiOjE3NDY2MTMyMDYsImV4cCI6MTc0NjYxNjgwNn0.-wPd6ZdXDhRTNGx9UMu2SDyZfp6OTh759fdVK2x2vChn3NF7l1rWCmL9tVHGoQYP0cRCMeN2OvzCKSlnz-ZqEw
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.app.server.core.security.dto
2+
3+
import com.app.server.user.domain.model.User
4+
import com.app.server.user.ui.dto.response.JwtTokenDto
5+
6+
7+
data class UserInfoResponseDto(
8+
val nickname: String,
9+
val email: String,
10+
val profileImageUrl: String?,
11+
val jwtTokenDto: JwtTokenDto?
12+
) {
13+
14+
companion object {
15+
fun fromUserEntity(user: User, jwtTokenDto: JwtTokenDto?): UserInfoResponseDto {
16+
return UserInfoResponseDto(
17+
nickname = user.nickname,
18+
email = user.email,
19+
profileImageUrl = user.profileImageUrl,
20+
jwtTokenDto = jwtTokenDto
21+
)
22+
}
23+
}
24+
}

server/src/main/kotlin/com/app/server/core/security/filter/JwtAuthenticationFilter.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@ import com.app.server.core.security.provider.JwtAuthenticationProvider
77
import com.app.server.core.security.util.JwtUtil
88
import io.jsonwebtoken.JwtException
99
import jakarta.servlet.FilterChain
10-
import jakarta.servlet.ServletException
1110
import jakarta.servlet.http.HttpServletRequest
1211
import jakarta.servlet.http.HttpServletResponse
13-
import lombok.RequiredArgsConstructor
14-
import lombok.extern.slf4j.Slf4j
1512
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
1613
import org.springframework.security.core.context.SecurityContextHolder
1714
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
1815
import org.springframework.util.AntPathMatcher
1916
import org.springframework.util.StringUtils
2017
import org.springframework.web.filter.OncePerRequestFilter
21-
import java.io.IOException
2218

2319
class JwtAuthenticationFilter(
2420
private val jwtUtil: JwtUtil,
@@ -45,7 +41,7 @@ class JwtAuthenticationFilter(
4541

4642
if (StringUtils.hasText(token)) {
4743
val claims = jwtUtil.validateAndGetClaimsFromToken(token)
48-
val jwtUserInfo = JwtUserInfo(id = claims.get(Constants.USER_ID_CLAIM_NAME, Long::class.java))
44+
val jwtUserInfo = JwtUserInfo(id = claims["uid"].toString().toLong())
4945

5046
val beforeAuthentication =
5147
JwtAuthenticationToken(null, jwtUserInfo.id)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.app.server.core.security.handler
2+
3+
import com.app.server.common.enums.CommonResultCode
4+
import com.app.server.common.exception.UnauthorizedException
5+
import jakarta.servlet.http.HttpServletRequest
6+
import jakarta.servlet.http.HttpServletResponse
7+
import org.springframework.security.core.AuthenticationException
8+
import org.springframework.security.web.authentication.AuthenticationFailureHandler
9+
import org.springframework.stereotype.Component
10+
import com.fasterxml.jackson.databind.ObjectMapper
11+
import com.app.server.common.response.ApiResponse
12+
13+
@Component
14+
class CustomOAuth2FailureHandler : AuthenticationFailureHandler {
15+
16+
override fun onAuthenticationFailure(
17+
request: HttpServletRequest?,
18+
response: HttpServletResponse?,
19+
exception: AuthenticationException?
20+
) {
21+
response?.contentType = "application/json"
22+
response?.status = HttpServletResponse.SC_UNAUTHORIZED
23+
24+
val errorMessage = exception?.message ?: CommonResultCode.OAUTH2_EXCEPTION.message
25+
26+
val responseBody = ApiResponse.failure<UnauthorizedException>(CommonResultCode.OAUTH2_EXCEPTION, errorMessage)
27+
28+
response?.writer?.write(ObjectMapper().writeValueAsString(responseBody))
29+
}
30+
}

0 commit comments

Comments
 (0)