Skip to content

Commit 6cad3d2

Browse files
정동하정동하
authored andcommitted
feat(be): security 기본 설정
1 parent c749840 commit 6cad3d2

File tree

12 files changed

+301
-24
lines changed

12 files changed

+301
-24
lines changed

build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ dependencies {
3434
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
3535
implementation("org.jetbrains.kotlin:kotlin-reflect")
3636

37+
// jwt
38+
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
39+
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
40+
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
41+
3742
// Spring AI - 1.1.0-M2 (최신 버전) - 새로운 artifact명
3843
implementation("org.springframework.ai:spring-ai-starter-model-openai:1.1.0-M2")
3944
// Spring AI - JDBC 채팅 메모리 저장소 (PostgreSQL 대화 기록 저장)

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

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.back.koreaTravelGuide.security
2+
3+
import com.back.koreaTravelGuide.domain.user.enums.UserRole
4+
import com.back.koreaTravelGuide.domain.user.repository.UserRepository
5+
import jakarta.servlet.http.HttpServletRequest
6+
import jakarta.servlet.http.HttpServletResponse
7+
import org.springframework.security.core.Authentication
8+
import org.springframework.security.oauth2.core.user.OAuth2User
9+
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler
10+
import org.springframework.stereotype.Component
11+
import org.springframework.transaction.annotation.Transactional
12+
13+
@Component
14+
class CustomOAuth2LoginSuccessHandler(
15+
private val jwtTokenProvider: JwtTokenProvider,
16+
private val userRepository: UserRepository,
17+
) : SimpleUrlAuthenticationSuccessHandler() {
18+
@Transactional
19+
override fun onAuthenticationSuccess(
20+
request: HttpServletRequest,
21+
response: HttpServletResponse,
22+
authentication: Authentication,
23+
) {
24+
val oAuth2User = authentication.principal as OAuth2User
25+
val email = oAuth2User.attributes["email"] as String
26+
27+
val user = userRepository.findByEmail(email)!!
28+
29+
if (user.role == UserRole.PENDING) {
30+
val registerToken = jwtTokenProvider.createRegisterToken(user.id!!)
31+
val targetUrl = "http://localhost:3000/signup/role?token=$registerToken"
32+
redirectStrategy.sendRedirect(request, response, targetUrl)
33+
} else {
34+
val accessToken = jwtTokenProvider.createAccessToken(user.id!!, user.role)
35+
val targetUrl = "http://localhost:3000/oauth/callback?accessToken=$accessToken"
36+
redirectStrategy.sendRedirect(request, response, targetUrl)
37+
}
38+
}
39+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.back.koreaTravelGuide.security
2+
3+
import org.springframework.security.core.GrantedAuthority
4+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User
5+
6+
class CustomOAuth2User(
7+
val id: Long,
8+
val email: String,
9+
authorities: Collection<GrantedAuthority>,
10+
attributes: Map<String, Any>,
11+
) : DefaultOAuth2User(authorities, attributes, "email")
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.back.koreaTravelGuide.security
2+
3+
import com.back.koreaTravelGuide.domain.user.entity.User
4+
import com.back.koreaTravelGuide.domain.user.enums.UserRole
5+
import com.back.koreaTravelGuide.domain.user.repository.UserRepository
6+
import org.springframework.security.core.authority.SimpleGrantedAuthority
7+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService
8+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
9+
import org.springframework.security.oauth2.core.user.OAuth2User
10+
import org.springframework.stereotype.Service
11+
import org.springframework.transaction.annotation.Transactional
12+
13+
@Service
14+
class CustomOAuth2UserService(
15+
private val userRepository: UserRepository,
16+
) : DefaultOAuth2UserService() {
17+
@Transactional
18+
override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
19+
val oAuth2User = super.loadUser(userRequest)
20+
val provider = userRequest.clientRegistration.registrationId
21+
val attributes = oAuth2User.attributes
22+
23+
val oAuthUserInfo =
24+
when (provider) {
25+
"google" -> parseGoogle(attributes)
26+
else -> throw IllegalArgumentException("지원하지 않는 소셜 로그인입니다.")
27+
}
28+
29+
val user =
30+
userRepository.findByOauthProviderAndOauthId(provider, oAuthUserInfo.oauthId)
31+
?: userRepository.save(
32+
User(
33+
oauthProvider = provider,
34+
oauthId = oAuthUserInfo.oauthId,
35+
email = oAuthUserInfo.email,
36+
nickname = oAuthUserInfo.nickname,
37+
profileImageUrl = oAuthUserInfo.profileImageUrl,
38+
role = UserRole.PENDING,
39+
),
40+
)
41+
42+
val authorities = listOf(SimpleGrantedAuthority("ROLE_${user.role.name}"))
43+
44+
return CustomOAuth2User(
45+
id = user.id!!,
46+
email = user.email,
47+
authorities = authorities,
48+
attributes = attributes,
49+
)
50+
}
51+
52+
private fun parseGoogle(attributes: Map<String, Any>): OAuthUserInfo {
53+
return OAuthUserInfo(
54+
oauthId = attributes["sub"] as String,
55+
email = attributes["email"] as String,
56+
nickname = attributes["name"] as String,
57+
profileImageUrl = attributes["picture"] as String?,
58+
)
59+
}
60+
}
61+
62+
data class OAuthUserInfo(
63+
val oauthId: String,
64+
val email: String,
65+
val nickname: String,
66+
val profileImageUrl: String?,
67+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.koreaTravelGuide.security
2+
3+
import jakarta.servlet.FilterChain
4+
import jakarta.servlet.http.HttpServletRequest
5+
import jakarta.servlet.http.HttpServletResponse
6+
import org.springframework.security.core.context.SecurityContextHolder
7+
import org.springframework.stereotype.Component
8+
import org.springframework.web.filter.OncePerRequestFilter
9+
10+
@Component
11+
class JwtAuthenticationFilter(
12+
private val jwtTokenProvider: JwtTokenProvider,
13+
) : OncePerRequestFilter() {
14+
override fun doFilterInternal(
15+
request: HttpServletRequest,
16+
response: HttpServletResponse,
17+
filterChain: FilterChain,
18+
) {
19+
val token = resolveToken(request)
20+
21+
if (token != null && jwtTokenProvider.validateToken(token)) {
22+
val authentication = jwtTokenProvider.getAuthentication(token)
23+
SecurityContextHolder.getContext().authentication = authentication
24+
}
25+
26+
filterChain.doFilter(request, response)
27+
}
28+
29+
private fun resolveToken(request: HttpServletRequest): String? {
30+
val bearerToken = request.getHeader("Authorization")
31+
return if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
32+
bearerToken.substring(7)
33+
} else {
34+
null
35+
}
36+
}
37+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.back.koreaTravelGuide.security
2+
3+
import com.back.koreaTravelGuide.domain.user.enums.UserRole
4+
import io.jsonwebtoken.Claims
5+
import io.jsonwebtoken.Jwts
6+
import io.jsonwebtoken.security.Keys
7+
import org.springframework.beans.factory.annotation.Value
8+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
9+
import org.springframework.security.core.Authentication
10+
import org.springframework.security.core.authority.SimpleGrantedAuthority
11+
import org.springframework.stereotype.Component
12+
import java.util.Base64
13+
import java.util.Date
14+
import javax.crypto.SecretKey
15+
16+
@Component
17+
class JwtTokenProvider(
18+
@Value("\${jwt.secret-key}") private val secretKey: String,
19+
@Value("\${jwt.access-token-expiration-minutes}") private val accessTokenExpirationMinutes: Long,
20+
) {
21+
private val key: SecretKey by lazy {
22+
Keys.hmacShaKeyFor(Base64.getEncoder().encode(secretKey.toByteArray()))
23+
}
24+
25+
fun createAccessToken(
26+
userId: Long,
27+
role: UserRole,
28+
): String {
29+
val now = Date()
30+
val expiryDate = Date(now.time + accessTokenExpirationMinutes * 60 * 1000)
31+
32+
return Jwts.builder()
33+
.subject(userId.toString())
34+
.claim("role", "ROLE_${role.name}")
35+
.issuedAt(now)
36+
.expiration(expiryDate)
37+
.signWith(key)
38+
.compact()
39+
}
40+
41+
fun createRegisterToken(userId: Long): String {
42+
val now = Date()
43+
val expiryDate = Date(now.time + 5 * 60 * 1000)
44+
45+
return Jwts.builder()
46+
.subject(userId.toString())
47+
.issuedAt(now)
48+
.expiration(expiryDate)
49+
.signWith(key)
50+
.compact()
51+
}
52+
53+
fun validateToken(token: String): Boolean {
54+
try {
55+
getClaimsFromToken(token)
56+
return true
57+
} catch (e: Exception) {
58+
return false
59+
}
60+
}
61+
62+
fun getAuthentication(token: String): Authentication {
63+
val claims = getClaimsFromToken(token)
64+
val userId = claims.subject.toLong()
65+
val role = claims["role"] as? String ?: "ROLE_PENDING"
66+
val authorities = listOf(SimpleGrantedAuthority(role))
67+
68+
return UsernamePasswordAuthenticationToken(userId, null, authorities)
69+
}
70+
71+
private fun getClaimsFromToken(token: String): Claims {
72+
return Jwts.parser()
73+
.verifyWith(key)
74+
.build()
75+
.parseSignedClaims(token)
76+
.payload
77+
}
78+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.back.koreaTravelGuide.common.config
2+
3+
import com.back.koreaTravelGuide.security.CustomOAuth2LoginSuccessHandler
4+
import com.back.koreaTravelGuide.security.CustomOAuth2UserService
5+
import com.back.koreaTravelGuide.security.JwtAuthenticationFilter
6+
import org.springframework.context.annotation.Bean
7+
import org.springframework.context.annotation.Configuration
8+
import org.springframework.security.config.annotation.web.builders.HttpSecurity
9+
import org.springframework.security.config.annotation.web.invoke
10+
import org.springframework.security.config.http.SessionCreationPolicy
11+
import org.springframework.security.web.SecurityFilterChain
12+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
13+
14+
@Configuration
15+
class SecurityConfig(
16+
private val customOAuth2UserService: CustomOAuth2UserService,
17+
private val customOAuth2LoginSuccessHandler: CustomOAuth2LoginSuccessHandler,
18+
private val jwtAuthenticationFilter: JwtAuthenticationFilter,
19+
) {
20+
@Bean
21+
fun filterChain(http: HttpSecurity): SecurityFilterChain {
22+
http {
23+
// 기본 보안 기능
24+
csrf { disable() }
25+
formLogin { disable() }
26+
httpBasic { disable() }
27+
logout { disable() }
28+
29+
sessionManagement {
30+
sessionCreationPolicy = SessionCreationPolicy.STATELESS
31+
}
32+
33+
oauth2Login {
34+
userInfoEndpoint {
35+
userService = customOAuth2UserService
36+
}
37+
authenticationSuccessHandler = customOAuth2LoginSuccessHandler
38+
}
39+
40+
authorizeHttpRequests {
41+
authorize("/api/auth/**", permitAll)
42+
authorize("/swagger-ui/**", "/v3/api-docs/**", permitAll)
43+
authorize("/h2-console/**", permitAll)
44+
authorize("/favicon.ico", permitAll)
45+
authorize(anyRequest, authenticated)
46+
}
47+
addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
48+
}
49+
50+
return http.build()
51+
}
52+
}

src/main/kotlin/com/back/koreaTravelGuide/domain/user/enums/UserRole.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ enum class UserRole {
44
USER,
55
ADMIN,
66
GUIDE,
7+
8+
// 신규 사용자
9+
PENDING,
710
}

src/main/kotlin/com/back/koreaTravelGuide/domain/user/repository/UserRepository.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,11 @@ import org.springframework.stereotype.Repository
88
@Repository
99
interface UserRepository : JpaRepository<User, Long> {
1010
fun findByRole(role: UserRole): List<User>
11+
12+
fun findByOauthProviderAndOauthId(
13+
oauthProvider: String,
14+
oauthId: String,
15+
): User?
16+
17+
fun findByEmail(email: String): User?
1118
}

0 commit comments

Comments
 (0)