밸브와 필터
본 장에서는 JEUS의 밸브와 필터를 설정하는 방법을 확인한다.
1. 개요
밸브는 Tomcat에서 가져온 개념으로 요청을 검사 혹은 변경을 위해 추가하는 구성요소를 의미한다. 밸브의 설정은 웹엔진, 가상호스트, 컨텍스트 단위로 설정할 수 있다. 필터는 javax.servlet.Filter를 의미하며, 밸브와 달리 서블릿 단위로 설정할 수 있다. 자세한 내용은 javax.servlet.Filter 참고한다.
2. 밸브의 개념
본 절에서는 밸브에 대한 기본적인 개념과 사용법에 대해 설명한다.
밸브는 서블릿의 필터 개념을 서버와 가상호스트 레벨로 확장시킨 개념이다. 웹 엔진, 가상 호스트, 컨텍스트 순서로 밸브 코드를 실행시키게 되며, 같은 단위 내의 호출 순서는 설정에 지정한 순서대로 시행된다. 필터와 밸브가 모두 설정되어 있는 경우에는 밸브의 설정 모두 적용된 이후에 필터가 적용되게 된다.
jeus-web-dd.xml 설정
WEB-INF/ 디렉터리 아래에 jeus-web-dd.xml을 생성한다. 이와 관련된 자세한 설명은 웹 컨텍스트를 참고한다.
다음은 jeus-web-dd.xml 파일의 예이다. 몇몇 항목들은 생략되어 있다. 생략된 항목들은 "JEUS XML Reference"의 "13. jeus-web-dd.xml 설정"을 참고한다.
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="21">
<context-path>/examples</context-path>
<pipeline>
<valve>
<class-name>jeus.servlet.valve.RemoteAddressValve</class-name>
<property>
<key>deny</key>
<value>127\.0\.0\.1</value>
</property>
<property>
<key>denyStatus</key>
<value>403</value>
</property>
</valve>
</pipeline>
</jeus-web-dd>
다음은 domain.xml의 web-engine에 valve를 설정하는 예시이다. 관련 없는 설정은 생략했다.
<domain xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="21">
<web-engine>
<pipeline>
<valve>
<class-name>jeus.servlet.valve.RemoteAddressValve</class-name>
<property>
<key>deny</key>
<value>127\.0\.0\.1</value>
</property>
<property>
<key>denyStatus</key>
<value>403</value>
</property>
</valve>
</pipeline>
</web-engine>
</domain>
다음은 설정 태그에 대한 설명이다.
태그 | 설명 |
---|---|
<valve> |
사용할 valve를 설정한다. |
<class-name> |
적용할 밸브의 클래스 명이다. 필터의 class-name을 생각하면 된다. |
<property> |
밸브에 적용할 프로퍼티이다. 이 프로퍼티는 key value 쌍으로 구성되어 있고, 이는 밸브 내부의 파라미터 맵에 저장된다. 여러개 설정할 수 있다. |
<key> |
valve 객체 내의 파라미터맵에서 사용할 key를 의미한다. 맵 내에선 유일한 값이고, 중복되는 경우 나중에 설정된 key의 value값이 저장된다. |
<value> |
valve 내부의 파라미터 맵에 저장될 값이다. String으로 저장되며, 타입 캐스팅은 후술할 init() 메소드에서 진행한다. |
class-name에 들어갈 class는 jeus.servlet.valve.ValveBase 클래스를 상속받아야 한다. |
3. JEUS 제공 valve 설정
JEUS에서는 이용자 편의를 위해 밸브 구현체를 제공한다.
3.1. 메소드 제한 옵션 설정
method를 검사해서 특정 메소드를 막거나 허용하는 옵션을 의미한다.
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="21">
<context-path>/examples</context-path>
<pipeline>
<valve>
<class-name>jeus.servlet.valve.MethodConstraintValve</class-name>
<property>
<key>allow</key>
<value>GET,POST</value>
</property>
</valve>
</pipeline>
</jeus-web-dd>
JEUS가 제공하는 메소드 제약 옵션에 사용할 클래스 이름은 jeus.servlet.valve.MethodConstraintValve이다. key로 allow와 deny를 줄 수 있으며, value로 메소드 목록을 주면 된다. 이 때, 각 http 메소드를 구분하는 구분자는 콤마( , )이다.
3.2. remote host/adderss valve 설정
원격 호스트/주소를 허용할지 말지 결정하는 옵션을 제공한다.
차단하고자 하면 key에 deny를, 허용하고자 하면 key에 allow를 적는다. 이 때 대소문자를 구분하므로 주의해서 key를 주도록 한다. value에는 차단, 혹은 허용하고자 하는 remote address나 remote host를 적는다. 여기서 remote host의 값은 servletRequest 객체에서 getRemoteHost() 메소드를 호출했을 때 리턴되는 값을 사용한다. 이에 대한 자세한 내용은 https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/servletrequest#getRemoteHost--를 참조한다.
value에 줄 문자열은 Java 정규표현식을 따른다. 정규표현식 규칙은 java.util.regex 패키지의 규칙을 따른다. denyStatus는 요청을 막는 경우에 적용할 status 코드이다. 설정하지 않은 경우의 기본값은 403이다.
아래의 예시는 로컬에서의 접근만 허용하는 예제이다.
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="21">
<context-path>/examples</context-path>
<pipeline>
<valve>
<class-name>jeus.servlet.valve.RemoteAddressValve</class-name>
<property>
<key>allow</key>
<value>127\.0\.0\.1</value>
</property>
<property>
<key>denyStatus</key>
<value>403</value>
</property>
</valve>
</pipeline>
</jeus-web-dd>
아래 예제는 remote host valve의 사용 예제이다. remote host의 hostname이 www.tamx.co.kr인 경우 접근을 거부하는 예제이다.
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="21">
<context-path>/examples</context-path>
<pipeline>
<valve>
<class-name>jeus.servlet.valve.RemoteHostValve</class-name>
<property>
<key>deny</key>
<value>www\.tmax\.co\.kr</value>
</property>
</valve>
</pipeline>
</jeus-web-dd>
key와 value는 대소문자를 구분하기 때문에 옵션을 줄 때 주의하도록 한다. 또한, deny와 allow를 동시에 주지 않도록 주의한다. |
3.3. URL rewrite 옵션 설정
URL을 재작성하기 위해 설정하는 밸브를 의미한다. Tomcat의 RewriteValve를 참고해 구현했고, 같은 규칙을 따른다. url rewrite 기능의 목적은 외부에 자신이 설정한 도메인이 노출되지 않게 하거나, 다른 곳으로 redirect하게 하는 목적이 있다. rewrite 규칙은 rewrite.config란 파일에 별도로 작성한다. rewrite 규칙에 대한 자세한 내용은 https://tomcat.apache.org/tomcat-8.5-doc/rewrite.html를 참조한다.
설정은 web-engine 및 context 단위로 할 수 있다. web-engine 단위로 하는 경우에는 DOMAIN/config에 한다. app 단위로 하는 경우에는 APP_HOME/WEB-INF 에 파일을 위치시키면 된다. JEUS가 제공하는 url rewrite 옵션을 적용하기 위한 클래스 이름은 jeus.servlet.valve.rewrite.RewriteValve이다.
<jeus-web-dd xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="21">
<context-path>/examples</context-path>
<pipeline>
<valve>
<class-name>jeus.servlet.valve.rewrite.RewriteValve</class-name>
<property>
<key>encoding</key>
<value>utf-8</value>
</property>
</valve>
</pipeline>
</jeus-web-dd>
key로 encoding을 줄 수 있으며 url rewrite 시에 사용할 인코딩을 지정한다. 지정하지 않는다면 JEUS의 요청 쿼리 인코딩을 적용하도록 되어 있다. JEUS의 인코딩과 rewrite.config의 인코딩, RewriteValve의 인코딩은 일치해야 한글 사용시 정상적으로 사용 가능하다.
JEUS의 query encoding은 다음과 같은 우선순위에 따라 결정된다.
-
domain.xml 내의 forced url encoding 설정
-
forced request encoding 설정
-
client override encoding 설정
-
url mapping 의 encoding 설정
-
default encoding 설정
-
아무것도 설정되어있지 않은 경우엔 ISO-8859-1
절대 jeus.servlet.request.6CompatibleSetCharacterEncoding 옵션과 함께 사용해선 안된다. 해당 설정은 하위 호환성을 위해 제공하는 옵션으로 스펙과 다르게 동작하도록 만들어 인코딩 우선순위를 엉망으로 만든다. |
4. 사용자 정의 밸브
JEUS 개발자 또한 밸브를 상속받아 구현할 수 있다.
밸브를 추가하기 위해선 JEUS의 jeus.servlet.valve.ValveBase를 상속 받아 구현한다. ValveBase를 상속받아 사용하기 위해선 JEUS_HOME/lib/system의 javaee.jar와 jeus-servlet-engine.jar 라이브러리가 필요하다. 빌드한 이후에는 jar로 패키징해 JEUS_HOME/lib/thirdparty에 추가하면 된다. 이 때, jar파일 내에 javaee.jar와 jeus-servlet-engine.jar가 들어가지 않도록 주의한다.
아래는 모든 http response header에 test test를 추가해서 내보내는 valve의 예제이다.
package com.test.valve;
import jeus.servlet.valve.ValveBase;
import jeus.servlet.valve.ValveException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
public class TestValve extends ValveBase {
private String test;
@Override public void init() throws ValveException {
Map<String,String> paramMap = getValveParameterMap();
test = paramMap.get("test");
}
@Override public void invoke(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
//어플리케이션 실행 전
getNext().invoke(req, resp);
//어플리케이션 실행 후
resp.setHeader(test,test);
}
}
init() 메소드의 경우에는 context 단위에 설정된 경우 deploy될 때 domain.xml의 경우 가상호스트나 웹 엔진이 시작할 때 호출된다. getValveParameter() 메소드는 jeus-web-dd.xml이나 domain.xml에서 설정한 property의 map을 가져오는 메소드이다. invoke 메소드의 경우에는 요청이 들어왔을 때의 valve의 동작을 정의한다. 기본적인 구조는 서블릿 필터와 유사하다. getNext().invoke() 메소드는 필터 구현에서의 chain.doFilter() 메소드와 같은 역할을 한다. getNext().invoke() 메소드 호출 이전에는 request를 이용해 필터 작업을 수행하고, response를 통해 응답의 필터링 작업을 실행한다.
JEUS_HOME/docs/api/jeus-servlet의 jeus.servlet.valve.ValveBase에 대한 설명이 있다.
init() 메소드에서 아무런 동작을 하지 않더라도 구현은 해야 한다. invoke() 메소드 또한 valve의 동작을 정의하기 때문에 구현해야 한다. 또한, invoke 구현시에 getNext().invoke()메소드를 호출하는 것을 잊어선 안된다. 요청의 처리는 항상 JEUS에서 정의한 마지막 밸브에서 하기 때문에, 다음 밸브로 넘어가지 못하는 경우에는 요청을 처리하지 못한다. |
5. JEUS 제공 filter 설정
JEUS에서는 이용자 편의를 위해 필터 구현체를 제공한다.
5.1. samesite 제한 옵션 설정
samesite는 context 별로 설정할 수 있지만, user-agent를 식별해 다르게 동작할 수는 없다. 특정 클라이언트에서는 samesite를 처리하지 못하는 경우가 있어, JEUS에서 user-agent를 식별해 samesite를 삭제하는 필터를 제공한다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd">
<filter>
<filter-name>NoSameSiteFilter</filter-name>
<filter-class>jeus.servlet.filters.NoSameSiteFilter</filter-class>
<init-param>
<param-name>agent</param-name>
<param-value>^Chrome/\w$</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>NoSameSiteFilter</filter-name>
<servlet-name>NoSameSiteServletTest</servlet-name>
</filter-mapping>
</web-app>
JEUS가 제공하는 samesite 제한 옵션에 사용할 클래스 이름은 jeus.servlet.filters.NoSameSiteFilter이다. init-param에 name에는 agent로 설정해 주어야 하며, value로 samesite를 삭제하고자 하는 user-agent의 정규 표현식으로 표현한다.
5.2. Forwarded Header 옵션 설정
RFC 7239에 정의되어 있는 Forwarded Header와 de facto("사실 상 표준") 헤더들을(ex. X-Forwarded)를 식별하여, javax.servlet.HttpServletRequest API에서 proxy가 아닌 client 정보를 얻어 올 수 있도록 하는 필터를 제공한다.
필터 클래스 이름은 jeus.servlet.filters.ForwardedFilter이고, 필터를 추가하면 자동으로 forwarded 헤더를 찾고 적용한다. 하지만, forwarded는 스펙상 "for, host, proto, by" 4가지만 찾아서 적용한다. 자세한 내용은 RFC 7239를 참고한다.
de facto 헤더도 같이 고려하기 위해서는 필터의 InitParamter로 찾고자 하는 헤더를 적용하면 된다. InitParameter의 key는 총 6가지가 있는데, Forwarded-For, Forwarded-Host, Forwarded-Proto, Forwarded-By, Forwarded-Port, Forwarded-Server 가 있다. InitParameter value에는 적용하고자 하는 헤더들을 콤마(,)로 구분해서 넣으면 된다. 예를 들어, 아래와 같이 정의할 수 있다.
<filter>
<filter-name>ForwardedFilter</filter-name>
<filter-class>jeus.servlet.filters.ForwardedFilter</filter-class>
<init-param>
<param-name>Forwarded-For</param-name>
<param-value>X-Forwarded-For,Proxy-Client-IP,WL-Proxy-Client-IP,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR</param-value>
</init-param>
</filter>
적용되는 우선순위는 Forwarded 헤더가 가장 높고, 그 다음에는 param-value에 적힌 순서대로 우선순위가 높다. 그리고, key와 전혀 관련없는 헤더들을 적용할 경우(Forwarded-For에 host와 관련된 헤더들을 넣을 경우) API를 호출할 때 잘못된 값이 나올 수 있으니 주의해야 한다.
필터에서 필터링된 후보 헤더와 값을 확인하기 위해서는, 다음 아래의 값으로 HttpServletRequest.getAttribute() API를 호출하면 확인 가능하다. 리턴값은 Map<String, List<String>>으로 첫번째 String은 헤더 이름이고, List<String>은 헤더 값들이다.
jeus.servlet.filters.ForwardedFilter.For jeus.servlet.filters.ForwardedFilter.Host jeus.servlet.filters.ForwardedFilter.Proto jeus.servlet.filters.ForwardedFilter.By jeus.servlet.filters.ForwardedFilter.Port jeus.servlet.filters.ForwardedFilter.Server
관련된 HttpServletRequest의 API는 getRemoteAddr(), getServerName(), getServerPort(), getScheme(), isSecure() 이다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="4.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_4_0.xsd">
<filter>
<filter-name>ForwardedFilter</filter-name>
<filter-class>jeus.servlet.filters.ForwardedFilter</filter-class>
<init-param>
<param-name>Forwarded-For</param-name>
<param-value>X-Forwarded-For,Proxy-Client-IP,WL-Proxy-Client-IP,HTTP_CLIENT_IP,HTTP_X_FORWARDED_FOR</param-value>
</init-param>
<init-param>
<param-name>Forwarded-Host</param-name>
<param-value>X-Forwarded-Host</param-value>
</init-param>
<init-param>
<param-name>Forwarded-Proto</param-name>
<param-value>X-Forwarded-Proto, WL-Proxy-SSL</param-value>
</init-param>
<init-param>
<param-name>Forwarded-By</param-name>
<param-value>X-Forwarded-By</param-value>
</init-param>
<init-param>
<param-name>Forwarded-Port</param-name>
<param-value>X-Forwarded-Port</param-value>
</init-param>
<init-param>
<param-name>Forwarded-Server</param-name>
<param-value>X-Forwarded-Server</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>ForwardedFilter</filter-name>
<servlet-name>ForwardedFilterServletTest</servlet-name>
</filter-mapping>
</web-app>
JEUS가 제공하는 Forwarded Header 옵션에 사용할 클래스 이름은 jeus.servlet.filters.ForwardedFilter이다.