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

PR :: Authentication에서 UserDetails 제거 #182

Merged
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,41 +13,42 @@ import org.springframework.web.cors.CorsUtils
class RequestPermitConfig : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {

internal companion object {
val permittedURI = mapOf<HttpMethod, String>(
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<String, HttpMethod> = 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<HttpMethod, String>(
Pair(HttpMethod.GET, "/report")
val needAdminRoleURIs: Map<String, HttpMethod> = 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<HttpSecurity>
.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<HttpSecurity>
.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)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,37 +25,47 @@ class JwtFilter(
private val jwtProperties: JwtProperties
) : OncePerRequestFilter() {

companion object {
// 현재 요청에서 인증한 User를 전역적으로 저장, 필요 여부에 따라 Nullable
internal var authenticatedUser: ThreadLocal<User>? = null
}

private var antPathMatcher: AntPathMatcher = AntPathMatcher()

override fun doFilterInternal(
request: HttpServletRequest,
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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)
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
)
}

/**
Expand Down
Loading