42 - 세션 / 쿠키 / 필터 / 리스너 (2)
필터를 이용한 로그인 체크
- 필터 (Servlet Filter ) : 특정한 서블릿이나 JSP 등에 도달하는 과정에서 데이터를 필터링하는 역할을 위해서 존재하는 서블릿 API의 특별한 객체
→ @WebFilter 어노테이션 이용해 특정한 경로에 접근할 때 필터가 동작하도록 설계하면, 동일한 로직을 필터로 분리 가능
• 한 개 이상, 여러 개 적용 가능
• @WebFilter 어노테이션의 urlPatterns 속성 이용 : { } 안에 여러 url 주소 작성 가능
▷ LoginCheckFilter 클래스
- Filter 인터페이스
• javax.servlet.Filter를 import 해야 함
• doFilter라는 추상메서드 존재 → 필터가 필터링이 필요한 로직을 구현해야 함
• @WebFilter 어노테이션 이용 : 특정한 경로를 지정해 해당 경로의 요청에 대해서 doFilter( ) 메서드를 실행
→ 현재 @WebFilter(urlPatterns= {"/todo/*"}) 지정 = '/todo/...'로 시작하는 모든 경로에 대해서 필터링 시도함
• FilterChain의 doFilter( ) 메서드를 이용해 다음 필터나 목적지(서블릿, JSP)로 갈 수 있도록 doFilter( ) 메서드 마지막 추가
package org.zerock.w2.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
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;
import lombok.extern.log4j.Log4j2;
@WebFilter(urlPatterns= {"/todo/*"})
@Log4j2
// LoginCheckFilter 클래스 : doFilter() 메서드를 구현해야 하는 구현클래스
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
log.info("Login check filter...");
// chain.doFilter() 메서드 실제 구현해보기
// 세션에 로그인 정보를 가지고 있는지 체크 : session.getAttribute("loginInfo") == null
// Type mismatch: cannot convert from ServletRequest to HttpServletRequest
// -> Add cast (다운캐스팅 : 업캐스팅된 데이터를 실제 상속된 자기 자신의 데이터로 형변환 )
// 다운캐스팅은 진짜 자기 자신의 데이터형으로만 변경될 수 있음
// 실무에서는 실제 변형하려는 데이터형인지 검사하는 것도 좋은 프로그램밍 습관임
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 클라이언트에서 이전에 같은 클라이언트라는 것을 증명하는 것이 세션 정보이므로,
// 클라이언트 요청 객체에서 얻음
HttpSession session = req.getSession();
// 세션 객체에서 우리가 얻고자 하는 key 변수의 값(=userInfo)을 조사함
// userInfo key가 없다면, 로그인한 적이 없다는 뜻이므로
// 로그인 페이지로 돌려보낸다.
if(session.getAttribute("userInfo") == null) {
res.sendRedirect("/login");
// 메서드 실행이 끝났으므로, 다음 실행을 막기 위해 return문 사용
return;
}
// 메인 필터가 끝났으므로, 남아있는 필터를 실행한다.
// chain.doFilter() 메서드의 경우,
// try - catch - finally 문법에서 finally 부분 같은 기능을 함
chain.doFilter(request, response);
}
}
▷ 로그인 체크 구현
- LoginCheckFilter의 경우, '/todo/...'로 시작하는 모든 자원에 접근할 때 동작하도록 설정
→ 로그인 여부를 체크하도록 수정
@WebFilter(urlPatterns= {"/todo/*"})
@Log4j2
// LoginCheckFilter 클래스는 doFilter() 메서드를 구현해야 하는 구현클래스
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
log.info("Login check filter...");
// ServletRequest : 부모 | HttpServletRequest : 자식
// 다운캐스팅을 해주어야 HTTP와 관련된 작업 가능
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 클라이언트 요청 객체로부터 세션 정보를 얻음
// ∵ 세션 정보의 로그인기록을 통해 이전의 클라이언트와 동일한 클라이언트라는 것을 증명 가능
HttpSession session = req.getSession();
// 세션 객체에서 우리가 얻고자 하는 key 변수의 값(=userInfo)을 조사함
// userInfo key가 없다면, 로그인한 적이 없다는 뜻이므로
// 로그인 페이지로 돌려보낸다.
if(session.getAttribute("userInfo") == null) {
res.sendRedirect("/login");
// 메서드 실행이 끝났으므로, 다음 실행을 막기 위해 return문 사용
return;
}
// 메인 필터 종료 → 남아있는 필터 실행 또는 다른 목적지(서블릿, JSP)로 이동
chain.doFilter(request, response);
}
}
- 서버 실행시킨 후 '/todo/list'를 호출하면, 브라우저는 LoginCheckFilter를 통해서 '/login'으로 이동하게 됨
- 로그인 정보를 전달해서 로그인이 처리되면, 이후로는 '/todo/...' 경로 이용 가능
UTF-8 처리 필터
package org.zerock.w2.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
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 lombok.extern.log4j.Log4j2;
// 모든 요청 주소에 대해서 한글필터 적용
@WebFilter(urlPatterns= {"/*"})
@Log4j2
public class UTF8Filter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("UTF-8 Filter 처리...");
// HTTP에 관련된 작업을 하기 위해 다운캐스팅 필수
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 요청값으로 들어오는 데이터 호환을 위한 문자코드체계 설정 : 보통 UTF-8로 설정
req.setCharacterEncoding("UTF-8");
// 웹 브라우저의 요청결과로 보일 페이지의 contentType을 설정
res.setContentType("text/html; charset=UTF-8");
// 나머지 필터 정리
// 들어왔던 필터 정보 그대로 보내야 함 ( req, res 아님 )
chain.doFilter(request, response);
}
}
- UTF8Filter 테스트 : TodoRegisterController에 작성한 출력스트림 확인
• TodoRegisterController에 아래 코드 추가 후
• UTF8Filter의 아래 코드를 주석처리할 경우, 한글이 깨져서 출력됨
• 주석을 풀고 다시 실행하게 되면, 한글이 깨지지 않고 출력되는 것 확인 가능
세션을 이용하는 로그아웃 처리
- 보안 정책 상, 로그아웃하는 과정은 GET 방식 보다는 POST 방식으로!
• doGet() 메서드 만들지 않는 것이 좋다.
• doGet() 메서드의 접근제어자를 private로 지정하기
(외부에서 접근 못하는 것이 맞지만, 어떤 이유로든 불려질 가능성 있음 애초에 만들지 말자 )
-> 불필요한 메서드를 만들지 말자.
- HttpSession을 이용하는 경우
① 간단하게 로그인 확인 시 사용했던 정보 삭제하는 방식 : removeAttribute( String name ) 메서드 이용
② 현재의 HttpSession이 더 이상 유효하지 않도록 만드는 방식 : invalidate( ) 메서드 이용
→ 관련있는 세션 항목을 삭제한 후, 무효화시키기
package org.zerock.w2.controller;
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;
import lombok.extern.log4j.Log4j2;
@Log4j2
@WebServlet(name="todoLogoutController", urlPatterns="/logout")
public class TodoLogoutController extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
log.info("Logout 처리 중...");
// 요청객체로부터 세션 얻기
HttpSession session = req.getSession();
// 세션의 원하는 속성을 제거한 후, 세션을 무효화시킨다.
// ➊ 세션 객체에서 속성 삭제 : removeAttribute("속성 키")
// ❷ 세션 객체 무효화 : invalidate()
session.removeAttribute("userInfo");
session.invalidate();
// 로그아웃했으므로, 정책에서 정해진 페이지로 이동
// 보통 메인 페이지(시작페이지)로 이동시킴
res.sendRedirect("/");
}
}
▷ list.jsp와 register.jsp에 로그아웃 버튼 만들고 실행시켜 보기
- 로그아웃을 실행하기 위해 list.jsp 및 register.jsp 수정
• <form> 태그 이용해 버튼 추가
( register.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>
세션정보 : <br />
loginInfo : ${ userInfo }<br />
loginInfo.mname : ${ userInfo.mname }
<h1> 해야할 일 목록 리스트 </h1>
<!-- 로그아웃 버튼 추가 -->
<form action="/logout" method="post">
<button>Logout</button>
</form>
</body>
</html>
- 로그인한 후, list화면의 Logout 버튼 클릭 → 'localhost'로 이동해 시작페이지 출력 → 그 상태에서 '/todo/register' 호출 시 다시 로그인 화면 출력됨
데이터베이스에서 회원 정보 이용하기
- 이제 실제 데이터베이스를 이용해서 회원정보를 구성한 후, 활용해보자
- localhost:3306에 연결된 localhost의 이름 변경
- 새 SQL 편집기 열어 member 테이블 생성
• 식별자에 ‘_’를 이용한 스네이크표기법 이용
• 테이블이라는 것을 쉽게 파악할 수 있게 ‘tbl_’를 표기해주기
ex ) 컬럼명 : ‘col_’
데이터베이스명 : ‘db_’
• archer : 문자열
- 테이블에 사용자 데이터 추가 : INSERT문
- 실제 로그인 시, 이용할 SQL 테스트 : 로그인 사용자 정보 조회 질의(Query)
자바에서 회원 데이터 처리하기
- 자바의 변수명과 DB 테이블의 컬럼명을 서로 동일하게 처리함 (다만, 표기법의 차이 - 카멜 / 스네이크 )
- 자바에서 객체로 처리하도록 V0 / DAO 등을 구현함
▷ MemberVO
package org.zerock.w2.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter // VO : ReadOnly
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberVO {
private String mid;
private String mpw;
private String mname;
private String uuid;
}
▷ MemberDAO
package org.zerock.w2.domain;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.zerock.jdbcex.dao.ConnectionUtil;
import lombok.Cleanup;
public class MemberDAO {
public MemberVO getWithPassword(String mid, String mpw) throws Exception {
// DB에 연결하기 위해 SQL문 작성
String query = """
SELECT mid, mpw, mname FROM tbl_member
WHERE mid = ? AND mpw = ?;
""";
// MemberVO를 지역변수로 선언
MemberVO memberVO = null;
@Cleanup Connection conn = ConnectionUtil.INSTANCE.getConnection();
@Cleanup PreparedStatement pstmt = conn.prepareStatement(query);
// pstmt에 파라미터 추가
pstmt.setString(1, mid);
pstmt.setString(2, mpw);
@Cleanup ResultSet rs = pstmt.executeQuery();
// 데이터가 하나 있다는 것을 알고 있지만, 실제로는 while 루프문 이용하기
rs.next();
memberVO = MemberVO.builder()
.mid(rs.getString(1))
.mpw(rs.getString(2))
.mname(rs.getString(3))
.build();
return memberVO;
}
}
• Lombok의 @Builder를 통해 builder 메서드 이용
→ 필요한 데이터에 대해서 메서드를 통해 step-by-step으로 값을 입력 받음
→ build( ) 메서드를 통해 최종적으로 하나의 인스턴스를 리턴하는 방식
▷ MemberDTO
package org.zerock.w2.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberDTO {
private String mid;
private String mpw;
private String mname;
private String uuid;
}
▷ MemberService
- 컨트롤러들의 비즈니스 로직을 처리하는 역할 → CRUD(등록/조회/수정/삭제) 기능들을 서비스 객체에 모아서 구현함
- 내부적으로 DAO를 이용해 처리함 → 멤벼변수로 DAO를 가짐
- 모델로 보낼 데이터를 비즈니스 로직에 맞게 정제하는 역할
- ModelMapper를 이용해 DTO와 VO 간의 변환 처리함
- 로그인 처리를 위한 login( ) 메서드 작성
package org.zerock.w2.service;
import org.modelmapper.ModelMapper;
import org.zerock.jdbcex.util.MapperUtil;
import org.zerock.w2.domain.MemberDAO;
import org.zerock.w2.domain.MemberDTO;
import org.zerock.w2.domain.MemberVO;
import lombok.extern.log4j.Log4j2;
@Log4j2
public enum MemberService {
INSTANCE;
private MemberDAO dao;
private ModelMapper modelMapper;
MemberService(){
dao = new MemberDAO(); // 생성자를 이용해 DAO 초기화
modelMapper = MapperUtil.INSTANCE.get();
}
// 로그인 체크
public MemberDTO login(String mid, String mpw) throws Exception {
// MemberDAO의 getWithPassword() 메서드를 통해 MemberVO 반환
MemberVO vo = dao.getWithPassword(mid, mpw);
// 반환된 MemberVO를 modelMapper를 이용해 MemberDTO로 변환
MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);
return memberDTO;
}
public void updateUuid(String mid, String uuid) throws Exception {
dao.updateUuid(mid, uuid);
}
}