꿈틀꿈틀 개발일기

Spring | SecurityConfig, JwtPlugin

by jeongminy

1. Spring Security, JWT 의존성 추가

implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")

runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")

 

 

2. Spring Security 를 추가하면 15개의 필터가 기본값으로 적용 된다.

[org.springframework.security.web.session.DisableEncodeUrlFilter@6bf27411, 
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@4a66949a, 
org.springframework.security.web.context.SecurityContextHolderFilter@2eef43f5, 
org.springframework.security.web.header.HeaderWriterFilter@6a2d0a19, 
org.springframework.security.web.csrf.CsrfFilter@324afa73, 
org.springframework.security.web.authentication.logout.LogoutFilter@6792aa3e, 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@20673498, 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@46c28400, 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@57e83608, 
org.springframework.security.web.authentication.www.BasicAuthenticationFilter@4b4b02d, 
org.springframework.security.web.savedrequest.RequestCacheAwareFilter@6c65a7fc, 
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@3b09582d, 
org.springframework.security.web.authentication.AnonymousAuthenticationFilter@57d8d8e2, 
org.springframework.security.web.access.ExceptionTranslationFilter@4a4979bf, 
org.springframework.security.web.access.intercept.AuthorizationFilter@7a14d8a4]

 

주요 필터들에 대한 설명

  • SecurityContextHolderFilter
      - SecurityContext 객체를 생성, 저장, 조회하는 역할을 합니다! 없어서는 안되겠죠!
  • CsrfFilte
      - CSRF 공격을 막는 필터입니다. CSRF공격은 유저가 특정 이미지, url등을 클릭할때 링크 또는 스크립트를 사용하여 서버측으로 의도치않은 HTTP요청을 전송하게 하여 정보를 빼내거나 조작하는 공격이에요! 일반적으로 쿠키와 세션 기반으로 인증을 진행할때, 많이 일어날 수 있는 공격인데요, 우리의 경우 Rest API를 통해 stateless하게 서버를 유지하고, JWT를 사용하기 때문에, 꺼 놓아도 괜찮은 필터에요.
  • LogoutFilter
        - 설정된 로그아웃 URL(default: `/logout`)로 요청이 들어오면, 세션을 무효화하고, 쿠키를 삭제하고, SecurityContext를 비우는 역할을 합니다.
  • UsernamePasswordAuthenticationFilter
        - 설정된 로그인 URL(default: `/login` )로 요청이 들어왔을때, username과 password를 비교하여 실제 인증을 수행하는 Filter입니다. 마무리 되면, SecurityContext에 인증에 성공한 `Authentication` 객체가 저장되겠죠!
  • DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter
        - 이름 그대로 로그인, 로그아웃 페이지를 띄우는 필터에요! 여러분이 본 로그인 페이지가 이 필터들로 인해 떳다고 보면 됩니다 🙂
  • SecurityContextHolderAwareRequestFilter
        - SecurityContext에 저장된 정보로 부터 Request를 구성해주는 필터입니다.
  • AnonymousAuthenticationFilter
        - 익명 사용자에 대한 인증 처리 필터입니다. 앞단의 필터에서 SecurtiyContext에 `Authentication` 객체가 없으면, `Authentication` 의 구현제 중 하나인 `AnonymousAuthenticationToken` 을 SecurityContext에 설정합니다.
  • ExceptionTranslationFilter
        - AccessDeniedException(인가예외)와 AuthenticationException(인증예외)을 처리하는 필터입니다.
  • AuthorizationFilter
        - 권한을 확인하는 필터입니다.

 

3. SecurityConfig 작성

  • 필요없는 필터 4개를 꺼준다.
    BasicAuthenticationFilter,
    UsernamePassworedAuthenticationFilter, 
    DefaultLoginPageGeneratingFilter, 
    DefaultLogoutPageGeneratingFilter
@Configuration
@EnableWebSecurity  //http관련 보안 기능을 설정 하기 위해
class SecurityConfig {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
    //filterChain 함수는 Spring Security에서 사용되는 SecurityFilterChain 객체를 생성하는 함수입니다.
    //이 함수는 http라는 HttpSecurity 객체를 매개변수로 받아와서 SecurityFilterChain을 구성하고 반환합니다.
        return http
            .httpBasic { it.disable() } // BasicAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter 제외
            .formLogin { it.disable() } // UsernamePassworedAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter 제외
            .csrf { it.disable() } // CsrfFilter 제외
            .build()
    }

}

 

 

 

4. JWT 구현하기 -> JwtPlugin 작성

JwtPlugin 클래스의 역할

  • JWT를 검증할 수 있어야 한다.
  • JWT를 생성할 수 있어야 한다.
  • 대칭키 알고리즘인 HS256을 사용할 예정이다.
  • iss, exp, sub, role, email을 설정할 것이다.
  • issuer, secret, accessTokenExpirationHour는 application.yml에 따로 빼놓고, 생성자 주입을 하는게 좋다.
package com.teamsparta.courseregistration.infra.security.jwt

import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jws
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.time.Instant
import java.util.*

@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>>{
        //Result는 Kotlin 표준 라이브러리에서 제공하는 클래스로, 작업 수행 중에 예외가 발생할 수 있는 경우 예외 처리를 보다 편리하게 할 수 있도록 도와줍니다.
        // Result는 작업의 성공 또는 실패를 나타내는 Success와 Failure 두 가지 하위 클래스를 가지고 있습니다.

        return kotlin.runCatching { //runCatching 함수를 사용하여 예외가 발생할 수 있는 부분을 감싸고 있습니다. //try-catch 구문을 좀더 우아하게 표현함! (예외를 처리)
            val key = Keys.hmacShaKeyFor(secret.toByteArray(StandardCharsets.UTF_8))
            //이 코드는 secret라는 문자열로 표현된 비밀 키를 HMAC-SHA 알고리즘에 사용할 수 있는 형태의 키로 변환하는 역할을 합니다.
            //여기서 secret는 문자열로 표현된 비밀 키입니다. toByteArray 메서드를 사용하여 secret 문자열을 바이트 배열로 변환합니다.
            //이때 StandardCharsets.UTF_8을 인코딩으로 사용하여 문자열을 바이트 배열로 변환합니다. 인코딩은 문자열을 바이트로 변환하는 과정에서 문자 인코딩 방식을 지정하는 것입니다. UTF-8은 유니코드 문자 인코딩 방식 중 하나로, 대부분의 문자를 효율적으로 표현할 수 있는 방식입니다.
            //secret.toByteArray(StandardCharsets.UTF_8)를 통해 얻어진 바이트 배열은 HMAC-SHA 알고리즘에서 사용할 수 있는 형태의 키입니다. Keys.hmacShaKeyFor 메서드는 이 바이트 배열을 HMAC 키로 변환하여 key 변수에 할당합니다.
            //즉, Keys.hmacShaKeyFor 메서드는 주어진 문자열로 표현된 비밀 키를 UTF-8 인코딩을 사용하여 바이트 배열로 변환한 뒤, 그 바이트 배열을 HMAC 키로 변환하여 반환합니다. 이렇게 생성된 key는 HMAC-SHA 알고리즘에서 JWT의 서명 검증에 사용됩니다.

            Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt)
            //Jwts.parser(): JWT를 파싱하기 위한 JWT 파서를 생성합니다.
            //.verifyWith(key): 생성된 JWT 파서에 서명 검증에 사용할 키를 설정합니다. 앞서 생성한 key 변수가 여기에 사용됩니다. 이렇게 설정된 키를 사용하여 JWT의 서명을 검증합니다.
            //.build(): JWT 파서를 빌드하여 JWT 파싱 및 검증을 수행할 준비를 합니다.
            //.parseSignedClaims(jwt): 준비된 JWT 파서를 사용하여 주어진 JWT를 파싱하고 서명을 검증합니다. jwt는 검증할 JWT를 의미합니다. 이 과정에서 JWT의 헤더와 페이로드를 추출하고, 서명을 검증하여 토큰의 무결성을 확인합니다. 검증이 성공하면 JWT의 클레임(claim)을 반환합니다. 클레임은 토큰에 포함된 정보를 나타내며, 검증된 토큰의 내용을 확인할 수 있습니다.
            //즉, 주어진 코드는 key를 사용하여 JWT를 파싱하고 서명을 검증하는 과정을 수행합니다. 이를 통해 JWT의 무결성을 확인하고, 클레임을 추출하여 토큰에 포함된 정보를 확인할 수 있습니다.
        }
    }

    //토큰(Access Token)을 생성하는 역할
    fun generateAccessToken(subject: String, email: String, role: String): String {
        return generateToken(subject, email, role, Duration.ofHours(accessTokenExpirationHour))
    }


    //토큰(Access Token)을 실질적으로 생성하는 역할
    private fun generateToken(subject: String, email: String, role: String, expirationPeriod: Duration): String {
        val claims: Claims = Jwts.claims() //JWT의 클레임(claim)을 생성하기 위한 메서드입니다. 이 메서드를 호출하면 클레임을 생성할 준비가 됩니다.
            .add(mapOf("role" to role, "email" to email)) //생성된 클레임에 키-값 쌍 형태의 정보를 추가합니다. 여기서는 "role"과 "email"이라는 키에 해당하는 값을 추가하고 있습니다. role과 email은 메소드의 인자로 전달된 값입니다.
            .build()

        val key = Keys.hmacShaKeyFor(secret.toByteArray(StandardCharsets.UTF_8))
        //Keys.hmacShaKeyFor 메소드는 HMAC 알고리즘을 사용하는 키를 생성하는데, 주로 서명에 활용됨.

        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() //모든 설정이 완료된 후에 토큰을 생성하고, 이를 문자열로 변환합니다.

        //이렇게 설정된 값들은 JWT의 헤더(Header), 페이로드(Payload), 서명(Signature)에 해당함.
    }
}

 

 

 

 

'대칭키 알고리즘'과 '비대칭키 알고리즘'의 차이점

  • 공통점: 대칭키 알고리즘과 비대칭키 알고리즘은 모두 암호화와 복호화를 위한 방법이다.

  • 대칭키 알고리즘 -> 우리는 대칭키 알고리즘인 HS256 을 사용할 예정
    이 방법은 암호화와 복호화에 동일한 키를 사용합니다. 이것이 '대칭'이라는 용어의 출처입니다. 이 방법의 장점은 과정이 빠르고 단순하다는 것입니다. 하지만, 키를 안전하게 전달하는 것이 어렵기 때문에 키 관리가 주요 문제가 될 수 있습니다. 
    예를 들어, 이 키는 우리가 서로 비밀을 공유하려면 우편으로 편지를 보내는 것과 같습니다. 우리 모두가 그 편지의 내용을 읽을 수 있는 열쇠를 가지고 있지만, 중간에 누군가가 편지를 가로채면 그 내용이 노출될 수 있습니다.

  • 비대칭키 알고리즘
    이 방법은 암호화와 복호화에 다른 키를 사용합니다. 하나는 공개키로, 누구나 암호화에 사용할 수 있습니다. 다른 하나는 개인키로, 오직 복호화에만 사용됩니다. 이 방법의 장점은 키를 안전하게 교환할 필요가 없다는 것입니다. 하지만, 대칭키 알고리즘이 비교적 빠르기 때문에 비대칭키 알고리즘은 보통 더 느립니다. 
    이를 비유하면, 개인키는 우리가 집에 있는 금고와 같습니다. 누구나 공개키를 사용하여 금고에 무언가를 넣을 수 있지만, 오직 우리만이 개인키를 사용하여 그것을 꺼낼 수 있습니다.

  • 요약하자면, 대칭키 알고리즘은 키 교환 문제를 가지고 있지만 빠르고 효율적이며,
    비대칭키 알고리즘은 안전한 키 교환을 제공하지만 상대적으로 느리다는 특징이 있습니다.

 

 

HS256

HS256은 HMAC-SHA256의 줄임말로, 암호화에 사용되는 알고리즘 중 하나입니다. HMAC은 해시 기반 메시지 인증 코드(Hash-based Message Authentication Code)의 약자로, 해시 알고리즘에 기반한 특정 유형의 메시지 인증 코드(MAC)를 생성하는 데 사용됩니다.

HS256은 대칭키 알고리즘을 사용합니다. 즉, 암호화와 복호화에 동일한 키를 사용하며, 이 키를 안전하게 보관해야 합니다. HS256은 JWT(JSON Web Token)에서 자주 사용되는 알고리즘 중 하나로, 토큰을 생성하고 검증하는 데 사용됩니다.

HS256 알고리즘의 주요 특징은 다음과 같습니다:

  • 안전성: SHA-256은 충돌 방지 기능이 있어 안전성이 높습니다. 이는 동일한 해시 값을 생성하는 두 개의 다른 입력 값을 찾는 것이 매우 어렵다는 것을 의미합니다.
  • 속도: HMAC 알고리즘은 비대칭키 알고리즘보다 일반적으로 더 빠르므로, 성능이 중요한 시스템에서 자주 사용됩니다.
  • 단순성대칭키 알고리즘은 비대칭키 알고리즘에 비해 구현이 더 간단합니다.
    단, 대칭키 알고리즘이기 때문에 키 관리에 주의를 기울여야 합니다. 키가 노출되면, 악의적인 사용자가 해당 키를 사용하여 유효한 토큰을 생성하거나 기존 토큰을 변조할 수 있습니다.

-> 256bit 즉, 32byte(32글자) 이상이 되어야 함

 

 

 

Registered Claim

JWT(JSON Web Token)에서 사용되는 'Registered Claim'은 토큰에 포함된 정보 중 일부로, 토큰이 어떻게 사용되어야 하는지를 명시하는 데 도움을 주는 표준화된 필드를 의미합니다. 이러한 클레임은 선택적으로 포함될 수 있지만, 포함되면 JWT를 처리하는 모든 시스템이 이해할 수 있도록 표준화되어 있습니다.

Registered Claim에는 다음과 같은 종류가 있습니다:

"iss" (Issuer): 토큰을 발행한 발행자를 의미합니다.
"sub" (Subject): 토큰이 관련된 주체를 의미합니다. 보통 사용자 ID나 이메일 주소 등이 될 수 있습니다.
"aud" (Audience): 토큰이 의도된 수신자를 의미합니다.
"exp" (Expiration Time): 토큰의 만료 시간을 나타냅니다. 이 시간 이후에는 토큰이 더 이상 유효하지 않습니다.
"nbf" (Not Before): 이 시간 이전에는 토큰이 아직 유효하지 않다는 것을 의미합니다.
"iat" (Issued At): 토큰이 발행된 시간을 나타냅니다.
"jti" (JWT ID): 토큰에 대한 고유 식별자로, 중복 처리를 방지하는 데 사용될 수 있습니다.

이러한 Registered Claim들은 JWT를 사용하는 시스템 간에 정보를 일관되게 전달하는 방식을 제공하며, 토큰의 유효성 검사 및 사용 방법을 정의하는 데 도움이 됩니다.

 

 

Custom Claim

Custom Claim은 JWT(JSON Web Token)에서 사용자가 정의한 추가 정보를 나타냅니다. 이는 JWT의 유연성을 보여주는 좋은 예시로, 사용자는 필요에 따라 원하는 어떠한 데이터든 Custom Claim으로 추가할 수 있습니다.

예를 들어, 사용자의 역할이나 권한, 선호하는 언어 등의 추가 정보를 Custom Claim으로 설정할 수 있습니다. 이 정보는 서버에서 클라이언트로 토큰을 전송할 때 함께 전송되며, 클라이언트는 이 토큰을 통해 서버로부터 수신된 정보를 인증하고 검증할 수 있습니다.

 

그러나 Custom Claim을 사용할 때 주의해야 할 몇 가지 사항이 있습니다

  • 정보 노출: 토큰은 보통 클라이언트와 서버 사이에서 전송되므로, 민감한 정보는 Custom Claim에 포함되어서는 안 됩니다. JWT는 누군가가 복호화하지 않고도 내용을 읽을 수 있도록 Base64로 인코딩되기 때문입니다.
  • 크기 제한: JWT는 HTTP 헤더를 통해 전송되므로, 토큰의 크기는 제한적입니다. 너무 많은 Custom Claim을 추가하면 토큰의 크기가 커져 전송 시 문제가 발생할 수 있습니다.

따라서 Custom Claim은 꼭 필요한 정보만을 포함하도록 하고, 이 정보가 민감한 정보를 포함하지 않도록 주의해야 합니다.

 

 

 

블로그의 정보

꿈틀꿈틀 개발일기

jeongminy

활동하기