Skip to content

Commit 1284fa4

Browse files
정동하정동하
authored andcommitted
work
1 parent 4992302 commit 1284fa4

File tree

6 files changed

+269
-29
lines changed

6 files changed

+269
-29
lines changed

src/main/kotlin/com/back/koreaTravelGuide/common/security/CustomOAuth2LoginSuccessHandler.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ class CustomOAuth2LoginSuccessHandler(
3939

4040
redirectStrategy.sendRedirect(request, response, targetUrl)
4141
} else {
42-
val accessToken = jwtTokenProvider.createAccessToken(user.id!!, user.role)
43-
4442
val refreshToken = jwtTokenProvider.createRefreshToken(user.id!!)
4543

4644
val redisKey = "refreshToken:${user.id}"
@@ -60,7 +58,7 @@ class CustomOAuth2LoginSuccessHandler(
6058

6159
response.addCookie(cookie)
6260

63-
val targetUrl = "http://localhost:3000/oauth/callback?accessToken=$accessToken"
61+
val targetUrl = "http://localhost:3000/oauth/callback"
6462

6563
redirectStrategy.sendRedirect(request, response, targetUrl)
6664
}

src/main/kotlin/com/back/koreaTravelGuide/common/security/JwtTokenProvider.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package com.back.koreaTravelGuide.common.security
22

3+
import com.back.koreaTravelGuide.common.logging.log
34
import com.back.koreaTravelGuide.domain.user.enums.UserRole
45
import io.jsonwebtoken.Claims
56
import io.jsonwebtoken.Jwts
67
import io.jsonwebtoken.security.Keys
7-
import org.slf4j.LoggerFactory
88
import org.springframework.beans.factory.annotation.Value
99
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
1010
import org.springframework.security.core.Authentication
@@ -20,10 +20,7 @@ class JwtTokenProvider(
2020
@Value("\${jwt.access-token-expiration-minutes}") private val accessTokenExpirationMinutes: Long,
2121
@Value("\${jwt.refresh-token-expiration-days}") private val refreshTokenExpirationDays: Long,
2222
) {
23-
private val logger = LoggerFactory.getLogger(JwtTokenProvider::class.java)
24-
2523
private val key: SecretKey by lazy {
26-
2724
Keys.hmacShaKeyFor(Base64.getEncoder().encode(secretKey.toByteArray()))
2825
}
2926

@@ -76,7 +73,7 @@ class JwtTokenProvider(
7673

7774
return true
7875
} catch (e: Exception) {
79-
logger.error("Token validation error: ${e.message}")
76+
log.error("Token validation error: ${e.message}")
8077

8178
return false
8279
}

src/main/kotlin/com/back/koreaTravelGuide/common/security/SecurityConfig.kt

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.back.koreaTravelGuide.common.config
22

3-
import com.back.koreaTravelGuide.security.CustomOAuth2LoginSuccessHandler
4-
import com.back.koreaTravelGuide.security.CustomOAuth2UserService
5-
import com.back.koreaTravelGuide.security.JwtAuthenticationFilter
3+
import com.back.koreaTravelGuide.common.security.CustomOAuth2LoginSuccessHandler
4+
import com.back.koreaTravelGuide.common.security.CustomOAuth2UserService
5+
import com.back.koreaTravelGuide.common.security.JwtAuthenticationFilter
66
import org.springframework.context.annotation.Bean
77
import org.springframework.context.annotation.Configuration
88
import org.springframework.core.env.Environment
@@ -46,30 +46,29 @@ class SecurityConfig(
4646
}
4747
}
4848

49-
if (!isDev) {
50-
oauth2Login {
51-
userInfoEndpoint {
52-
userService = customOAuth2UserService
53-
}
54-
authenticationSuccessHandler = customOAuth2LoginSuccessHandler
49+
oauth2Login {
50+
userInfoEndpoint {
51+
userService = customOAuth2UserService
5552
}
53+
authenticationSuccessHandler = customOAuth2LoginSuccessHandler
5654
}
5755

5856
authorizeHttpRequests {
57+
// 인증 없이 접근을 허용할 경로들
5958
authorize("/h2-console/**", permitAll)
6059
authorize("/swagger-ui/**", "/v3/api-docs/**", permitAll)
61-
authorize("/api/auth/**", permitAll)
60+
authorize("/api/auth/**", permitAll) // 토큰 재발급 API
6261
authorize("/favicon.ico", permitAll)
63-
if (isDev) {
64-
authorize(anyRequest, permitAll)
65-
} else {
66-
authorize(anyRequest, authenticated)
67-
}
68-
}
6962

70-
if (!isDev) {
71-
addFilterBefore<UsernamePasswordAuthenticationFilter>(jwtAuthenticationFilter)
63+
// 소셜 로그인 흐름을 위한 경로 허용
64+
authorize("/login/oauth2/code/*", permitAll)
65+
authorize("/oauth2/authorization/*", permitAll)
66+
67+
// 위에서 허용한 경로 외의 모든 요청은 인증 필요
68+
authorize(anyRequest, authenticated)
7269
}
70+
71+
addFilterBefore<UsernamePasswordAuthenticationFilter>(jwtAuthenticationFilter)
7372
}
7473

7574
return http.build()

src/main/kotlin/com/back/koreaTravelGuide/domain/user/entity/User.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import jakarta.persistence.Table
1313
import org.springframework.data.annotation.CreatedDate
1414
import org.springframework.data.annotation.LastModifiedDate
1515
import org.springframework.data.jpa.domain.support.AuditingEntityListener
16-
import java.time.LocalDateTime
16+
import java.time.ZoneId
17+
import java.time.ZonedDateTime
1718

1819
@Entity
1920
@Table(name = "users")
@@ -41,8 +42,8 @@ class User(
4142
var description: String? = null,
4243
@CreatedDate
4344
@Column(name = "created_at", nullable = false, updatable = false)
44-
val createdAt: LocalDateTime = LocalDateTime.now(),
45+
val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
4546
@LastModifiedDate
4647
@Column(name = "last_login_at")
47-
var lastLoginAt: LocalDateTime = LocalDateTime.now(),
48+
var lastLoginAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
4849
)
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.back.koreaTravelGuide.domain.auth.controller
2+
3+
import com.back.koreaTravelGuide.common.security.JwtTokenProvider
4+
import com.back.koreaTravelGuide.domain.user.entity.User
5+
import com.back.koreaTravelGuide.domain.user.enums.UserRole
6+
import com.back.koreaTravelGuide.domain.user.repository.UserRepository
7+
import com.fasterxml.jackson.databind.ObjectMapper
8+
import jakarta.servlet.http.Cookie
9+
import org.junit.jupiter.api.Assertions.assertTrue
10+
import org.junit.jupiter.api.BeforeEach
11+
import org.junit.jupiter.api.DisplayName
12+
import org.junit.jupiter.api.Test
13+
import org.springframework.beans.factory.annotation.Autowired
14+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
15+
import org.springframework.boot.test.context.SpringBootTest
16+
import org.springframework.data.redis.core.RedisTemplate
17+
import org.springframework.http.MediaType
18+
import org.springframework.test.context.ActiveProfiles
19+
import org.springframework.test.web.servlet.MockMvc
20+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
21+
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
22+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie
23+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
24+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
25+
import org.springframework.transaction.annotation.Transactional
26+
import java.util.concurrent.TimeUnit
27+
28+
@ActiveProfiles("test")
29+
@SpringBootTest
30+
@AutoConfigureMockMvc
31+
@Transactional
32+
class AuthControllerTest {
33+
@Autowired
34+
private lateinit var mockMvc: MockMvc
35+
36+
@Autowired
37+
private lateinit var userRepository: UserRepository
38+
39+
@Autowired
40+
private lateinit var jwtTokenProvider: JwtTokenProvider
41+
42+
@Autowired
43+
private lateinit var redisTemplate: RedisTemplate<String, String>
44+
45+
@Autowired
46+
private lateinit var objectMapper: ObjectMapper
47+
48+
private lateinit var pendingUser: User
49+
private lateinit var generalUser: User
50+
51+
@BeforeEach
52+
fun setUp() {
53+
pendingUser =
54+
userRepository.save(
55+
User(
56+
email = "[email protected]",
57+
nickname = "pendingUser",
58+
role = UserRole.PENDING,
59+
oauthProvider = "test",
60+
oauthId = "test1234",
61+
),
62+
)
63+
64+
generalUser =
65+
userRepository.save(
66+
User(
67+
email = "[email protected]",
68+
nickname = "generalUser",
69+
role = UserRole.USER,
70+
oauthProvider = "test",
71+
oauthId = "test5678",
72+
),
73+
)
74+
}
75+
76+
@Test
77+
@DisplayName("신규 사용자 역할 선택 성공")
78+
fun t1() {
79+
// given
80+
val registerToken = jwtTokenProvider.createRegisterToken(pendingUser.id!!)
81+
val requestBody = mapOf("role" to UserRole.USER)
82+
83+
// when & then
84+
mockMvc.perform(
85+
post("/api/auth/role")
86+
.header("Authorization", "Bearer $registerToken")
87+
.contentType(MediaType.APPLICATION_JSON)
88+
.content(objectMapper.writeValueAsString(requestBody)),
89+
)
90+
.andDo(print())
91+
.andExpect(status().isOk)
92+
.andExpect(jsonPath("$.msg").value("역할이 선택되었으며 로그인에 성공했습니다."))
93+
.andExpect(jsonPath("$.data.accessToken").exists())
94+
}
95+
96+
@Test
97+
@DisplayName("로그아웃 성공")
98+
fun t2() {
99+
// given
100+
val accessToken = jwtTokenProvider.createAccessToken(generalUser.id!!, generalUser.role)
101+
102+
// when & then
103+
mockMvc.perform(
104+
post("/api/auth/logout")
105+
.header("Authorization", "Bearer $accessToken"),
106+
)
107+
.andDo(print())
108+
.andExpect(status().isOk)
109+
.andExpect(jsonPath("$.msg").value("로그아웃 되었습니다."))
110+
111+
// verify
112+
val isBlacklisted = redisTemplate.opsForValue().get(accessToken) != null
113+
assertTrue(isBlacklisted)
114+
}
115+
116+
@Test
117+
@DisplayName("토큰 재발급 성공")
118+
fun t3() {
119+
// given
120+
val refreshToken = jwtTokenProvider.createRefreshToken(generalUser.id!!)
121+
val redisKey = "refreshToken:${generalUser.id}"
122+
redisTemplate.opsForValue().set(redisKey, refreshToken, 7, TimeUnit.DAYS)
123+
124+
// when & then
125+
mockMvc.perform(
126+
post("/api/auth/refresh")
127+
.cookie(Cookie("refreshToken", refreshToken)),
128+
)
129+
.andDo(print())
130+
.andExpect(status().isOk)
131+
.andExpect(jsonPath("$.msg").value("Access Token이 성공적으로 재발급되었습니다."))
132+
.andExpect(jsonPath("$.data.accessToken").exists())
133+
.andExpect(cookie().exists("refreshToken"))
134+
}
135+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.back.koreaTravelGuide.domain.user.controller
2+
3+
import com.back.koreaTravelGuide.common.security.JwtTokenProvider
4+
import com.back.koreaTravelGuide.domain.user.entity.User
5+
import com.back.koreaTravelGuide.domain.user.enums.UserRole
6+
import com.back.koreaTravelGuide.domain.user.repository.UserRepository
7+
import com.fasterxml.jackson.databind.ObjectMapper
8+
import org.junit.jupiter.api.Assertions.assertFalse
9+
import org.junit.jupiter.api.BeforeEach
10+
import org.junit.jupiter.api.DisplayName
11+
import org.junit.jupiter.api.Test
12+
import org.springframework.beans.factory.annotation.Autowired
13+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
14+
import org.springframework.boot.test.context.SpringBootTest
15+
import org.springframework.http.MediaType
16+
import org.springframework.test.context.ActiveProfiles
17+
import org.springframework.test.web.servlet.MockMvc
18+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
19+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
20+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch
21+
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
22+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
23+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
24+
import org.springframework.transaction.annotation.Transactional
25+
26+
@ActiveProfiles("test")
27+
@SpringBootTest
28+
@AutoConfigureMockMvc
29+
@Transactional
30+
class UserControllerTest {
31+
@Autowired
32+
private lateinit var mockMvc: MockMvc
33+
34+
@Autowired
35+
private lateinit var userRepository: UserRepository
36+
37+
@Autowired
38+
private lateinit var jwtTokenProvider: JwtTokenProvider
39+
40+
@Autowired
41+
private lateinit var objectMapper: ObjectMapper
42+
43+
private lateinit var testUser: User
44+
private lateinit var accessToken: String
45+
46+
@BeforeEach
47+
fun setUp() {
48+
testUser =
49+
userRepository.save(
50+
User(
51+
email = "[email protected]",
52+
nickname = "testUser",
53+
role = UserRole.USER,
54+
oauthProvider = "test",
55+
oauthId = "test12345",
56+
),
57+
)
58+
accessToken = jwtTokenProvider.createAccessToken(testUser.id!!, testUser.role)
59+
}
60+
61+
@Test
62+
@DisplayName("내 정보 조회 성공")
63+
fun t1() {
64+
// when & then
65+
mockMvc.perform(
66+
get("/api/users/me")
67+
.header("Authorization", "Bearer $accessToken"),
68+
)
69+
.andDo(print())
70+
.andExpect(status().isOk)
71+
.andExpect(jsonPath("$.data.email").value(testUser.email))
72+
.andExpect(jsonPath("$.data.nickname").value(testUser.nickname))
73+
}
74+
75+
@Test
76+
@DisplayName("내 프로필 수정 성공")
77+
fun t2() {
78+
// given
79+
val updatedNickname = "updatedUser"
80+
val requestBody = mapOf("nickname" to updatedNickname)
81+
82+
// when & then
83+
mockMvc.perform(
84+
patch("/api/users/me")
85+
.header("Authorization", "Bearer $accessToken")
86+
.contentType(MediaType.APPLICATION_JSON)
87+
.content(objectMapper.writeValueAsString(requestBody)),
88+
)
89+
.andDo(print())
90+
.andExpect(status().isOk)
91+
.andExpect(jsonPath("$.data.nickname").value(updatedNickname))
92+
}
93+
94+
@Test
95+
@DisplayName("회원 탈퇴 성공")
96+
fun t3() {
97+
// when & then
98+
mockMvc.perform(
99+
delete("/api/users/me")
100+
.header("Authorization", "Bearer $accessToken"),
101+
)
102+
.andDo(print())
103+
.andExpect(status().isOk)
104+
.andExpect(jsonPath("$.msg").value("회원 탈퇴가 완료되었습니다."))
105+
106+
// verify
107+
val userExists = userRepository.existsById(testUser.id!!)
108+
assertFalse(userExists)
109+
}
110+
}

0 commit comments

Comments
 (0)