Filter란 클라이언트(Client)에서 서버(Server)로 요청이 들어오기 전 특정 서블릿을 데이터를 정제하거나 분기를 정하는 등의 목적으로 만들고자 할 때 사용하는 추상 인터페이스로, 클라이언트의 요청을 서버가 받아 메인 서블릿(컨테이너)에 넘기기 전 단계라고 보시면 됩니다.
(자료 출처 : https://wch18735.github.io/jsp/JSP_Servlet_Filter/)
이러한 필터는 주로 인터넷 뱅킹 사이트 같은 곳에서 민감한 개인 정보를 다루기 위해 사용자에게 추가적인 인증을 별도로 요구하는 용도로, 혹은 사용자가 입력한 로그인 값이 올바른지를 대조하는대에 사용되는 용도로 사용됩니다.
앞서 살펴본 리다이렉션 섹션에서 혹시 눈치 채신 분들이 계실수도 있으실텐데요. 저는 그 섹션에서 session을 이용한 정보 전달과 리다이렉션 조합 부분에서 인코딩 세팅 작업을 하지 않고도 sub2 페이지에서 올바른 정보들을 출력할 수 있었습니다.
- sub1.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <% String name = request.getParameter("name"); String age = request.getParameter("age"); session = request.getSession(); session.setAttribute("name", name); session.setAttribute("age", age); response.sendRedirect("sub2.jsp"); %>
- sub2.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>사용자 정보</h1> <p>name : ${name} </p> <p>age : ${age} </p> </body> </html>
어떻게 가능했던 걸까요? 네. 그렇습니다. filter를 구현하고 그 구현 정보를 web.xml에 등록했기에 가능한 일이였죠.
우선 web.xml에 해당 filter의 정보를 등록하기 위해 다음과 같은 태그 목록들을 web.xml에 삽입해 줍니다.
<!-- 필터의 기본적인 정보 --> <filter> <!-- 필터 이름 --> <filter-name>EncodingFilter</filter-name> <!-- 필터의 위치 --> <filter-class>com.practice.test.EncodingFilter</filter-class> <!-- init 메서드에 사용할 filter 초기 객체의 변수와 값들--> <init-param> <!-- request.setCharacterEncoding 설정에 사용할 값 설정--> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> <init-param> <!-- response.setContentType 설정에 사용할 값 설정--> <param-name>contentType</param-name> <param-value>text/html; charset=UTF=8</param-value> </init-param> </filter> <!-- 해당 필터가 영향을 끼치는 url의 범위를 설정, 참고로 아스터리스크(*)를 표기하면 모든 범위의 서블릿과 JSP 페이지로 이동할 때마다 해당 filter가 적용됨. --> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
그 다음 작업으로는 web.xml에 등록했던 filter의 정보를 토대로 다음과 같이 filter를 구현하는 클래스를 아래와 같이 작성해야 하는데요.
package com.practice.test; import java.io.IOException; import javax.servlet.Filter; public class EncodingFilter implements Filter{ }
filter는 추상 인터페이스이기 때문에 인터페이스를 구현하는 쪽에서 해당 추상 메서드들을 구현해 주어야 하는데요. 보통은 init, doFilter, destroy 메서드를 구현합니다.
public class EncodingFilter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { } @Override public void destroy() {} }
그 다음 web.xml에서 정의한 filter 초기 객체의 값들을 사용하기 위해 필드로 다음 코드를 선언한 뒤 생성된 필드들에 init 메서드에 해당 파라미터 값들을 할당해 줍니다.
private String encoding; private String contentType; @Override public void init(FilterConfig filterConfig) throws ServletException { // encoding 파라미터의 값은 utf-8 이었음 encoding = filterConfig.getInitParameter("encoding"); // contentType 파라미터의 값은 text/html; charset=UTF=8 이었음 contentType = filterConfig.getInitParameter("contentType"); }
그 다음 doFilter 메서드에 각 객체로부터 문자 인코딩 방식과 컨텐츠 타입 인코딩 방식을 가져와 null일 경우, 그러니까 set 메서드로 설정을 해주지 않은 경우 파라미터 값들을 할당 받은 encoding과 contentType 변수의 값으로 각각 set 메서드를 호출한 뒤 할당해 줍니다.
if(request.getCharacterEncoding() == null) request.setCharacterEncoding(encoding); if(response.getContentType() == null) response.setContentType(contentType); // doFilter가 실행 되었다는 것을 확인하기 위한 콘솔 출력문 System.out.println("필터 초기화 완료"); // doFilter 작업이 끝난 뒤 request와 response 정보를 그제야 서블릿이나 JSP 페이지로 제어 전달 chain.doFilter(request, response);
그럼 다음과 같이 페이지를 이동할 때마다 filter가 발동 되어 다음과 같은 문구가 콘솔에 출력되는걸 확인할 수 있습니다.
앞서 살펴본 리다이렉션 색션에서 활용한 세션과 리다이렉션의 예제를 활용하여 사용자가 입력한 값 중 하나가 없을 경우 자연스럽게 로그인 창으로 리다이렉션 하는 코드를 만들어 볼겁니다.
우선 사용자에게 보여질 메인 화면을 다음과 같이 만듭니다. 이때 경로는 서버에 요청을 보낼 것이고, 해당 서블릿들은 @WebServlet 어노테이션으로 각자 매핑이 되어있기 때문에 절대경로로 해당 어노테이션 경로를 지정해 줍니다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <ul> <li><a href="showUserInfo">유저 정보 확인</a></li> <li><a href="moveLoginPage">로그인 화면</a></li> </ul> </body> </html>
이제 서블릿을 만들어줄 차례인데요. 서블릿은 경로대로 showUserInfo 하나와 moveLoginPage 서블릿을 각각 만들어 줍니다. 이 서블릿들은 말 그대로 서버에서 다른 페이지로 리다이렉트 시켜주려는 목적의 페이지들이죠.
1.showUserInfo 페이지
package com.practice.test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/showUserInfo") public class showUserInfo extends HttpServlet { private static final long serialVersionUID = 1L; public showUserInfo() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendRedirect("/project/showUserInfo.jsp"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
- moveLoginPage
package com.practice.test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/moveLoginPage") public class moveLoginPage extends HttpServlet { private static final long serialVersionUID = 1L; public moveLoginPage() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendRedirect("/project/loginForm.jsp"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
서블릿들도 만들어 줬으니 이제 해당 서블릿들이 보내고자 하는 페이지도 만들어 줘야겠죠? showUserInfo와 loginform jsp를 만들어 줍니다.
- showUserInfo
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> id : ${id} password : ${pw} </body> </html>
- loginForm
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <form action="login"> <p> 아이디 : <input type = "text" name = "id"> </p> <p> 비밀번호 : <input type = "password" name = "pw"> </p> <input type="submit" value = "가입하기" > <input type="reset" value = "초기화" > </form> </body> </html>
이제 loginForm을 만들어 줬으니 form 정보를 보내줄 서블릿을 또 하나 만들어 보도록 하겠습니다.
- login
package com.practice.test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebServlet("/login") public class login extends HttpServlet { private static final long serialVersionUID = 1L; public login() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html; charset=utf-8"); String id = request.getParameter("id"); String pw = request.getParameter("pw"); System.out.println(id); System.out.println(pw); HttpSession session = request.getSession(); session.setAttribute("id", id); session.setAttribute("pw", pw); response.sendRedirect("/project/showUserInfo.jsp"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
겉으로 보기에는 완성되어 보이는 듯 하군요. 근데 문제가 있습니다. 로그인 폼이라고 한다면 둘 중 하나의 값이 입력된 경우와, 아무것도 입력을 안한 경우 로그인을 하도록 해야하는데, 그 처리를 안해줬습니다.
그래서 우리는 이 처리를 filter를 이용해서 진행을 해볼겁니다.
우선 filter를 하나 만들어 보도록 하겠습니다.
package com.practice.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @WebFilter("") public class LoginFilter implements Filter{ @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { } @Override public void destroy() {} }
우리가 해야할 작업은 우선 사용자가 넣은 값, 즉 입력한 값이 잘 들어왔는지를 확인하는 법입니다. 그럴려면 login에서 사용자가 입력한 값을 세션에 저장한 값을 사용해야겠죠?
HttpSession session = request.getSession();
앗! 근데 문제가 있습니다. filter의 doFilter 부분을 잘 보시면 request 부분은 ServletRequest 클래스 타입인걸 확인할 수 있는데요. getSession 메서드는 HTTPServletRequest 클래스 타입에게 있는 전용 메서드로, 부모인 ServletRequest 클래스에는 해당 메서드가 없어서 사용을 못합니다.
그래서 우리는 doFilter의 request를 다음과 같이 다운그레이드 시켜줄겁니다. 그럼 무사히 다운 그레이드 된 인스턴스로부터 getSession 메서드를 호출해 사용자가 입력한 값을 가져올 수 있겠죠.
HttpServletRequest req = (HttpServletRequest)request; HttpSession session = req.getSession();
이제 우리가 해야할건 사용자가 값을 하나라도 안 넣은 경우 로그인 창을 다시 이동하도록 하는 작업입니다. 그럴려면 다음과 같은 조건이 필요하겠죠?
// 모든 속성을 가져올때는 타입이 오브젝트 이므로 적절한 형변환이 필요함 String id = (String)session.getAttribute("id"); String pw = (String)session.getAttribute("pw"); if( (id == null || pw == null) || (id == "" || pw == "") ){ //request와 마찬가지로 response도 다운그레이드 HttpServletResponse rep = (HttpServletResponse)response; // 현재 웹 어플리케이션의 컨텍스트패스인 project를 반환 받고, 그 바로 밑의 loginForm으로 돌아가 올바르게 입력하도록 함 String cPath = req.getContextPath(); rep.sendRedirect(cPath + "/loginForm.jsp"); }else{ // 올바르게 입력한 경우라면 그대로 서블릿을 진행 chain.doFilter(request, response); }
자, 이제 가장 중요한 WebFilter가 남았습니다. WebFilter는 해당 서블릿이 실행하기 전에 거쳐가도록 하는 경로를 지정하는데요. 우린 이 필터를
showUserInfo 서블릿에 걸어줄 겁니다. 그래야 사용자가 값을 입력한 뒤에 서블릿에 의해 showUserInfo에 진입하기 전에 filter를 걸어줘 만약 둘 중 하나의 값이라도 없는 경우 다시 로그인을 하게끔 만들고,
두 값을 다 입력했다면 그대로 서블릿을 다시 재개하게끔 만들어야 하니까요. 그래서 showUserInfo의 어노테이션에 다음과 같은 매핑 주소를 추가해 줍니다.
@WebServlet("/private/showUserInfo")
어차피 각각의 고유의 절대 경로를 가질 수 있기 때문에 가능한 작업이죠. 그리고 WebFilter의 주소도 다음과 같이 작성을 해줍니다.
@WebFilter("/private/*")
이 의미는, 절대 경로로 private이 붙은 다음의 모든 경로에 있는 파일들에 이 filter를 적용하겠다는 의미입니다. 그래서 showUserInfo 또한 해당 filter를 거치게 되는거죠. 자 이제 그럼 진짜 시연을 해볼까요?
시연을 하고 보니 약간 아쉬운건 있지만 그래도 의도 대로 작동되는군요...?