웹 개발을 하다보면 서버에서도 유효성 검증을 필수로 하지만 프론트에서 1차적으로 필터링을 해주면 아무래도 서버에 부하도 적어지고 데이터 타입등 유효하지 않은 데이터를 걸러낼수 있어서, 자바스크립트를 통해 필수적으로 ajax나 submit 처리 전에 데이터 검사를 하게 됩니다.
문자열 검사하기
문자열을 검사하고 싶으면 생각보다 굉장히 간단하게 검사할 수 있습니다.
기존에는 undefined, null, 공백값등을 체크하는 조건문을 길게 쓰기 싫어서 함수를 만들어서 적용하는 방식으로 사용하거나 조건문에 길게 나열하곤 했었는데, 그 모습은 아래와 같습니다.
let text;
if(text === undefined || text === null || text === ''){
alert("문자열이 비었습니다.");
}
하지만 실상 빈값에 대한 유효성 검증은 변수에 !(not 연산자) 하나만 붙여주면 됩니다.
NOT연산자 하나를 통한 유효성 검증
let text = '';
if(!text){
alert("문자열이 비었습니다.");
}
!undefined //true
!null //true
!'' //true
!0 //true
!NaN //true
- annotation-driven: xml이 길고 복잡해지면서 대안으로 나타났습니다. 클래스, 메소드 등에 @를 달아 기능을 수행할 수 있도록 동작하기 위해 설정합니다. (xml Bean 등록이 더 우선순위가 높습니다.)
- mvc:annotation-driven: @Controller @RequestMapping 등 컨트롤러에서 해당하는 기능을 설정할 수 있게 해줍니다.
- task:annotation-driven: @Scheduled 어노테이션을 사용을 위해 처리합니다.(스케줄러 관련)
- org.springframework.web.servlet.view.InternalResourceViewResolver: 컨트롤러가 모델을 리턴하고 DisapatcherServlet이 해당하는 파일을 처리할 때 필요한 정보를 기술하는 태그 접미사가 'WEB-INF/jsp'이면서 '.jsp'로 끝나는 파일들을 찾습니다.
- context:component-scan: 특정 패키지의 클래스들을 스캔하여 어노테이션을 확인하고 bean 등록을 한다. @Component @Controller @Service @Repository등이 클래스에 설정해야 등록된다.
웹 요청 동작순서
1. 사용자가 브라우저를 통해 주소를 입력합니다. - ( http://localhost/login.do )
web.xml에서 설정했던 정보에 맞는 요청일 경우 DispatcherServlet이 해당 요청을 먼저 가로채서 적합한 Controller에 매핑을 해줍니다.
2. <servlet-mapping/>부분이 있는데, 설정된 <servlet-name/>에 해당하는 매핑 설정을 할 수 있습니다.
예를들어 <url-pattern/>부분이 '/'으로 처리되어 있다면 모든 요청을 가로챕니다. 하지만 설정부분은 '*.do'로 되어있어서 'login.do'와 같은 형태만 적용이 됩니다.
3. 가로챈 매핑정보를 통해 처리할 Controller를 찾은 후, 적용된 코드에 따라 기능이 처리된다.
3-1.Controller(mapping) -> View
3-2. Controller(mapping) -> Service -> DAO -> mapper -> DB -> DAO -> Service -> Controller -> Return
4. 3-1과 같은 처리가 수행되면 ViewResolver에 의해 해당하는 view를 반환한다.
console.log("결과는? >" + test());
function test(){
return "hi";
}
해당 코드의 결과는?! 아래와 같습니다!
함수 test를 호출함으로써 "hi"라는 문자열이 정상적으로 반환되어서 콘솔에 hi가 잘 찍히는걸 볼 수 있습니다.
실질적으로 정의된 test함수는 출력하는 콘솔로그보다 아래에 위치하지만 신기하게도 동작하는걸 볼 수 있는데 이러한 동작을 호이스팅이라고 합니다.
변수와 함수간에 동작이 조금 다른데, 이번엔 변수 동작부분을 확인해보겠습니다.
변수의 호이스팅 동작
var a = "테스트입니다~";
function test(){
console.log("a1 >",a);
var a = "테스트 함수에서 a변수를 재정의합니다!";
console.log("a2 >",a);
}
test();
이번에 작성한 코드의 결과는 어떨까요? 저도 이부분에 처음에 당황을 했습니다.
결과는 아래와 같습니다.
예상한 결과와 같나요?
제가 처음 예상했던 답은 "테스트입니다~"가 먼저 출력되고, 재정의되었다는 문자열이 출력 될것이라고 생각했지만, 호이스팅으로 인해 전혀 다른 결과가 나왔습니다.
아래는 호이스팅 방식을 기반으로 해석한 코드입니다.
var a = "테스트입니다~";
function test(){
var a;
console.log("a1 >",a); //undefined
a = "테스트 함수에서 a변수를 재정의합니다!";
console.log("a2 >",a); //테스트 함수에서 a변수를 재정의합니다!
}
test();
해석한 코드와 기존 코드의 차이가 보이나요?
호이스팅으로 인해 test함수 최상단에 a라는 변수가 선언되었고, 콘솔로그에서 출력이 되면서 undefined라는 결과물이 나왔고, 그 이후 대입연산자를 통해 재정의라는 문자열이 대입된 것을 볼 수 있습니다.
이처럼 블록 구문(함수) 최상단에 변수 또는 함수명인 식별자를 최상단으로 끌어올리는 효과를 보여주는 것을 호이스팅이라고 부릅니다.
복잡한 호이스팅 동작과 전역 스코프
이번엔 복잡해진 호이스팅을 확인해보겠습니다.
var a = 1;
function outer () {
function inner () {
console.log(a);
var a = 3;
console.log(a);
var a;
console.log(a);
var a = 5;
console.log(a);
}
inner();
console.log(a);
}
outer();
console.log(a);
복잡하지만 차근차근 호이스팅을 분석해보겠습니다.
var a;
a = 1;
function outer () {
function inner () {
var a;
var a;
var a;
console.log(a); //undefined
a = 3;
console.log(a); //3
console.log(a); //3
a = 5;
console.log(a); //5
}
inner();
//내부에 a변수가 없으므로 외부 스코프영역에서 a가 존재하는지 찾음
console.log(a); //1
}
outer();
console.log(a); //1
어떤가요? 분석구문과 실제 결과와 동일한가요?
내부 스코프에 해당 변수의 선언된게 없으면 외부 스코프에서 해당 변수가 선언되었는지 확인을 합니다.
아우터부분의 a에서 그래서 1이 찍히는걸 볼 수 있습니다.
정리
다만, 변수와 함수는 또 다른 차이점이 있습니다.
- 변수는 변수명만 최상단에 정의 된 후 선언 위치에서 값이 대입되는 모습을 보여줍니다.
- 함수는 정의부 자체와 함께 최상단에 선언되는 것을 볼 수 있습니다.
이러한 차이점때문에 함수는 처음에 호출하여도 정의부가 잘 동작 후 출력되는것을 볼 수 있엇고, 변수는 최초 선언만 되었기 때문에, 결과 호출시 undefined와 같은 결과를 만날 수 있습니다.
다만, 함수 동작의 호이스팅에서 함수 정의 방법에 따라 달라는 결과를 볼 수 있습니다.
다양한 함수 표현방법의 호이스팅 결과
console.log(test1());
console.log(test2());
console.log(test3());
function test1(){
return "test1";
}
var test2 = function(){
return "test2";
}
var test3 = () => {
return "test3";
}
function 키워드를 통해 함수를 정의하지만 변수에 함수를 대입하여 함수의 동작을 처리할 수도 있고, 화살표 함수를 통해 표현할 수도 있습니다.
다만 호이스팅 처리방식의 변수와 함수 표현식의 차이로 인해 test2, test3는 정상적으로 동작하지 않습니다.
아래는 동작 결과입니다.
초점을 동작하는 행위에 두지 않는다면 당연한 결과일수도 있습니다. (변수로 시작했는지, 함수로 시작했는지)
상단에 정의된 내용까지 호이스팅하는 동작은 function키워드로 정의한 부분만 동작하는 것을 볼 수 있습니다.
<!-- 멀티파트 필터링을 위한 설정 -->
<filter>
<filter-name>multipartFilter</filter-name>
<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>multipartFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- lucy 설정 -->
<filter>
<filter-name>xssEscapeServletFilter</filter-name>
<filter-class>com.navercorp.lucy.security.xss.servletfilter.XssEscapeServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>xssEscapeServletFilter</filter-name>
<!-- <url-pattern>/*</url-pattern> -->
<url-pattern>*.do</url-pattern>
</filter-mapping>
멀티파트필터 없이 경우에는 루시필터만 적용하게되면 멀티파트의 행위에 대해서는 감시가 이루어지지 않기 때문에 해당 필터를 적용해주셔야 합니다!
lucy-xss-servlet-filter-rule.xml
- resources/ 하위에 해당 파일을 생성하고 위치시킵니다.
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns="http://www.navercorp.com/lucy-xss-servlet">
<defenders>
<!-- XssPreventer 등록 -->
<defender>
<name>xssPreventerDefender</name>
<class>com.navercorp.lucy.security.xss.servletfilter.defender.XssPreventerDefender</class>
</defender>
<!-- XssSaxFilter 등록 -->
<defender>
<name>xssSaxFilterDefender</name>
<class>com.navercorp.lucy.security.xss.servletfilter.defender.XssSaxFilterDefender</class>
<init-param>
<param-value>lucy-xss-sax.xml</param-value> <!-- lucy-xss-filter의 sax용 설정파일 -->
<param-value>false</param-value> <!-- 필터링된 코멘트를 남길지 여부, 성능 효율상 false 추천 -->
</init-param>
</defender>
<!-- XssFilter 등록 -->
<defender>
<name>xssFilterDefender</name>
<class>com.navercorp.lucy.security.xss.servletfilter.defender.XssFilterDefender</class>
<init-param>
<param-value>lucy-xss.xml</param-value> <!-- lucy-xss-filter의 dom용 설정파일 -->
<param-value>false</param-value> <!-- 필터링된 코멘트를 남길지 여부, 성능 효율상 false 추천 -->
</init-param>
</defender>
</defenders>
<!-- default defender 선언, 필터링 시 지정한 defender가 없으면 여기 정의된 default defender를 사용해 필터링 한다. -->
<default>
<defender>xssPreventerDefender</defender>
</default>
<!-- global 필터링 룰 선언 -->
<global>
<!-- 모든 url에서 들어오는 globalParameter 파라메터는 필터링 되지 않으며
또한 globalPrefixParameter1로 시작하는 파라메터도 필터링 되지 않는다.
globalPrefixParameter2는 필터링 되며 globalPrefixParameter3은 필터링 되지 않지만
더 정확한 표현이 가능하므로 globalPrefixParameter2, globalPrefixParameter3과 같은 불분명한 표현은 사용하지 않는 것이 좋다. -->
<params>
<param name="globalParameter" useDefender="false" />
<param name="globalPrefixParameter1" usePrefix="true" useDefender="false" />
<param name="globalPrefixParameter2" usePrefix="true" />
<param name="globalPrefixParameter3" usePrefix="false" useDefender="false" />
</params>
</global>
<!-- url 별 필터링 룰 선언 -->
<url-rule-set>
<!-- url disable이 true이면 지정한 url 내의 모든 파라메터는 필터링 되지 않는다. -->
<url-rule>
<url disable="true">/disableUrl1.do</url>
</url-rule>
<!-- url disable이 false인 설정은 기본이기 때문에 불필요하다. 아래와 같은 불필요한 설정은 하지 않는다.-->
<url-rule>
<url disable="false">/disableUrl2.do</url>
</url-rule>
<!-- url disable이 true이면 지정한 url 내의 모든 파라메터가 필터링 되지 않기 때문에 <params> 로 선언한 설정은 적용되지 않는다.
아래와 같은 불필요한 설정은 하지 않는다. -->
<url-rule>
<url disable="true">/disableUrl3.do</url>
<params>
<param name="query" useDefender="false" />
<param name="prefix1" usePrefix="true" />
<param name="prefix2" usePrefix="false" useDefender="false" />
<param name="prefix3" usePrefix="true" useDefender="true" />
<param name="prefix4" usePrefix="true" useDefender="false" />
<param name="prefix5" usePrefix="false" useDefender="true" />
</params>
</url-rule>
<!-- url disable이 false인 설정은 기본이기 때문에 불필요하다. <params> 선언한 설정은 적용이 된다.-->
<url-rule>
<url disable="false">/disableUrl4.do</url>
<params>
<!-- disableUrl4.do 의 query 파라메터와 prefix4로 시작하는 파라메터들은 필터링 되지 않는다.
usePrefix가 false, useDefender가 true인 설정은 기본이기 때문에 불필요하다. -->
<param name="query" useDefender="false" />
<param name="prefix1" usePrefix="true" />
<param name="prefix2" usePrefix="false" useDefender="false" />
<param name="prefix3" usePrefix="true" useDefender="true" />
<param name="prefix4" usePrefix="true" useDefender="false" />
<param name="prefix5" usePrefix="false" useDefender="true" />
</params>
</url-rule>
<!-- url1 내의 url1Parameter는 필터링 되지 않으며 또한 url1PrefixParameter로 시작하는 파라메터도 필터링 되지 않는다. -->
<url-rule>
<url>/url1.do</url>
<params>
<param name="url1Parameter" useDefender="false" />
<param name="url1PrefixParameter" usePrefix="true" useDefender="false" />
</params>
</url-rule>
<!-- url2 내의 url2Parameter1만 필터링 되지 않으며 url2Parameter2는 xssSaxFilterDefender를 사용해 필터링 한다. -->
<url-rule>
<url>/url2.do</url>
<params>
<param name="url2Parameter1" useDefender="false" />
<param name="url2Parameter2">
<defender>xssSaxFilterDefender</defender>
</param>
</params>
</url-rule>
</url-rule-set>
</config>
네이버 제공 샘플을 참고하여 자기 프로젝트에 맞게 설정을 변경해주세요.
(작성하다보니 XssFilter, XssSaxFilter 부분에 lucy-xss-sax.xml, lucy-xss.xml를 적용하는 부분이 있는데 별도로 생성하지 않아도 문제없이 잘 동작했습니다.)
중요한 부분은 필터링을 제외처리할 부분과 특정 파라미터만 제외할 부분인데, 에디터등을 사용한 프로젝트라면 당연히 html코드로 넘어오기 때문에 제외처리를 잘 해주셔야합니다.
lucy필터의 특징은 넘어오는 파라미터를 필터링(치환)을 처리해주는거지 필터에서 컨트롤러접근을 막아주는 필터는 아닌점만 인지하고 적용하시면 될 것 같습니다.
* 멀티파트(Multipart)에서도 필터링이 되어야하는분들은 추가 작업이 있습니다
이후 구동하는 톰캣의 config디렉토리의 context.xml을 열어줍니다.
기존 소스가 아래와 같은 형태로 되어 있을텐데,
<Context>
<!-- Default set of monitored resources. If one of these changes, the -->
<!-- web application will be reloaded. -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" />
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
</Context>
<Context allowCasualMultipartParsing="true" path="/">
<!-- Default set of monitored resources. If one of these changes, the -->
<!-- web application will be reloaded. -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
<CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" />
<!-- Uncomment this to disable session persistence across Tomcat restarts -->
<!--
<Manager pathname="" />
-->
</Context>