diff --git a/api/src/main/kotlin/com/few/api/security/authentication/authority/AuthorityUtils.kt b/api/src/main/kotlin/com/few/api/security/authentication/authority/AuthorityUtils.kt new file mode 100644 index 00000000..e2ca7f59 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/security/authentication/authority/AuthorityUtils.kt @@ -0,0 +1,20 @@ +package com.few.api.security.authentication.authority + +import org.apache.commons.lang3.StringUtils +import org.springframework.security.core.GrantedAuthority + +object AuthorityUtils { + + @Throws(IllegalArgumentException::class) + fun toAuthorities(roles: String): List { + val tokens = StringUtils.splitPreserveAllTokens(roles, "[,]") + val rtn: MutableList = ArrayList() + for (token in tokens) { + if (token != "") { + val role = token.trim { it <= ' ' } + rtn.add(Roles.valueOf(role).authority) + } + } + return rtn + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt index 4fbede4c..f3eb5514 100644 --- a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt +++ b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetails.kt @@ -3,7 +3,7 @@ package com.few.api.security.authentication.token import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails -class TokenUserDetails( +open class TokenUserDetails( val authorities: List, val id: String, val email: String, diff --git a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt index 69c9664e..66352a94 100644 --- a/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt +++ b/api/src/main/kotlin/com/few/api/security/authentication/token/TokenUserDetailsService.kt @@ -1,12 +1,10 @@ package com.few.api.security.authentication.token -import com.few.api.security.authentication.authority.Roles +import com.few.api.security.authentication.authority.AuthorityUtils import com.few.api.security.exception.AccessTokenInvalidException import com.few.api.security.token.TokenResolver import io.github.oshai.kotlinlogging.KotlinLogging import io.jsonwebtoken.Claims -import org.apache.commons.lang3.StringUtils -import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.stereotype.Component @@ -44,24 +42,8 @@ class TokenUserDetailsService( String::class.java ) - val authorities = toAuthorities(roles) + val authorities = AuthorityUtils.toAuthorities(roles) return TokenUserDetails(authorities, id.toString(), email) } - - private fun toAuthorities(roles: String): List { - val tokens = StringUtils.splitPreserveAllTokens(roles, "[,]") - val rtn: MutableList = ArrayList() - for (token in tokens) { - if (token != "") { - val role = token.trim { it <= ' ' } - try { - rtn.add(Roles.valueOf(role).authority) - } catch (exception: IllegalArgumentException) { - log.error { "${"Invalid role. role: {}"} $role" } - } - } - } - return rtn - } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt index 40498245..a9b17fca 100644 --- a/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt +++ b/api/src/main/kotlin/com/few/api/web/config/WebConfig.kt @@ -3,15 +3,19 @@ package com.few.api.web.config import com.few.api.web.config.converter.DayCodeConverter import com.few.api.web.config.converter.ViewConverter import com.few.api.web.config.converter.WorkBookCategoryConverter +import com.few.api.web.support.method.UserArgumentHandlerMethodArgumentResolver import org.springframework.context.annotation.Configuration import org.springframework.format.FormatterRegistry import org.springframework.web.cors.CorsConfiguration +import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer @Configuration -class WebConfig : WebMvcConfigurer { +class WebConfig( + private val userArgumentHandlerMethodArgumentResolver: UserArgumentHandlerMethodArgumentResolver, +) : WebMvcConfigurer { override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") .allowedOriginPatterns(CorsConfiguration.ALL) @@ -31,4 +35,8 @@ class WebConfig : WebMvcConfigurer { registry.addConverter(ViewConverter()) registry.addConverter(DayCodeConverter()) } + + override fun addArgumentResolvers(argumentResolvers: MutableList) { + argumentResolvers.add(userArgumentHandlerMethodArgumentResolver) + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt index c5ce0f12..ecb4d6f0 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/article/ArticleController.kt @@ -4,16 +4,15 @@ import com.few.api.domain.article.usecase.ReadArticleUseCase import com.few.api.domain.article.usecase.BrowseArticlesUseCase import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn import com.few.api.domain.article.usecase.dto.ReadArticlesUseCaseIn -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.article.response.ReadArticleResponse import com.few.api.web.controller.article.response.ReadArticlesResponse import com.few.api.web.controller.article.response.WorkbookInfo import com.few.api.web.controller.article.response.WriterInfo import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import com.few.data.common.code.CategoryType -import jakarta.servlet.http.HttpServletRequest import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -26,22 +25,16 @@ import org.springframework.web.bind.annotation.* class ArticleController( private val readArticleUseCase: ReadArticleUseCase, private val browseArticlesUseCase: BrowseArticlesUseCase, - private val tokenResolver: TokenResolver, ) { @GetMapping("/{articleId}") fun readArticle( - servletRequest: HttpServletRequest, + @UserArgument userArgumentDetails: UserArgumentDetails, @PathVariable(value = "articleId") @Min(value = 1, message = "{min.id}") articleId: Long, ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } ?: 0L + val memberId = userArgumentDetails.id.toLong() val useCaseOut = ReadArticleUseCaseIn( articleId = articleId, diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt index c978841b..1e1b45fa 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/WorkBookController.kt @@ -4,14 +4,13 @@ import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase import com.few.api.domain.workbook.usecase.dto.ReadWorkbookUseCaseIn import com.few.api.domain.workbook.usecase.ReadWorkbookUseCase import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.workbook.response.* import com.few.api.web.support.WorkBookCategory import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator import com.few.api.web.support.ViewCategory -import jakarta.servlet.http.HttpServletRequest +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -28,7 +27,6 @@ import org.springframework.web.bind.annotation.RestController class WorkBookController( private val readWorkbookUseCase: ReadWorkbookUseCase, private val browseWorkBooksUseCase: BrowseWorkbooksUseCase, - private val tokenResolver: TokenResolver, ) { @GetMapping("/categories") @@ -48,18 +46,14 @@ class WorkBookController( @GetMapping fun browseWorkBooks( - servletRequest: HttpServletRequest, + @UserArgument userArgumentDetails: UserArgumentDetails, @RequestParam(value = "category", required = false) category: WorkBookCategory?, @RequestParam(value = "view", required = false) viewCategory: ViewCategory?, ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } + val memberId = userArgumentDetails.id.toLong() + val useCaseOut = BrowseWorkbooksUseCaseIn(category ?: WorkBookCategory.All, viewCategory, memberId).let { useCaseIn -> browseWorkBooksUseCase.execute(useCaseIn) diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt index e1cf175e..049d8de0 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleController.kt @@ -2,12 +2,11 @@ package com.few.api.web.controller.workbook.article import com.few.api.domain.workbook.article.dto.ReadWorkBookArticleUseCaseIn import com.few.api.domain.workbook.article.usecase.ReadWorkBookArticleUseCase -import com.few.api.security.filter.token.AccessTokenResolver -import com.few.api.security.token.TokenResolver import com.few.api.web.controller.workbook.article.response.ReadWorkBookArticleResponse import com.few.api.web.support.ApiResponse import com.few.api.web.support.ApiResponseGenerator -import jakarta.servlet.http.HttpServletRequest +import com.few.api.web.support.method.UserArgument +import com.few.api.web.support.method.UserArgumentDetails import jakarta.validation.constraints.Min import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -22,12 +21,11 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping(value = ["/api/v1/workbooks/{workbookId}/articles"], produces = [MediaType.APPLICATION_JSON_VALUE]) class WorkBookArticleController( private val readWorkBookArticleUseCase: ReadWorkBookArticleUseCase, - private val tokenResolver: TokenResolver, ) { @GetMapping("/{articleId}") fun readWorkBookArticle( - servletRequest: HttpServletRequest, + @UserArgument userArgumentDetails: UserArgumentDetails, @PathVariable(value = "workbookId") @Min(value = 1, message = "{min.id}") workbookId: Long, @@ -35,12 +33,7 @@ class WorkBookArticleController( @Min(value = 1, message = "{min.id}") articleId: Long, ): ApiResponse> { - val authorization: String? = servletRequest.getHeader("Authorization") - val memberId = authorization?.let { - AccessTokenResolver.resolve(it) - }.let { - tokenResolver.resolveId(it) - } ?: 0L + val memberId = userArgumentDetails.id.toLong() val useCaseOut = ReadWorkBookArticleUseCaseIn( workbookId = workbookId, diff --git a/api/src/main/kotlin/com/few/api/web/support/method/UserArgument.kt b/api/src/main/kotlin/com/few/api/web/support/method/UserArgument.kt new file mode 100644 index 00000000..1fd913a0 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/method/UserArgument.kt @@ -0,0 +1,5 @@ +package com.few.api.web.support.method + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class UserArgument \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentDetails.kt b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentDetails.kt new file mode 100644 index 00000000..2dfe9af7 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentDetails.kt @@ -0,0 +1,15 @@ +package com.few.api.web.support.method + +import com.few.api.security.authentication.token.TokenUserDetails +import org.springframework.security.core.GrantedAuthority + +class UserArgumentDetails( + val isAuth: Boolean, + authorities: List, + id: String, + email: String, +) : TokenUserDetails( + authorities = authorities, + id = id, + email = email +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentHandlerMethodArgumentResolver.kt b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentHandlerMethodArgumentResolver.kt new file mode 100644 index 00000000..e44f5f55 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/web/support/method/UserArgumentHandlerMethodArgumentResolver.kt @@ -0,0 +1,59 @@ +package com.few.api.web.support.method + +import com.few.api.security.authentication.authority.AuthorityUtils +import com.few.api.security.filter.token.AccessTokenResolver +import com.few.api.security.token.TokenResolver +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer + +@Component +class UserArgumentHandlerMethodArgumentResolver( + private val tokenResolver: TokenResolver, +) : HandlerMethodArgumentResolver { + val log = KotlinLogging.logger {} + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(UserArgument::class.java) + } + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory?, + ): UserArgumentDetails { + val authorization: String? = webRequest.getHeader("Authorization") + + val memberId = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveId(it) + } ?: 0L + + val email = authorization?.let { + AccessTokenResolver.resolve(it) + }.let { + tokenResolver.resolveEmail(it) + } ?: "" + + val authorities = authorization?.let { + AccessTokenResolver.resolve(it) + }?.let { + tokenResolver.resolveRole(it) + }?.let { + AuthorityUtils.toAuthorities(it) + } ?: emptyList() + + return UserArgumentDetails( + isAuth = authorization != null, + id = memberId.toString(), + email = email, + authorities = authorities + ) + } +} \ No newline at end of file