안녕하세요, 코린이의 코딩 학습기 채니 입니다.
개인 포스팅용으로 내용에 오류 및 잘못된 정보가 있을 수 있습니다.
전체적인 디렉토리 구조는 아래 포스팅 참고!
https://chanychu.tistory.com/307?category=980487
웹페이지의 로그인 기능 구현하기
header.jsp
tabindex
- 탭 순서 변경하기
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello MVC</title>
<link rel="stylesheet" href="<%=request.getContextPath() %>/css/style.css" />
</head>
<body>
<div id="container">
<header>
<h1>Hello MVC</h1>
<div class="login-container">
<!-- 로그인폼 시작 -->
<form id="loginFrm" name="loginFrm" action="<%= request.getContextPath()%>/member/login" method="POST">
<table>
<tr>
<td><input type="text" name="memberId" id="memberId" placeholder="아이디" tabindex="1"></td>
<td><input type="submit" value="로그인" tabindex="3"></td>
</tr>
<tr>
<td><input type="password" name="password" id="password" placeholder="비밀번호" tabindex="2"></td>
<td></td>
</tr>
<tr>
<td colspan="2"><input type="checkbox" name="saveId" id="saveId" />
<label for="saveId">아이디저장</label>
<input type="button" value="회원가입"></td>
</tr>
</table>
</form>
<!-- 로그인폼 끝-->
</div>
<!-- 메인메뉴 시작 -->
<nav>
<ul class="main-nav">
<li class="home"><a href="<%= request.getContextPath() %>">Home</a></li>
<li class="notice"><a href="#">공지사항</a></li>
<li class="board"><a href="#">게시판</a></li>
</ul>
</nav>
<!-- 메인메뉴 끝-->
</header>
<section id="content">
tabindex 속성을 이용하여 로그인 폼의 탭 순서를 변경해주었으며, post 방식으로 데이터를 전송해주었습니다.
아이디와 비밀번호에 대한 유효성 검사
- 유효성 검사를 통과한 데이터만 서버로 전송
<script>
window.onload = () => {
document.loginFrm.onsubmit = (e) => {
const memberId = document.querySelector("#memberId");
const password = document.querySelector("#password");
if(!/^.{4,}$/.test(memberId.value)) {
alert("유효한 아이디를 입력해주세요.");
memberId.select();
return false;
}
if(!/^.{4,}$/.test(password.value)) {
alert("유효한 비밀번호를 입력해주세요.");
password.select();
return false;
}
};
};
</script>
mvc 패키지 구조
DTO (Data Transfer Object)
- 데이터를 전송하는 클래스
Member class
package com.ce.mvc2.member.model.dto;
import java.sql.Date;
import java.sql.Timestamp;
public class Member {
private String memberId;
private String password;
private String memberName;
private MemberRole memberRole; // U/A
private Gender gender; // M/F
private Date birthday; // 날짜
private String email;
private String phone;
private String hobby;
private int point;
private Timestamp enrollDate; // 날짜, 시각
public Member() {
super();
// TODO Auto-generated constructor stub
}
public Member(String memberId, String password, String memberName, MemberRole memberRole, Gender gender,
Date birthday, String email, String phone, String hobby, int point, Timestamp enrollDate) {
super();
this.memberId = memberId;
this.password = password;
this.memberName = memberName;
this.memberRole = memberRole;
this.gender = gender;
this.birthday = birthday;
this.email = email;
this.phone = phone;
this.hobby = hobby;
this.point = point;
this.enrollDate = enrollDate;
}
// getter, setter, toString 생략
}
memberRole과 gender는 enum으로 생성!
enum (상수클래스)
- namespace를 가진 상수 모음
- public static final보다 훨씬 안전
package com.ce.mvc2.member.model.dto;
public enum MemberRole {
U, A;
}
public enum Gender {
M, F;
}
memberRole과 gender에는 MemberRole.A/U와 Gender.M/F만 올 수 있습니다.
JdbcTemplate 생성
이전에 jdbc 공부를 하면서 만들어놨던 템플릿을 사용하려고 합니다.
public class JdbcTemplate {
static String driverClass;
static String url; // db접속프로토콜@ip:포트:db명(sid)
static String user;
static String password;
static {
// datasource.properties의 내용을 Properties 객체로 불러오기
Properties prop = new Properties();
try {
// buildpath (/WEB-INF/classes) 하위에 있는 datasource.properties의 절대경로
// getResource 메소드에 전달된 path의 /는 /WEB-INF/classes를 의미
String filename = JdbcTemplate.class.getResource("/datasource.properties").getPath();
System.out.println("filename@JdbcTemplate = " + filename);
prop.load(new FileReader(filename));
driverClass = prop.getProperty("driverClass");
url = prop.getProperty("url");
user = prop.getProperty("user");
password = prop.getProperty("password");
} catch (IOException e1) {
e1.printStackTrace();
}
// driver class 등록 - application 실행 시 최초 1회만!
try {
Class.forName(driverClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* Connection 객체 생성
* setAutoCommit(false) - 트랙잭션을 직접 관리 (DQM, DML)
*/
public static Connection getConnection() {
Connection conn = null;
try {
conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
public static void close(Connection conn) {
try {
if(conn != null && !conn.isClosed())
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void close(PreparedStatement pstmt) {
try {
if(pstmt != null && !pstmt.isClosed())
pstmt.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void close(ResultSet rset) {
try {
if(rset != null && !rset.isClosed())
rset.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void commit(Connection conn) {
try {
//conn이 null이 아니고, 닫혀있지 않다면 커밋처리
if(conn != null && !conn.isClosed()) // isClosed() : 반환되었다면 true리턴
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void rollback(Connection conn) {
try {
if(conn != null && !conn.isClosed())
conn.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
현재 클래스의 클래스 객체를 가져와서 (class()) buildpath에 "/datasource.properties"파일의 URL객체를 반환!(getResource()) 후 절대 경로 (getPath())를 가져와줍니다.
datasource.properties
###################################################
# datasource.properties
###################################################
driverClass = oracle.jdbc.OracleDriver
url = jdbc:oracle:thin:@localhost:1521:xe
user = web2
password = 비밀번호
중요한 정보들을 따로 datasource 파일로 빼놓았었습니다.
해당 파일을 가져와서 사용해야 하는데, 프로젝트에 source folder를 추가해줍니다. (폴더이름 - src/main/resources)
웹 프로젝트를 실행하는 것이기 때문에 tomcat 밑에서 프로젝트를 실행하게 됩니다.
따라서 tomcat 밑에서 실행할 때는 모든 파일들이 src/main/webapp 밑에 존재해야 합니다.
buildpath인 classes 디렉토리에는 실제 src/main/java의 컴파일된 파일들의 디렉토리도 확인되며, src/main/resource에 있는 properties 파일도 확인됩니다.
source folder는 source folder 하위에 내용을 buildpath로 반영해줍니다. (따라서 tomcat에서 참조하여 실행 가능!)
즉, 실제 작업 파일 (working file)은 src/main/resources/datasource.properties이지만, 실행 파일(running file)은 src/main/webapp/WEB-INF/classes/datasource.properties입니다.
member-query.properties
- 쿼리 관리
#######################################
# member-query.properties
#######################################
findById = select * from member where member_id = ?
필요한 파일들과 클래스들을 모두 생성했다면, 데이터를 가져와보도록 하겠습니다.
DQL
로그인 여부 판단
Controller
MemberLoginServlet
@WebServlet("/member/login")
public class MemberLoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private MemberService memberService = new MemberService();
/**
* @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
*/
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. 인코딩 처리
request.setCharacterEncoding("utf-8");
// 2. 사용자 입력 값 처리
String memberId = request.getParameter("memberId");
String password = request.getParameter("password");
System.out.println("memberId = " + memberId);
System.out.println("passowrd = " + password);
// 3. 업무로직 : 로그인 여부 판단
Member member = memberService.findById(memberId);
System.out.println("member@MemberLoginServlet = " + member);
// 4. 응답 처리
RequestDispatcher reqDispatcher = request.getRequestDispatcher("/index.jsp");
reqDispatcher.forward(request, response);
}
}
Service
MemberService
public class MemberService {
private MemberDao memberDao = new MemberDao();
/**
* DQL 요청 - service
* 1. Connection 객체 생성
* 2. Dao 요청 & Connection 전달
* 3. Connection 반환 (close)
*/
public Member findById(String memberId) {
Connection conn = getConnection();
Member member = memberDao.findById(memberId, conn);
close(conn);
return member;
}
}
Dao
MemberDao
public class MemberDao {
private Properties prop = new Properties();
public MemberDao() {
String filename = MemberDao.class.getResource("/sql/member/member-query.properties").getPath();
System.out.println("filename@MemberDao = " + filename);
try {
prop.load(new FileReader(filename));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* DQL 요청 - DAO
* 1. PreparedStatement 객체 생성 (sql 전달) & 값 대입
* 2. 쿼리실행 executeQuery - ResultSet 반환
* 3. ResultSet 처리 - dto 객체 변환
* 4. ResultSet, preparedStatement 객체 반환
*/
public Member findById(String memberId, Connection conn) {
PreparedStatement pstmt = null;
ResultSet rset = null;
Member member = null;
String sql = prop.getProperty("findById");
try {
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
rset = pstmt.executeQuery();
while(rset.next()) {
memberId = rset.getString("member_id");
String password = rset.getString("password");
String memberName = rset.getString("member_name");
MemberRole memberRole = MemberRole.valueOf(rset.getString("member_role"));
String _gender = rset.getString("gender");
Gender gender = _gender != null ? Gender.valueOf(_gender) : null; // Gender.M / Gender.F로 변환
Date birthday = rset.getDate("birthday");
String email = rset.getString("email");
String phone = rset.getString("phone");
String hobby = rset.getString("hobby");
int point = rset.getInt("point");
Timestamp enrollDate = rset.getTimestamp("enroll_date");
member = new Member(memberId, password, memberName, memberRole, gender, birthday, email, phone, hobby, point, enrollDate);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
close(rset);
close(pstmt);
}
return member;
}
sql를 모아놓은 properties 파일을 가져오기 위하여 MemberDao 생성자 안에 properties 파일을 load하는 코드를 짰습니다.
서버 실행 후 아이디/비밀번호 입력 - 콘솔 출력값
filename@MemberDao = /C:/Workspaces/web_server_workspace/hello-mvc2/src/main/webapp/WEB-INF/classes/sql/member/member-query.properties
memberId = honggd
passowrd = 1234
filename = /C:/Workspaces/web_server_workspace/hello-mvc2/src/main/webapp/WEB-INF/classes/datasource.properties
member@MemberLoginServlet = Member [memberId=honggd, password=1234, memberName=홍길동, memberRole=U, gender=M, birthday=2000-09-09, email=honggd@naver.com, phone=01012341234, point=1000, enrollDate=2022-06-20 14:15:57.0]
데이터들을 잘 가져오는 것을 확인할 수 있습니다.
로그인 성공 여부에 따른 분기처리
Controller
MemberLoginServlet
// 3. 업무로직 : 로그인 여부 판단
Member member = memberService.findById(memberId);
System.out.println("member@MemberLoginServlet = " + member);
HttpSession session = request.getSession();
if(member != null && password.equals(member.getPassword())) {
// 로그인 성공
session.setAttribute("loginMember", member);
} else {
// 로그인 실패
session.setAttribute("msg", "아이디 또는 비밀번호가 일치하지 않습니다.");
}
request는 매 요청이 있을 때마다 request/response 객체가 생성됩니다.
따라서 session이 아닌, request에 속성을 저장한다면 페이지를 이동 혹은 새로고침할 때마다 request 객체가 생성되어 로그인이 풀려버리는 것이죠.
따라서 request보다 생명 주기가 긴 session을 이용하여 로그인 멤버 처리를 하였습니다.
header.jsp
<%@page import="com.ce.mvc2.member.model.dto.Member"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String msg = (String)session.getAttribute("msg");
Member loginMember = (Member)session.getAttribute("loginMember");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello MVC</title>
<link rel="stylesheet" href="<%=request.getContextPath() %>/css/style.css" />
<script>
window.onload = () => {
<% if(msg != null) {%>
alert("<%= msg %>");
<% } %>
<% if(loginMember == null) { %>
document.loginFrm.onsubmit = (e) => {
const memberId = document.querySelector("#memberId");
const password = document.querySelector("#password");
if(!/^.{4,}$/.test(memberId.value)) {
alert("유효한 아이디를 입력해주세요.");
memberId.select();
return false;
}
if(!/^.{4,}$/.test(password.value)) {
alert("유효한 비밀번호를 입력해주세요.");
password.select();
return false;
}
};
<% } %>
};
</script>
</head>
<body>
<div id="container">
<header>
<h1>Hello MVC</h1>
<div class="login-container">
<!-- 로그인폼 시작 -->
<% if(loginMember == null) { %>
<form id="loginFrm" name="loginFrm" action="<%= request.getContextPath()%>/member/login" method="POST">
<table>
<tr>
<td><input type="text" name="memberId" id="memberId" placeholder="아이디" tabindex="1"></td>
<td><input type="submit" value="로그인" tabindex="3"></td>
</tr>
<tr>
<td><input type="password" name="password" id="password" placeholder="비밀번호" tabindex="2"></td>
<td></td>
</tr>
<tr>
<td colspan="2"><input type="checkbox" name="saveId" id="saveId" />
<label for="saveId">아이디저장</label>
<input type="button" value="회원가입"></td>
</tr>
</table>
</form>
<!-- 로그인폼 끝-->
<% } else { %>
<table id="login">
<tr>
<td>[<%=loginMember.getMemberName() %>]님, 안녕하세요!</td>
</tr>
<tr>
<td>
<input type="button" value="내정보보기" />
<input type="button" value="로그아웃" />
</td>
</tr>
</table>
<% } %>
</div>
<!-- 메인메뉴 시작 -->
<nav>
<ul class="main-nav">
<li class="home"><a href="<%= request.getContextPath() %>">Home</a></li>
<li class="notice"><a href="#">공지사항</a></li>
<li class="board"><a href="#">게시판</a></li>
</ul>
</nav>
<!-- 메인메뉴 끝-->
</header>
<section id="content">
session에 속성 등록을 하였기 때문에, 페이지를 이동하거나 새로고침을 해도 로그인 상태가 잘 유지 되어있습니다.
다만, 로그인 시 UML이 그대로 남아있기 때문에 새로고침을 하면 또 로그인 요청이 들어가게 됩니다.
따라서 UML을 필히 변경해줘야 합니다. (DML도!)
UML 변경
Controller
MemberLoginServlet
response.sendRedirect()
// 4. 응답 처리 : 로그인 후 url변경을 위해 리다렉트 처리
response.sendRedirect(request.getContextPath() + "/"); // /mvc/
/mvc/로 url을 변경하였습니다.
그렇다면, 응답 메세지에 302 redirect를 전송하고 브라우저에게 location (/mvc/)으로 재요청을 명령합니다.
브라우저의 네트워크 탭을 확인해보겠습니다.
기존 login은 302 상태, mvc2/는 200(정상처리) 상태로 되어있는 것을 확인할 수 있습니다.
login의 헤더를 확인해보겠습니다.
location에 요청한 '/mvc2/'가 정의 되어있는 것을 확인할 수 있습니다.
즉, login이 redirect 처리(302)가 되면 location에 정의된 UML로 이동해달라(재요청)! 라고 명령하는 것이죠.
※ redirect는 반드시 GET 방식!!
로그인을 실패했을 때도 /mvc2/로 잘 이동하는 것을 확인할 수 있습니다.
다만, 로그인 실패 후 새로고침을 해도 해당 alert가 계속 나타납니다.
그 이유는 msg 속성 또한 session에 저장해두었기 때문이죠. session이 제거되지 않는 한 계속해서 해당 메세지가 나올 것입니다. (심지어 로그인 성공을 해도!!!)
header.jsp
removeAttribute()
<%
String msg = (String)session.getAttribute("msg");
if(msg != null) {
session.removeAttribute("msg"); // 한번만 사용 후 제거
}
Member loginMember = (Member)session.getAttribute("loginMember");
%>
removeAttribute를 이용하여 한 번만 사용하고 제거해줍니다.
로그인 실패 후 최초 1회만 alert가 실행되고 이후로는 실행되지 않는 것을 확인할 수 있습니다.
'Java > Servlet & JSP' 카테고리의 다른 글
JSP) 기본적인 session과 cookie의 흐름, session의 유효기간 (0) | 2022.06.22 |
---|---|
JSP) 웹 페이지 로그아웃 기능 구현하기 (0) | 2022.06.21 |
JSP) SQL과 함께 사용하기 (접속 테스트 및 파일 구조) (0) | 2022.06.20 |
JSP) JSP에 데이터 전달 (setAttribute, getAttribute) (0) | 2022.06.20 |
JSP) 페이지 재사용 - include 처리 (0) | 2022.06.18 |