본문 바로가기

카테고리 없음

jwt설정, UnsatisfiedDependencyException 발생

@Component
class JwtPlugin(
    @Value("\${auth.jwt.issuer}") private val issuer: String,
    @Value("\${auth.jwt.secret}") private val secret: String,
    @Value("\${auth.jwt.accessTokenExpirationHour}") private val accessTokenExpirationHour: Long,
) {
    fun validateToken(jwt: String): Result<Jws<Claims>> {
        return kotlin.runCatching {
            val key = Keys.hmacShaKeyFor(secret.toByteArray(StandardCharsets.UTF_8))
            Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt)
        }
    }//

    fun generateAccessTokenForSocialUser(
        subject: String,
        nickname: String,
        email: String,
//        roles: List<String>, // 역할 정보 추가
        httpServletResponse: HttpServletResponse,
    ): String {
        val token = generateToken(subject, nickname, email,  Duration.ofHours(accessTokenExpirationHour))
        addTokenToCookie(token, httpServletResponse)
        return token
    }

    fun generateAccessToken(
        subject: String,
        nickname: String,
        email: String,
    ): String {
        return generateToken(subject, nickname, email, Duration.ofHours(accessTokenExpirationHour))
    }

    private fun generateToken(
        subject: String,
        nickname: String,
        email: String,
//        roles: List<String>, // 역할 정보 추가
        expirationPeriod: Duration,
    ): String {
        val claims: Claims = Jwts.claims()
            .add(mapOf(
                "email" to email,
                "nickname" to nickname,
//                "roles" to roles // 역할 정보를 클렘에 포함
            ))
            .build()

        val key = Keys.hmacShaKeyFor(secret.toByteArray(StandardCharsets.UTF_8))
        val now = Instant.now()

        return Jwts.builder()
            .subject(subject)
            .issuer(issuer)
            .issuedAt(Date.from(now))
            .expiration(Date.from(now.plus(expirationPeriod)))
            .claims(claims)
            .signWith(key)
            .compact()
    }

    private fun addTokenToCookie(
        token: String,
        httpServletResponse: HttpServletResponse,
    ) {
        val cookie = Cookie("jwt_token", token)
        cookie.isHttpOnly = false
        cookie.maxAge = (accessTokenExpirationHour * 60 * 60 * 24 * 7).toInt() // 약 8.4일
        cookie.path = "/"
        httpServletResponse.addCookie(cookie)
    }
}

이렇게 JwtPlugin을 만들고,

class JwtAuthenticationToken(
    private val token: String,
    private val email: String,
    private val nickname: String,
    authorities: Collection<GrantedAuthority>
) : AbstractAuthenticationToken(authorities) {

    override fun getCredentials(): Any {
        return token
    }

    override fun getPrincipal(): Any {
        return email
    }

    fun getNickname(): String {
        return nickname
    }
}
class JwtAuthenticationFilter(
    private val jwtPlugin: JwtPlugin
) : BasicAuthenticationFilter(null) {

    @Throws(IOException::class, ServletException::class)
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        chain: FilterChain
    ) {
        val header = request.getHeader("Authorization")

        if (header == null || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response)
            return
        }

        val token = header.replace("Bearer ", "")
        val result = jwtPlugin.validateToken(token)

        if (result.isSuccess) {
            val claims: Jws<Claims> = result.getOrNull() ?: return
            val body = claims.body
            val email = body["email"] as String
            val nickname = body["nickname"] as String

            // 필요한 권한 정보를 추출하여 GrantedAuthority 리스트를 만듭니다.
            val authorities = mutableListOf<GrantedAuthority>()
            // 예: roles를 클레임에서 추출하여 authorities에 추가
            // val roles = body["roles"] as List<String>
            // roles.forEach { role -> authorities.add(SimpleGrantedAuthority(role)) }

            val authentication = JwtAuthenticationToken(token, email, nickname, authorities)
            SecurityContextHolder.getContext().authentication = authentication
        }

        chain.doFilter(request, response)
    }
}

jwt를 제대로 작동시키려면 이런것들을 만들어 놔야 한다.

 

그리고 더 필요한것이

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val jwtAuthenticationFilter: JwtAuthenticationFilter,
    private val authenticationEntrypoint: AuthenticationEntryPoint
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .httpBasic { it.disable() }
            .formLogin { it.disable() }
            .csrf { it.disable() }
            .authorizeHttpRequests {
                it.requestMatchers(
                    "/login",
                    "/signup",
                    "/swagger-ui/**",
                    "/v3/api-docs/**"
                ).permitAll()
                    .anyRequest().authenticated()
            }
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
            // 예외처리
            .exceptionHandling{
                it.authenticationEntryPoint(authenticationEntrypoint)
            }
            .build()
    }

}

 

 

 

 

@Component
class CustomAuthenticationEntrypoint : AuthenticationEntryPoint {
    override fun commence(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authException: AuthenticationException,
    ) {
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        response.contentType = MediaType.APPLICATION_JSON_VALUE
        response.characterEncoding = "UTF-8"

        val objectMapper = ObjectMapper()
        val jsonString = objectMapper.writeValueAsString(Error("JWT verification failed"))
        response.writer.write(jsonString)
    }
}

CustomAuthenticationEntrypoint 클래스는 Spring Security에서 인증 과정 중 예외가 발생했을 때 실행되는 커스텀 엔트리 포인트입니다. 이 클래스는 AuthenticationEntryPoint 인터페이스를 구현하여, 인증 실패 시 클라이언트에게 반환할 응답을 정의합니다.

주요 역할은 다음과 같습니다:

인증 실패 응답 설정:

인증 과정에서 예외(예: JWT 검증 실패)가 발생하면, 이 클래스의 commence 메소드가 호출됩니다.
response.status = HttpServletResponse.SC_UNAUTHORIZED: HTTP 상태 코드를 401 (Unauthorized)로 설정합니다. 이는 인증이 실패했음을 나타냅니다.
response.contentType = MediaType.APPLICATION_JSON_VALUE: 응답의 콘텐츠 타입을 application/json으로 설정합니다. 클라이언트가 JSON 형식의 데이터를 기대하도록 합니다.
response.characterEncoding = "UTF-8": 응답의 문자 인코딩을 UTF-8로 설정합니다. 이는 다양한 문자 집합을 정확히 표현하기 위해 필요합니다.
예외 메시지 JSON 변환 및 응답 작성:

val objectMapper = ObjectMapper(): Jackson 라이브러리의 ObjectMapper 인스턴스를 생성합니다. 이 객체는 Java 객체를 JSON 문자열로 변환하는 데 사용됩니다.
val jsonString = objectMapper.writeValueAsString(Error("JWT verification failed")): Error 객체를 JSON 문자열로 변환합니다. 여기서 Error 클래스는 주어진 메시지를 포함하는 간단한 데이터 클래스입니다.
response.writer.write(jsonString): 변환된 JSON 문자열을 HTTP 응답 본문에 씁니다.
즉, CustomAuthenticationEntrypoint는 인증 실패 시 발생하는 예외를 처리하고, 클라이언트에게 명확한 JSON 형식의 에러 메시지를 반환하는 역할을 합니다

 

 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-05-20T16:17:31.084+09:00 ERROR 19448 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'adminController' defined in file [C:\Users\asdf\IdeaProjects\Board\build\classes\kotlin\main\org\example\domain\admin\controller\AdminController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'adminServiceImpl' defined in file [C:\Users\asdf\IdeaProjects\Board\build\classes\kotlin\main\org\example\domain\admin\service\AdminServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 2: Error creating bean with name 'jwtPlugin' defined in file [C:\Users\asdf\IdeaProjects\Board\build\classes\kotlin\main\org\example\domain\infra\jwt\JwtPlugin.class]: Unexpected exception during bean creation

 

이런오류가 떳는데, 이걸 문의해본결과 이런 대답이 왔다.

 

AdminController 빈을 생성하는 중에 오류가 발생했습니다.
이는 AdminController의 생성자에서 AdminServiceImpl 빈을 필요로 하기 때문입니다.
AdminServiceImpl 빈을 생성하는 중에 오류가 발생했습니다.
이는 AdminServiceImpl의 생성자에서 JwtPlugin 빈을 필요로 하기 때문입니다.
JwtPlugin 빈을 생성하는 중에 예기치 않은 예외가 발생했습니다.

 

즉 JwtPlugin 가 문제인 것이다.

 

지금은 UnsatisfiedDependencyException를 해결하기 위해 방법을 찾는 중이다