[java / kotlin] Spring boot 3.x + kotlin + Spring Security

[java / kotlin] Spring boot 3.x + kotlin + Spring Security

·

3 min read

들어가기 앞서
우연히 새로운 프로젝트를 만들 일이 생겨 관련 설정을 기록하고자 합니다.
개발환경은 아래와 같습니다.
- java (JDK 21, AWS Corretto)
- kotlin 1.9.22
- spring boot 3.2.3

진행하면서 변경될 부분은 몇가지 있지만 현재 기본 설정 기준으로 하면
- Database : H2 -> (Postgres) : 도커 hub에 관련 도커 올리고 나서 변경하고자 합니다.

현재 초기버전에 대한 기록을 작성합니다.

나중에 할일 / 고민은?
- UserDetails 연동 (DB 연동)
- Postgres DB 전환 테이블 설계
- Roles 설계
- 추가 API 설계
.....

Dependency

사용한 라이브러리는 아래와 같습니다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-jdbc")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-cache")

    implementation("com.github.ben-manes.caffeine:caffeine")

    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.h2database:h2")
    // https://mvnrepository.com/artifact/com.github.ulisesbocchio/jasypt-spring-boot-starter
    implementation("com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5")
    // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
    implementation("io.jsonwebtoken:jjwt-api:0.12.5")
    runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
    runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")
    implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4")
    implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
    // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
    implementation("org.apache.commons:commons-lang3:3.12.0")

    // https://mvnrepository.com/artifact/io.netty/netty-resolver-dns-native-macos
//    implementation("io.netty:netty-resolver-dns-native-macos:4.1.107.Final")
    //For mac M1 : https://github.com/netty/netty/issues/11020
    implementation(group= "io.netty",
            name="netty-resolver-dns-native-macos",
            version= "4.1.107.Final",
            classifier="osx-aarch_64")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
}
  1. org.springframework.boot
    - JPA, Security, web을 사용하였고 webflux (webclient), cache(caffeine cache) 사용을 위해 추가하였습니다.

  2. jasypt
    - 양방향 암호화를 위해 (yaml 암호화도) 사용

  3. jo.jsonwebtoken
    - json web token 사용을 위한 라이브러리

  4. io.netty
    - webclient https (ssl) 통신을 위한 라이브러리

Spring Security Config

설정된 코드는 아래와 같습니다.

@Configuration
class SecurityFilterChainConfig {
    private val allowedUrls = arrayOf(
            "/", "/swagger-ui/**", "/v3/api-docs/**", "/v1/user/signup", "/v1/user/login",
    )

    //CORS 설정
    fun corsConfigurationSource(): CorsConfigurationSource {
        return CorsConfigurationSource {
            val config = CorsConfiguration()
            config.allowedHeaders = Collections.singletonList("*")
            config.allowedMethods = Collections.singletonList("*")
//            config.setAllowedOriginPatterns(Collections.singletonList("http://localhost")) // client 존재시
            config.allowCredentials = true
            config
        }
    }

    @Bean
    fun filterChain(http: HttpSecurity, tokenProvider: JwtTokenProvider) = http
            .authorizeHttpRequests {
                it.requestMatchers(*allowedUrls).permitAll()
                        .requestMatchers(PathRequest.toH2Console()).permitAll()
                        .anyRequest().authenticated()
            }
            // h2 web console iframe 오류 해결(X-Frame-Options' to 'deny')
            .headers { a -> a.frameOptions { o -> o.disable() } }
            .csrf { csrf -> csrf.disable() }
            .cors { corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()) }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .addFilterBefore(JwtAuthorizeFilter(tokenProvider), UsernamePasswordAuthenticationFilter::class.java)
            .build()!!

    @Bean
    fun passwordEncoder() = BCryptPasswordEncoder()

    @Bean
    fun webSecurityCustomizer() = WebSecurityCustomizer { web: WebSecurity ->
        web.ignoring().requestMatchers(*allowedUrls)
    }

    //TODO : UserDetails (DB 연동 하는 부분은 생략하자~~~)
}
  1. permition All
    - 앞으로 만들 API 중 회원가입 / 로그인에 대한 API 모두 허용으로 설정하였습니다.
    - swagger 경로 허용
    - PathRequest.toH2Console() : H2 경로 허용
    - 이외 모든 경로는 인증인가 후 API 사용가능

Authorize Filter

시큐리티 설정에서 addFilterBefore에 추가되는 필터입니다.
해당 필터의 경우 UsernamePasswordAuthenticationFilter 보다 먼저 (before) 실행됩니다.

class JwtAuthorizeFilter(
        private val jwtTokenProvider: JwtTokenProvider
) : GenericFilterBean() {
    private val log = KotlinLogging.logger {}
    private val authorizationPrefix = "Bearer "

    @Throws(KsException::class)
    override fun doFilter(req: ServletRequest, res: ServletResponse, filterChain: FilterChain) {

        try{
            val token: String? = resolveToken(req as HttpServletRequest)
            log.info("Extracting token from HttpServletRequest: {}", token)

            if (token != null && jwtTokenProvider.validateToken(token)) {
                val auth: Authentication = jwtTokenProvider.getAuthentication(token)

                if (auth !is AnonymousAuthenticationToken) {
                    val context = SecurityContextHolder.createEmptyContext()
                    context.authentication = auth
                    SecurityContextHolder.setContext(context)
                }
            }
            filterChain.doFilter(req, res)
        }

        catch (e: KsException){
            log.error("#### KsException ::: $e")
            val json = ObjectMapper()
                    .writeValueAsString(e.ksResponse().toResponse())
            res.writer.write(json);
        }

        catch (e: Exception){
            log.error("#### Unchecked Exception ::: $e")
            val json = ObjectMapper()
                    .writeValueAsString(KsResponse.KS_SERVER_INTERNAL_ERROR.toResponse())
            res.writer.write(json);
        }

    }


    private fun resolveToken(request: HttpServletRequest): String? {
        val bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION)

        log.info("#### bearerToken=$bearerToken, prefix=$authorizationPrefix, key=${HttpHeaders.AUTHORIZATION}")

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(authorizationPrefix)) {
            return bearerToken.substring(7)
        }
        return bearerToken
    }
}

결과

서비스를 올리고 아래의 결과를 테스트해보도록 하겠습니다.

  1. 루트 경로 (/) : 차단

  1. web h2 console (/h2-console) : 허용

  1. swagger 경로 (/swagger-ui/index.html) : 허용
    - 따로 Swagger 설명은 기록 안해두었지만 (아직 API 도 못만들었네요..)
    dependency 에 org.spring.doc 추가되어있습니다.

이상 다음에는 앞서 말씀드린 할일중 일부분을 수정하고 포스트를 작성하도록 하겠습니다.