들어가기 앞서
우연히 새로운 프로젝트를 만들 일이 생겨 관련 설정을 기록하고자 합니다.
개발환경은 아래와 같습니다.
- 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")
}
org.springframework.boot
- JPA, Security, web을 사용하였고 webflux (webclient), cache(caffeine cache) 사용을 위해 추가하였습니다.jasypt
- 양방향 암호화를 위해 (yaml 암호화도) 사용jo.jsonwebtoken
- json web token 사용을 위한 라이브러리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 연동 하는 부분은 생략하자~~~)
}
- 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
}
}
결과
서비스를 올리고 아래의 결과를 테스트해보도록 하겠습니다.
- 루트 경로 (/) : 차단
- web h2 console (/h2-console) : 허용
- swagger 경로 (/swagger-ui/index.html) : 허용
- 따로 Swagger 설명은 기록 안해두었지만 (아직 API 도 못만들었네요..)
dependency 에 org.spring.doc 추가되어있습니다.
이상 다음에는 앞서 말씀드린 할일중 일부분을 수정하고 포스트를 작성하도록 하겠습니다.