백엔드/웹 개발

로그인, 로그아웃 구현하기(필터)

study_yeon 2023. 6. 29. 09:15

2023.06.28수업 

- 교재 : 자바 웹 개발 워크북

※ 브라우저 개발자도구(f12)
Network : 현재 접속된 주소의 진행사항
Application : 쿠키, 브라우저에 저장된 내용
Element : 홈페이지 요소(html, css 등을 보여줌)
Console : 자바스크립트


* console.log(`${i} + ${j} = ${k}`)
`${표현식} + ${표현식}`

* referer(레퍼러)
- HTTP의 헤더
- 레퍼러를 참조하여 현재 표시하는 웹 페이지가 어떤 웹 페이지에서 요청되었는지 알 수 있음
- 어디서(웹 사이트나 웹 서버) 방문자가 왔는지 파악


2023.06.28 - [백엔드/웹 개발] - 방문자 기억 - (세션/쿠키)

이어서

 

방문자 기억 - (세션/쿠키)

2023.06.27 수업 ● 웹 개발 3장 ※ 세션과 필터 - 웹은 과거의 상태를 유지하지 않는 무상태(stateless) -> 기존 사용자에 대한 정보를 기억하지 않음 - 세션과 쿠키는 웹의 단점(Connection less) 극복 * 세

dustj0824.tistory.com

 

● 필터를 이용한 로그인 체크

 

- 특정한 서블릿/JSP 등에 도달하는 과정에서 필터링하는 역할을 위해 존재하는 서블릿 API의 특별한 객체
@WebFilter(urlpatterns 사용)를 이하여 동일한 로직을 분리

- Filter 인터페이스를 사용하면 doFilter()를 구현해야함
- 서비스가 데이터에 도달하기전에 처리
- 다중 필터 가능


▶ 실습

 

▷ 필터를 이용하여 로그인 체크 구현
클래스 만들기
패키지명 org.zerock.w2.filter
클래스명 LoginCheckFilter
- /todo/.. 로 시작하는 모든 자원에 접근할 때 동작하도록 설정
- 로그인 여부 체크하기

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
public class LoginCheckFilter implements Filter{
	@Override
	public void doFilter(
		ServletRequest request, 
		ServletResponse response, 
		FilterChain chain) //chain : 요청처리 필터
			throws IOException, ServletException {
		log.info("Login check filter...");
		// chain.doFilter(req, res);  // 연습용
			
		//// chain.doFilter 실제 구현하기
		// 다운캐스팅(자기자신타입의 데이터로 형변환) 필요
		// 실무에서 실제 변형하려는 데이터형인지 검사하는 것도 좋은 프로그래밍법임
		HttpServletRequest req = (HttpServletRequest) request; 
		HttpServletResponse res = (HttpServletResponse) response;
		
		// 클라이언트에서 이전에 같은 클라이언트라는 것을 증명하는 것이 세션정보이므로
		// 클라이언트 요청객체에서 얻는다. 
		HttpSession session = req.getSession();
		
		// 세션객체에서 우리가 얻고자 하는 키(변수)의 값 조사
		// userInfo키가 없다면 로그인 한적이 없다는 뜻이므로 로그인 페이지로 돌려보낸다. 
        // 세션에 로그인 정보를 가지고 있는가
		if (session.getAttribute("userInfo") == null) {
			res.sendRedirect("/login");
			// 메소드 실행이 끝났으므로 다음 실행을 막기위해 return 반환문 사용
			return ;
		}
		// 메인 필터가 끝났으므로 남아있는 필터를 실행한다
		// try - catch - finally 문법의 finally 기능과 유사
		chain.doFilter(request, response);		
	}
}

 

Filter를 임포트해야함

 

dofiler를 구현해야함

* 디버그 
listcontroller로 실행
- LoginCheckFilter의 log.info("Login check filter...")중단점 걸기
-> /todo/list를 요청하면 TodoListController가 작동하기 전에 LoginCheckFilter가 작동함을 확인
(로그인 정보가 없을 시 /login으로 이동, 로그인정보가 있으면 /todo/.. 경로 이용 가능)



▷ UTF-8 필터
- 한글 깨짐 상태 처리
- 모든 경로에 적용
패키지명 org.zerock.w2.filter
클래스명 UTF8Filter

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 필터처리...");
		
		HttpServletRequest req = (HttpServletRequest)request;
		HttpServletResponse res = (HttpServletResponse)response;
		
		// 들어오는 데이터를 한글로 인식(해석)하게 하는 기능(주석처리하여 있고 없고의 차이 확인)
		// 사용자 정의 필터 처리 끝
		req.setCharacterEncoding("UTF-8");
		// 나가는 출력스트림 UTF-8로 처리
		// 브라우저에게 utf-8코드를 사용하라는 메세지 전달
		res.setContentType("text/html;charset=UTF-8"); // 마임타입
		
		
		// 나머지 필터 정리(들어온 정보 그대로 보내기)
		chain.doFilter(request, response);
				
	}
}


○ UTF8Filter 확인

- setCharacterEncoding과 setContentType를 주석처리하여 확인해보기

req.setCharacterEncoding("UTF-8");

- 들어오는 데이터를 한글로 인식(해석)하게 하는 기능(주석처리하여 있고 없고의 차이 확인)
- 서블릿에서 post방식으로 정보를 서버에 전달해줄 때

res.setContentType("text/html;charset=UTF-8");

- 나가는 출력스트림 UTF-8로 처리
- 브라우저에게 utf-8코드를 사용하라는 메세지 전달
- 서블릿에서 직접 브라우저에 출력해줄 경우


▷ 세션을 이용한 로그아웃 처리
패키지명 : org.zerock.w2.controller
클래스명 : TodoLogoutController
- 로그아웃은 보안과 관련되어있어서 post에서 처리

- get으로 처리시 주소에 노출될 수 있음 

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;

@WebServlet(name = "todoLogoutController", urlPatterns="/logout")
@Log4j2
public class TodoLogoutController extends HttpServlet{

	@Override
	protected void doPost(
		HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
		log.info("로그아웃 처리중...");
		
		// 로그아웃은 HttpSession객체의 invalidate()메소드를 이용하여 세션을 초기화(=무효화)하면 된다
		// 실제 세션을 무효화 할때는 세션의 원하는 속성을 제거하고 세션을 무효화 한다.
		HttpSession session = req.getSession();
		// 세션객체에서 속성삭제 : removeAttribute("속성키")
		// 세션객체 무효화 : invalidate()
		session.removeAttribute("userInfo");
		session.invalidate();
		
		// 로그아웃 했으므로 정책에서 정해진 페이지(메인페이지=시작페이지)로 이동
		res.sendRedirect("/");
	}
	
}

post만 구현하여 에러


▷ register / list에 로그아웃 폼 만들기

- 로그아웃 폼 추가

<%@ 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>
	<h2>게시글 작성 페이지</h2>
	
	<!-- 로그아웃 폼 추가 -->
	<form action="/logout" method="post">
		<button>Logout</button>
	</form>

</body>
</html>

 

<%@ 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>
	
	<!-- 로그아웃 폼 추가 -->
	<form action="/logout" method="post">
	<button>Logout</button>
	</form>
</body>
</html>

▷ 데이터베이스에서 회원정보 이용하기
* sts에서 데이터베이스 열기
- localhost2 이름 '마리아디비 - webuser'로 변경하기 (webuser에 대한 권한만 있는 데이터베이스임)

이름변경(f2)


▷ 테이블 만들고 유저추가

- '마리아디비 - webuser' 에서 새 SQL 편집기 열어 명령문 작성

use webdb;

create table tbl_member (
	mid varchar(50) primary key, -- pk가 2개이상일 경우 하단에 작성
	mpw varchar(50) not null,
	mname varchar(100) not null
);

-- 유저 추가
insert into tbl_member (mid, mpw, mname)
	values 
		('user00', '1111', '사용자0'), -- MariaDB 기능
		('user01', '1111', '사용자1'),
		('user02', '1111', '사용자2');

-- 로그인 사용자 정보 조회 쿼리(질의)
select * from tbl_member 
	where mid='user00' and mpw='1111';

▷ 자바에서 회원데이터 처리하기

 

* 비즈니스로직 
컨트롤러가 요청을 받아 서비스에게 전달하여 서비스에서 처리하여 다시 컨트롤러에 보내줌
서비스에서 처리하는 로직(VO, DTO 활용)


○ MemberVO 구현
패키지 org.zerock.w2.domain
- 자바의 필드이름 데이터베이스와 동일하게
- 어노테이션 / 변수선언 하기

package org.zerock.w2.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberVO {
	
	private String mid; 
	private String mpw; 
	private String mname;
}


○ MemberDAO 구현
패키지 org.zerock.w2.domain

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 {
		String query = """
			SELECT mid, mpw, mname FROM tbl_member
				WHERE mid = ? AND mpw = ?;				
			""";
		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();
		// 데이터가 하나 있다는 것을 알고있어서 이렇게 했으나 실제로는 rs.next()사용하면 안됨
		rs.next();
		
		memberVO = MemberVO.builder()
				.mid(rs.getString(1))
				.mpw(rs.getString(2))
				.mname(rs.getString(3))
				.build();
		
		return memberVO;
		
	}
}


○  MemberDTO 구현
패키지 org.zerock.w2.domain

- 서비스 계층과 컨트롤러에서 사용

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;
}


○ MemberService 구현
패키지 org.zerock.w2.service
- enum 클래스

- DAO이용

- 로그인 처리를 위한 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();
		modelMapper = MapperUtil.INSTANSE.get();
	}
	
	// 로그인 체크
	public MemberDTO login(String mid, String mpw) throws Exception {
		MemberVO vo = dao.getWithPassword(mid, mpw);
		MemberDTO memberDTO = modelMapper.map(vo, MemberDTO.class);
		
		return memberDTO;
	}
}

▷ 컨트롤러에서 로그인 연동
패키지 : org.zerock.w2.controller
서블릿 : LoginController
- TodoLoginController를 LoginController로 새로 만들기

- TodoLoginController(과거의 것)의 url 충돌하지 않게 변경하기

- doPost()에서 MemberService를 연동하여 실제로 로그인이 되도록 설정

- 정상적으로 로그인 된 경우 HttpSession을 이용해서 'userInfo' 객체로 저장

- 예외가 발생한 경우 login으로 이동(result라는 파라미터로 문제 발생을 알림)

- 데이터베이스에 추가한 유저만 로그인 가능

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 org.zerock.w2.domain.MemberDTO;
import org.zerock.w2.service.MemberService;

import lombok.extern.log4j.Log4j2;

@WebServlet(name = "loginController", value = "/login")
@Log4j2
public class LoginController extends HttpServlet {

	@Override
	protected void doGet(
			HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
		log.info("login get ...");
		req.getRequestDispatcher("/WEB-INF/login.jsp").forward(req, res);
	}
	
	@Override
	protected void doPost(
			HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
		log.info("login post ...");
		
		String mid = req.getParameter("mid");
		String mpw = req.getParameter("mpw");
		
		try {
			MemberDTO memberDTO = MemberService.INSTANCE.login(mid, mpw);
			// 세션정보에 로그인 성공 키 등록
			HttpSession session = req.getSession();
			session.setAttribute("userInfo", memberDTO);
			
			// 로그인 회원이용 페이지
			res.sendRedirect("/todo/list");
			
		} catch (Exception e) {
			res.sendRedirect("/login?result=error");
		}
	
	}
}

로그인 성공


▷ 로그인 실패 처리
○ login.jsp에 JSTL + EL추가

- 아이디와 비밀번호가 데이터베이스에 없는 경우 메세지 출력
- jstl 사용하기 위한 코드 상단에 태그 라이브러리 지시자 추가

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>    

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>      
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원 로그인 페이지</title>
</head>
<body>
    <!-- 로그인 실패 -->
    <c:if test="${param.result == 'error'}">
        <h1>로그인 에러</h1>
    </c:if>

    <form action="/login" method="post">
        아이디 : <input type="text" name="mid" value="" /><br />
        비밀번호 : <input type="password" name="mpw" value="" /><br />
        <input type ="submit" value="로그인" />
    </form>
</body>
</html>

로그인 실패 메세지

○ list.jsp에 EL 추가

- /WEB-INF/todo/list.jsp는 로그인한 사용자만 접근 가능

- list.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 />
	<h3>loginInfo : ${userInfo}</h3>
	<h3>loginInfo.mname : ${userInfo.mname}</h3>
	
	<h1>할일 목록 리스트</h1>
	<form action="/logout" method="post">
	<button>Logout</button>
	</form>
</body>
</html>

로그인한 사용자 정보 확인 가능


* EL의 Scope
Page Scope : 해당 페이지의 영역 ex)  list.jsp
Request Scope : 요청과 관련되어 연결이 끝날때(응답) 까지 연결되는 영역
Session Scope : 로그인한 유저의 특정 영역
Application Scope : 전역변수 

-> ${obj}를 출력하면 page > request > session > application순서대로 obj라는 객체를 찾는다