앞에서 게이트웨이에서 하기 좋은게 인증이라고 했다. 사실 다른 서킷브레이커 등등도 있지만 기본적으로 이것 먼저 하게 된다. jwt인증을 위한 글로벌 필터를 만들어 보자.
일단 설정
@Bean
fun customRouteLocator(builder : RouteLocatorBuilder): RouteLocator {
return builder.routes {
route(id = "auth-service") {
path("/user/auth/**")
.filters {
f -> f.filter(authFilterFactory.apply(AuthFilterFactory.Config()))
}
uri("http://localhost:8080")
}
route(id = "mall-service") {
path("/mall/**")
.filters {
f -> f.filter(authFilterFactory.apply(AuthFilterFactory.Config()))
}
uri("http://localhost:8082/mall")
}
}
}
여기서 authFilterFactory를 지나면서 인증을 시도할 것이다. 저기서 나중에 고쳐볼 부분은 uri가 /mall이 아니면 안되는데 이걸 나중에는 도메인형식으로가게끔 해야할 것이다. 또한 예전에 netflix zuul을 사용하면서 실수했던게 Access-control-allow-origin 이 response로 넘길 떄 두 번 더해지는 경우가 있다 서버를 두개를 거치기 때문에 따라서 하나를 ignore를 시키는 작업이 있어야 하는데 나중에 찾아보길 바란다. netflix zuul에서도 옵션이 있었던 것으로 안다.
authFilterFactory 에서 토큰검증이다. 세세한 코드는 알아서 짜보시라. 이렇게 해서 헤더에 변환해서 추가로 더해준다. 추가적으로 사실 특정 도메인만 열어놓으면 되지만 못 믿겠어서 secureHeaderValue라고 해서 내가 만들어 놓은 헤더를 보내주고 안쪽에서 받을 때 체크하게 될 것이다.
@Component
class AuthFilterFactory (
private val tokenProvider: TokenProvider
) : AbstractGatewayFilterFactory<AuthFilterFactory.Config>() {
@Value("secure-header-value")
private val secureHeaderValue : String = "";
class Config
private fun resolveToken(headerVal: List<String>?): String? {
if (headerVal.isNullOrEmpty()) {
return null
}
val strToken = headerVal[0]
return if (StringUtils.hasText(strToken) && strToken.startsWith("Bearer ")) {
strToken.substring(7)
} else null
}
override fun apply(config: Config): GatewayFilter {
return GatewayFilter { exchange: ServerWebExchange, chain: GatewayFilterChain ->
val request = exchange.request
val headers = request.headers
val headerVal = headers[HttpHeaders.AUTHORIZATION]
val token = resolveToken(headerVal)
if(!token.isNullOrBlank()) {
val tokenInfo = tokenProvider.getUserIdAndAuthorityByJwtAccessToken(token)
if (tokenInfo.userId != 0L) {
request.mutate().header(SecurityConst.getAuthoritiesHeaderName(), tokenInfo.authorities).build()
request.mutate().header(SecurityConst.getUserIdHeaderName(), tokenInfo.userId.toString()).build()
request.mutate().header(SecurityConst.getSecureHeaderName(), secureHeaderValue).build()
}
}
chain.filter(exchange.mutate().request(request).build())
}
}
}
각각의 서비스에서 넣어줄 인증필터이다. 인증필터라기는 뭐하고 이런식의 공통 필터로 spring security의 내용들을 사용할 수 있도록 Autentication을 저장하는 코드이다. (참고로 나같은 경우는 이것을 따로 모듈화 해서 관리하고 이것을 붙여서 쓴다.)
@Throws(IOException::class, ServletException::class)
override fun doFilter(servletRequest: ServletRequest, servletResponse: ServletResponse?, filterChain: FilterChain) {
val httpServletRequest = servletRequest as HttpServletRequest
if (secureHeaderValue != httpServletRequest.getHeader(SecurityConst.getSecureHeaderName())) {
throw CustomBadRequest(GlobalErrorCode.NOT_VALID_SECURE_HEADER, GlobalErrorCode.NOT_VALID_SECURE_HEADER.getMessage())
}
val userId = httpServletRequest.getHeader(SecurityConst.getUserIdHeaderName());
val authorityString = httpServletRequest.getHeader(SecurityConst.getAuthoritiesHeaderName());
if(userId.isNotBlank() && authorityString.isNotBlank()) {
this.setAuthentication(userId.toLong(), authorityString)
}
filterChain.doFilter(servletRequest, servletResponse)
}
private fun setAuthentication(userId : Long, authority: String) {
val authorities = Arrays.stream(
authority.split(",".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray())
.map {
a -> SimpleGrantedAuthority(a)
}
.collect(Collectors.toList())
val principal = User(userId.toString(), "", authorities)
val authentication: Authentication =
UsernamePasswordAuthenticationToken(principal, "", authorities)
SecurityContextHolder.getContext().authentication = authentication
}
이걸로 중요 부분은 끝났다. 세세한 부분은 jwt 등을 참고하여 개발하길 바란다. 해피한 개발 되길 빈다!