diff --git a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/auth/ReadCurrentUserAdapter.kt b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/auth/ReadCurrentUserAdapter.kt index caabc6eac..3b943f8fd 100644 --- a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/auth/ReadCurrentUserAdapter.kt +++ b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/auth/ReadCurrentUserAdapter.kt @@ -1,39 +1,37 @@ package com.info.maeumgagym.auth import com.info.maeumgagym.auth.port.out.ReadCurrentUserPort -import com.info.maeumgagym.common.exception.AuthenticationException -import com.info.maeumgagym.security.principle.CustomUserDetails +import com.info.maeumgagym.security.jwt.AuthenticationProvider +import com.info.maeumgagym.security.jwt.JwtFilter import com.info.maeumgagym.user.model.User import com.info.maeumgagym.user.port.out.ReadUserPort -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component @Component internal class ReadCurrentUserAdapter( - private val readUserPort: ReadUserPort + private val readUserPort: ReadUserPort, + private val authenticationProvider: AuthenticationProvider ) : ReadCurrentUserPort { override fun readCurrentUser(): User { + // User를 찾기 위한 정보가 담겨 있는 Authentication 로드 val authentication = SecurityContextHolder.getContext().authentication - // jwt filter에서 생성한 authDetail를 context holder에서 불러옴 - val authDetails = authentication.principal as CustomUserDetails - - // Lazy Loading으로 Nullable인 User를 확인하고, null인 경우 User를 Load 및 입력 - if (authDetails.getUser() == null) { - authDetails.fillUser( - readUserPort.readByOAuthId(authDetails.username) - // authDetails에 담긴 username = oauthId는 로직상 무조건 유저가 존재해야하므로 AuthenticationException throw - ?: throw AuthenticationException(401, "User Not Found In ReadCurrentUserPort") - ) + JwtFilter.run { + // Lazy Loading으로 Nullable인 User를 확인 + if (this.authenticatedUser?.get() == null || + this.authenticatedUser?.get()?.oauthId != authentication!!.principal + ) { + // null인 경우 User를 Load 및 SecurityContext, authenticatedUser에 입력 + SecurityContextHolder.getContext().authentication = + authenticationProvider.getAuthentication( + authentication.principal as String + ) + } } - // Loading된 User를 Authentication에도 반영 - SecurityContextHolder.getContext().authentication = - UsernamePasswordAuthenticationToken(authDetails, null, authDetails.authorities) - // User 반환 - return authDetails.getUser()!! + return JwtFilter.authenticatedUser!!.get() } } diff --git a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/config/RequestPermitConfig.kt b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/config/RequestPermitConfig.kt index 80a86d5f8..f0bff5f45 100644 --- a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/config/RequestPermitConfig.kt +++ b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/config/RequestPermitConfig.kt @@ -1,5 +1,6 @@ package com.info.maeumgagym.security.config +import com.info.maeumgagym.user.model.Role import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.SecurityConfigurerAdapter import org.springframework.security.config.annotation.web.builders.HttpSecurity @@ -12,41 +13,42 @@ import org.springframework.web.cors.CorsUtils class RequestPermitConfig : SecurityConfigurerAdapter() { internal companion object { - val permittedURI = mapOf( - Pair(HttpMethod.POST, "/**/signup"), - Pair(HttpMethod.GET, "/**/login"), - Pair(HttpMethod.PUT, "/**/recovery"), - Pair(HttpMethod.GET, "/auth/nickname/*"), - Pair(HttpMethod.GET, "/auth/re-issue"), - Pair(HttpMethod.GET, "/public/csrf"), - Pair(HttpMethod.GET, "/actuator/health") + val permittedURIs: Map = mapOf( + Pair("/**/signup", HttpMethod.POST), + Pair("/**/login", HttpMethod.GET), + Pair("/**/recovery", HttpMethod.PUT), + Pair("/auth/nickname/*", HttpMethod.GET), + Pair("/auth/re-issue", HttpMethod.GET), + Pair("/public/csrf", HttpMethod.GET), + Pair("/actuator/health", HttpMethod.GET) ) - val needAdminRoleURI = mapOf( - Pair(HttpMethod.GET, "/report") + val needAdminRoleURIs: Map = mapOf( + Pair("/report", HttpMethod.GET) ) } override fun configure(builder: HttpSecurity) { - builder.authorizeRequests().run { - requestMatchers(CorsUtils::isCorsRequest).permitAll() - permittedURIConfigure() - needAdminRoleURIConfigure() - anyRequest().authenticated() - } + builder.authorizeRequests() + .requestMatchers(CorsUtils::isCorsRequest).permitAll() + .permittedURIConfigure() + .needAdminRoleURIConfigure() + .anyRequest().authenticated() } private fun ExpressionUrlAuthorizationConfigurer - .ExpressionInterceptUrlRegistry.permittedURIConfigure() { - permittedURI.forEach { - antMatchers(it.key, it.value).permitAll() + .ExpressionInterceptUrlRegistry.permittedURIConfigure() = + this.apply { + permittedURIs.forEach { + antMatchers(it.value, it.key).permitAll() + } } - } private fun ExpressionUrlAuthorizationConfigurer - .ExpressionInterceptUrlRegistry.needAdminRoleURIConfigure() { - permittedURI.forEach { - antMatchers(it.key, it.value).permitAll() + .ExpressionInterceptUrlRegistry.needAdminRoleURIConfigure() = + this.apply { + needAdminRoleURIs.forEach { + antMatchers(it.value, it.key).hasRole(Role.ADMIN.name) + } } - } } diff --git a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/JwtFilter.kt b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/JwtFilter.kt index 4458cd3b7..376232a80 100644 --- a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/JwtFilter.kt +++ b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/JwtFilter.kt @@ -2,6 +2,7 @@ package com.info.maeumgagym.security.jwt import com.info.maeumgagym.security.config.RequestPermitConfig import com.info.maeumgagym.security.jwt.env.JwtProperties +import com.info.maeumgagym.user.model.User import org.springframework.security.core.context.SecurityContextHolder import org.springframework.stereotype.Component import org.springframework.util.AntPathMatcher @@ -24,6 +25,11 @@ class JwtFilter( private val jwtProperties: JwtProperties ) : OncePerRequestFilter() { + companion object { + // 현재 요청에서 인증한 User를 전역적으로 저장, 필요 여부에 따라 Nullable + internal var authenticatedUser: ThreadLocal? = null + } + private var antPathMatcher: AntPathMatcher = AntPathMatcher() override fun doFilterInternal( @@ -31,30 +37,35 @@ class JwtFilter( response: HttpServletResponse, filterChain: FilterChain ) { - // 헤더에 토큰이 존재하는지 확인 - val header = request.getHeader(jwtProperties.header) + try { + // 헤더에 토큰이 존재하는지 확인 + val header = request.getHeader(jwtProperties.header) - if (header != null) { - // 토큰이 유효한지 확인, 유효하다면 -> - jwtResolver(header)?.let { - // security context holder에 Authentication 저장 - SecurityContextHolder.getContext().authentication = - if (needRole(request)) { // Role 인증이 필요하다면 User Eager Loading - authenticationProvider.getAuthentication(it) - } else { // 필요하지 않다면 User Lazy Loading - authenticationProvider.getEmptyAuthentication(it) - } + if (header != null) { + // 토큰이 유효한지 확인, 유효하다면 -> + jwtResolver(header)?.let { + // security context holder에 Authentication 저장 + SecurityContextHolder.getContext().authentication = + if (needRole(request)) { // Role 인증이 필요하다면 User Loading + authenticationProvider.getAuthentication(it) + } else { // 필요하지 않다면 User Lazy Loading + authenticationProvider.getEmptyAuthentication(it) + } + } } - } - // 다음 필터로 넘기기 - filterChain.doFilter(request, response) + // 다음 필터로 넘기기 + filterChain.doFilter(request, response) + } finally { + // Filter가 종료되면 User 정보를 초기화 + authenticatedUser = null + } } private fun needRole(request: HttpServletRequest): Boolean { - RequestPermitConfig.needAdminRoleURI.forEach { - if (request.method == it.key.name && - antPathMatcher.match(it.value, request.requestURI) + RequestPermitConfig.needAdminRoleURIs.forEach { + if (request.method == it.value.name && + antPathMatcher.match(it.key, request.requestURI) ) { return true } diff --git a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/impl/AuthenticationProviderImpl.kt b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/impl/AuthenticationProviderImpl.kt index 380be6cf0..bd89a956c 100644 --- a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/impl/AuthenticationProviderImpl.kt +++ b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/jwt/impl/AuthenticationProviderImpl.kt @@ -2,10 +2,10 @@ package com.info.maeumgagym.security.jwt.impl import com.info.maeumgagym.common.exception.CriticalException import com.info.maeumgagym.security.jwt.AuthenticationProvider -import com.info.maeumgagym.security.principle.CustomUserDetails +import com.info.maeumgagym.security.jwt.JwtFilter import com.info.maeumgagym.user.port.out.ReadUserPort import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.stereotype.Component /** @@ -16,27 +16,28 @@ import org.springframework.stereotype.Component */ @Component class AuthenticationProviderImpl( - private val userDetailsService: UserDetailsService, private val readUserPort: ReadUserPort ) : AuthenticationProvider { override fun getAuthentication(subject: String): UsernamePasswordAuthenticationToken { - // UserDetails 생성 - val authDetails = userDetailsService.loadUserByUsername(subject) as CustomUserDetails - - val user = readUserPort.readByOAuthId(subject) - ?: throw CriticalException(500, "User Not Found In AuthenticationProvider") - - authDetails.fillUser(user) - - // Authentication발급 - return UsernamePasswordAuthenticationToken(authDetails, null, authDetails.authorities) + // User가 필요한 경우 불러와 전역적으로 저장 + JwtFilter.authenticatedUser = ThreadLocal.withInitial { + readUserPort.readByOAuthId(subject) + ?: throw CriticalException(500, "User Not Found In AuthenticationProvider") + } + + // Authentication에 subject를 넣어 반환 + return UsernamePasswordAuthenticationToken( + subject, + null, + JwtFilter.authenticatedUser?.get()?.roles?.map { + SimpleGrantedAuthority(it.name) + } + ) } override fun getEmptyAuthentication(subject: String): UsernamePasswordAuthenticationToken { - // UserDetails 생성 - val authDetails = userDetailsService.loadUserByUsername(subject) as CustomUserDetails - - return UsernamePasswordAuthenticationToken(authDetails, null, null) + // Authentication에 subject를 넣어 반환 + return UsernamePasswordAuthenticationToken(subject, null, null) } } diff --git a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/principle/CustomUserDetailService.kt b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/principle/CustomUserDetailService.kt deleted file mode 100644 index bd3cc4bdf..000000000 --- a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/principle/CustomUserDetailService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.info.maeumgagym.security.principle - -import org.springframework.security.core.userdetails.UserDetailsService -import org.springframework.stereotype.Component - -@Component -class CustomUserDetailService : UserDetailsService { - - // 유저를 DB에서 불러와 UserDetails를 반환하는 함수 - override fun loadUserByUsername(username: String): CustomUserDetails { - // CustomUserDetails에 User를 null로 삽입해 반환 - return CustomUserDetails(null, username) - } -} diff --git a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/principle/CustomUserDetails.kt b/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/principle/CustomUserDetails.kt deleted file mode 100644 index 5db56ef0f..000000000 --- a/maeumgagym-infrastructure/src/main/kotlin/com/info/maeumgagym/security/principle/CustomUserDetails.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.info.maeumgagym.security.principle - -import com.info.maeumgagym.user.model.User -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.core.userdetails.UserDetails - -class CustomUserDetails( - private var user: User?, - private val oauthId: String -) : UserDetails { - - fun getUser(): User? = user - - fun fillUser(user: User) { - if (this.user == null) { - this.user = user - } - } - - override fun getAuthorities(): MutableCollection = - user?.roles?.map { - SimpleGrantedAuthority(it.name) - }?.toMutableList() ?: mutableListOf() - - override fun getPassword(): String? = null - - override fun getUsername(): String = oauthId - - override fun isAccountNonExpired(): Boolean = true - - override fun isAccountNonLocked(): Boolean = true - - override fun isCredentialsNonExpired(): Boolean = true - - override fun isEnabled(): Boolean = true -} diff --git a/maeumgagym-infrastructure/src/test/kotlin/com/info/maeumgagym/domain/auth/AuthTestModule.kt b/maeumgagym-infrastructure/src/test/kotlin/com/info/maeumgagym/domain/auth/AuthTestModule.kt index 335efde71..3959fc734 100644 --- a/maeumgagym-infrastructure/src/test/kotlin/com/info/maeumgagym/domain/auth/AuthTestModule.kt +++ b/maeumgagym-infrastructure/src/test/kotlin/com/info/maeumgagym/domain/auth/AuthTestModule.kt @@ -2,8 +2,9 @@ package com.info.maeumgagym.domain.auth import com.info.maeumgagym.domain.user.entity.UserJpaEntity import com.info.maeumgagym.domain.user.mapper.UserMapper -import com.info.maeumgagym.security.principle.CustomUserDetails +import com.info.maeumgagym.security.jwt.JwtFilter import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.context.SecurityContextHolder internal object AuthTestModule { @@ -13,10 +14,15 @@ internal object AuthTestModule { fun UserJpaEntity.saveInContext(userMapper: UserMapper): UserJpaEntity = apply { + JwtFilter.authenticatedUser = null SecurityContextHolder.getContext().authentication = - CustomUserDetails(userMapper.toDomain(this), this.oauthId).run { - UsernamePasswordAuthenticationToken(this, null, this.authorities) - } + UsernamePasswordAuthenticationToken( + this.oauthId, + null, + this.roles.map { + SimpleGrantedAuthority(it.name) + } + ) } /**