JSP/Servlet - 게시판 - 회원기능!
게시판 구축하기
지금까지 인증처리, 간단한 게시판 구축 등을 했는데 이번엔 진짜 회원가입부터 로그인까지 가능한 하나의 완전한 웹 어플리케이션을 만들어보자.
프로젝트명은 jspPro, 파일 위치는 board21라는 폴더와 패키지 안에 게시판 관련된 파일들을 생성하자.
DB연결
DB연결과정은 기존에 있던것을 사용하자.
/WEB-INF/lib
폴더에 ojdbc6.jar
파일을 넣고 톰캣(WAS)서버가 제공하는 커넥션 풀 객체를 사용하자
public class ConnectionProvider {
public static Connection getConncection() throws NamingException, SQLException
{
Context initContext = new InitialContext();
Context envContext = (Context)initContext.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/myoracle");
Connection conn = ds.getConnection();
return conn;
}
}
https://kouzie.github.io/jsp/JSP-DBCP,-세션/
그리고 DB연결객체를 close
, rollback
할수 있도록 도와주는 JdbcUtil
클래스 정의하도록 하자.
public class JdbcUtil {
public static void close(ResultSet rs) {
if (rs != null) {
try { rs.close(); }
catch (SQLException ex) { }
}
}
public static void close(Statement stmt) {
if (stmt != null) {
try { stmt.close(); }
catch (SQLException ex) { }
}
}
public static void close(Connection conn) {
if (conn != null) {
try { conn.close(); }
catch (SQLException ex) { }
}
}
public static void rollback(Connection conn) {
if (conn != null) {
try { conn.rollback(); }
catch (SQLException ex) { }
}
}
}
굳이 이런 클래스를 만든 이유는 서블릿이나 jsp에서 close할 때 항상 try
, catch
문으로 감싸주어야 했는데 이를 간략하기 위해 사용하는 클래스
https://kouzie.github.io/jsp/JSP-DBCP,-세션/#dbcp-database-connection-pool
먼저 회원가입을 위한 회원테이블을 생성하자.
CREATE TABLE member21 (
memberid VARCHAR(50) PRIMARY KEY,
name VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
regdate DATE DEFAULT SYSDATE
);
Member - Member21에서 사용자 데이터저장용 DTO 객체
테이블에서 가져온 데이터를 저장, 관리용 DTO객체를 정의하자.
Meber21
테이블 속성에 해당하는 필드를 정의하고 getter, setter 메서드를 자동 생성.
추가적으로 회원가입시 패스워드와 확인용 패스워드가 일치하는지 검사하는 matchPassword
메서드를 정의한다.
public class Member {
private String memberid;
private String name;
private String password;
private Date regdate;
public Member(String memberid, String name, String password, Date regdate) {
this.memberid = memberid;
this.name = name;
this.password = password;
this.regdate = regdate;
}
public String getMemberid() {
return memberid;
}
public void setMemberid(String memberid) {
this.memberid = memberid;
}
...
...
public boolean matchPassword(String pwd) {
return password.equals(pwd);
}
MemberDao - 멤버 객체 삽입, 검색 클래스
실제 Membe21
테이블에 멤버를 추가하고 기존에 멤버가 있는지 검색하는 MemberDAO
객체를 정의한다.
public class MemberDao {
public Member selectById(Connection conn, String memberid) {
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = conn.prepareStatement("SELECT * FROM member21 WHERE memberid = ?");
pstmt.setString(1, memberid);
rs = pstmt.executeQuery();
Member member = null;
if (rs.next()) {
member = new Member(
rs.getString("memberid"),
rs.getString("name"),
rs.getString("password"),
rs.getDate("regdate")
);
return member;
}
} catch (Exception e) {
System.out.println(e);
} finally {
JdbcUtil.close(rs);
JdbcUtil.close(pstmt);
}
}
private Date toDate(Timestamp date){
return date == null ? null : new Date(date.getTime());
}
public void insert(Connection conn, Member mem) throws SQLException {
try (PreparedStatement pstmt
= conn.prepareStatement("INSERT INTO (memberid, name, password) member21 value(?,?,?)");)
{
pstmt.setString(1, mem.getMemberid());
pstmt.setString(2, mem.getName());
pstmt.setString(3, mem.getPassword());
pstmt.executeQuery();
} catch (Exception e) {
System.out.println(e);
}
}
}
회원가입을 위한 DTO, DAO객체를 만들었으니 이벤트 처리용 객체들을 만들어보자.
컨트롤러 정의
*.do
확장자를 요청하는 모든 url은 컨트롤러의 제어를 받아 요청, 응답되어진다.
저번 url기반 컨트롤러 정의했던 것 과 전혀 달라진 것 이 없다.
https://kouzie.github.io/jsp/JSP-MVC패턴/#요청-uri명령-기반-컨트롤러
<servlet>
<servlet-name>ControllerUsingURI</servlet-name>
<servlet-class>board21.mvc.controller.ControllerUsingURI</servlet-class>
<init-param>
<param-name>configFile</param-name>
<param-value>/WEB-INF/view/board21/commandHandler.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ControllerUsingURI</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
public class ControllerUsingURI extends HttpServlet{
private Map<String, CommandHandler> commandHandlerMap = new HashMap<>();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
process(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
process(request, response);
}
@Override
public void init() throws ServletException {
String configFile = getInitParameter("configFile");
Properties prop = new Properties();
String configFilePrath = getServletContext().getRealPath(configFile);
System.out.println(configFile);
try(FileInputStream fis = new FileInputStream(configFilePrath))
{
prop.load(fis);
} catch (IOException e) {
throw new ServletException(e);
}
Iterator<Object> keyiter = prop.keySet().iterator();
while (keyiter.hasNext()) {
String command = (String) keyiter.next();
String handlerClassName = prop.getProperty(command);
try {
Class<?> handlerClass = Class.forName(handlerClassName);
CommandHandler handlerInstance = (CommandHandler) handlerClass.newInstance();
commandHandlerMap.put(command, handlerInstance);
}
catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new ServletException(e);
}
}
}
private void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String command = request.getRequestURI();
if (command.indexOf(request.getContextPath()) == 0) {
command = command.substring(request.getContextPath().length());
}
System.out.println(command);
CommandHandler handler = commandHandlerMap.get(command);
if (handler == null) {
handler = new NullHandler(); //404에러를 응답하는 핸들러 클래스
}
String viewPage = null;
try {
viewPage = handler.process(request, response);
}
catch (Exception e) {
throw new ServletException(e);
}
if(viewPage != null) {
String prefix = "/WEB-INF/view/board21";
viewPage = prefix + viewPage;
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPage);
System.out.println(viewPage);
dispatcher.forward(request, response);
}
}
}
서버 시작시 commandHandler.properties
파일을 열어 url명령과 처리할 서블릿 객체를 Map
객체를 통해 목록화 시켜 가지고 있고
명령이 들어올 때 마다 Map
에서 알맞은 서블릿 객체를 꺼내 process(request, response)
메더스들 호출한다.
그리고 서블릿 객체가 반환한 View페이지로 포워딩 시키면 끝!
viewPage
가 not null
일 경우에만 포워딩 되기 때문에 null이 반환된 경우 아무것도 하지 않는다.(그냥 response객체를 반환할뿐….)
이벤트 처리 서블릿
commandHandler.properties
설정 파일에 따라 이벤트가 처리되는데 회원가입의 경우 join.do
라는 url을 서버에 요청한다.
위의 컨트롤러가 관리하는 이벤트 처리목록용 Map
객체 안의 요소가 CommandHandler
이다.
private Map<String, CommandHandler> commandHandlerMap = new HashMap<>();
CommandHandler
는 모든 이벤트 처리용 서블릿 객체(Model)이 상속하는 인터페이스로 다형성을 활용하여 하나의 Map
객체로 다양한 이벤트 처리 클래스를 관리할 수 있다.
public interface CommandHandler {
public String process(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
아무런 *.do
url패턴이지만 등록하지 않은 이벤트가 요청 될 수 있다.
위의 컨트롤러에서도 Map
에서찾는 이벤트 객체가 없어 null
을 반환했을 때 예외처리를 하였는데
NullHandler
로 이벤트를 처리하는 것이다.
CommandHandler handler = commandHandlerMap.get(command);
if (handler == null) {
handler = new NullHandler(); //404에러를 응답하는 핸들러 클래스
}
등록되지 않는 이벤트를 처리하는 Modal 객체인 NullHandler
를 아래와 같이 정의하자.
public class NullHandler implements CommandHandler {
@Override
public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return null;
}
}
그냥 404에러를 response
객체에 저장.
회원가입 기능 구현
DB 유틸과 컨트롤러를 위한 기본적인 객체들 정의가 끝났으니 회원가입 기능을 구현하도록 해보자.
이번 게시판 구축에서 MVC패턴은 모두 아래와 같은 형식으로 흘러간다.
Controlloer - Handler - service - dao(sql쿼리)
컨트롤러에 요청을 받아 요청에 맞는(url패턴으로 요청 구분) 핸든러의 처리메서드(process()
)를 호출한다.
Handler
는 실직적으로 요청을 처리(sql문 실행)하는 service
클래스를 만들어 각 기능을 가진 메서드를 호출한다.
왜 굳이 service
객체를 중간에 끼워두는지 궁금하다면 아래 링크 참조
https://kouzie.github.io/jdbc/JDBC.-4일차/#게시판-mvc-패턴으로-구성하기
service
클래스는 요청에 대한 응답정보(회원가입 성공/실패, 게시글 목록리스트 등)을 Handler
에게 반환하고
Handler
는 요청에 대한 응답데이터를 뿌려줄 View (응답데이터를 뿌릴 jsp) 주소를 Controller
에게 반환한다.
Controller
는 View 주소로 포워딩 시켜 클라이언트에게 jsp페이지로 안내한다.
회원가입, 로그인, 게시글목록보기 등 가릴 것 없이 모두 위와 같은 MVC패턴으로 진행시킨다.
회원가입(Join
)역시 Controller - JoinHandler - JoinService - MemberDao
형식으로 진행한다.
JoinHandler - 회원가입 이벤트처리
이벤트 처리 클래스를 등록하기 위해 commandHandler.properties
에 아래처럼 추가하자.
/join.do=board21.mvc.commmand.JoinHandler
이제 컨트롤러는 join.do
라는 url패턴으로 요청이 들어오면 JoinHandler
객체의 process()
메서드를 호출하게 될 것이다.
아래 회원가입 링크를 누르면
<a href="<%= request.getContextPath() %>/join.do">회원가입</a>
a
태그를 눌러 요청하는 것은 당연히 get
방식으로 요청이 서버로 전송될 것이고 서버는 회원가입 페이지로 포워딩 시킨다.
회원가입 데이터를 입력하고 submit
버튼을 누르면 post방식으로 요청이 서버로 전송될 것이고 서버는 회원가입 결과페이지로 포워딩 시킨다.
모든것이 포워딩 작업으로 이루어지기 때문에 같은 join.do
url로 get
, post
요청을 한다.
JoinHandler
는 get
방식요청일 경우 joinForm.jsp
페이지 url을 반환하고
post
방식요청일 경우 joinSuccess.jsp
페이지 url을 반환한다.
먼저 joinForm.jsp
을 만들어보자.
<!-- joinForm.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<title>회원가입</title>
</head>
<body>
<h3>JoinForm.jsp</h3>
<form action="join.do" method="post">
<p>
아이디: <input type="text" name="memberid" value="${ param.id }" />
<c:if test="${ errors.memberid }">
ID를 입력하세요
</c:if>
<c:if test="${ errors.duplicateId }">
이미 사용중인 ID입니다.
</c:if>
</p>
<p>
이름: <input type="text" name="name" value="${ param.name }"/>
<c:if test="${ errors.name }">
이름을 입력하세요
</c:if>
</p>
<p>
암호: <input type="password" name="password" value="${ param.password }"/>
<c:if test="${ errors.password }">
암호를 입력하세요
</c:if>
</p>
<p>
확인: <input type="password" name="comfirmPassword" value="${ param.comfirmPassword }" />
<c:if test="${ errors.confirmPassword }">
확인을 입력하세요
</c:if>
<c:if test="${ errors.notMatch }">
암호와 확인이 일치하지 않습니다.
</c:if>
</p>
<input type="submit" value="가입" />
</form>
</body>
</html>
중간중간에 errors
객체의 각종 속성을 검사하여 true
일 경우 오류문구를 출력한다.
먼저 이 error객체에 설명하고 가면 Map<String, Boolean> errors = new HashMap<>();
인런식으로 정의되어 있다.
key
로 문자열 notMatch
를, value
로 boolean
값을 두어 각종 오류가 발생했는지 발생하지 않았는지 errors
라는 Map
객체 하나로 확인할 수 있다.
오늘 알게된 사실: 코어태그로 Map객체의 name에 해당하는 value도 가져올 수 있다.
그럼 이제 가입결과 페이지 joinSuccess.jsp
를 만들자.
<!-- joinSuccess.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원가입 - 결과</title>
</head>
<body>
<h3>joinSuccess.jsp</h3>
${ param.name }님, 가입을 환영합니다.<br>
</body>
</html>
사실 회원가입 실패의 경우 errors
객체를 가지고 joinForm.jsp
로 포워딩 시킬 것이기 때문에 성공할 경우에만 joinSuccess.jsp
로 이동시킨다.
즉 JoinHandler
Model객체는 get
방식일때, 혹은 에러가 발생했을 경우 joinForm.jsp
문자열을 반환하고
post
방식일 때 joinSuccess.jsp
문자열을 반환한다.
public class JoinHandler implements CommandHandler{
private static final String FORM_VIEW = "/joinForm.jsp";
private JoinService joinService = new JoinService();
@Override
public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getMethod().equalsIgnoreCase("GET")) {
return processForm(request, response);
}
else if (request.getMethod().equalsIgnoreCase("POST")) {
return processSubmit(request, response);
}
else {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return null;
}
}
private String processForm(HttpServletRequest request, HttpServletResponse response) {
return FORM_VIEW;
}
private String processSubmit(HttpServletRequest request, HttpServletResponse response) {
JoinRequest joinReq = new JoinRequest();
//받은 요청을 유효한 값인지 확인하는 JoinRequest 객체
joinReq.setMemberid(request.getParameter("memberid"));
joinReq.setName(request.getParameter("name"));
joinReq.setPassword(request.getParameter("password"));
joinReq.setConfirmPassword(request.getParameter("comfirmPassword"));
Map<String, Boolean> errors = new HashMap<>();
request.setAttribute("errors", errors);
joinReq.validate(errors);
if (!errors.isEmpty()) {
//errors안에 값이 있다면
return FORM_VIEW;
}
try {
joinService.join(joinReq);
//INSERT작업 수행
return "/joinSuccess.jsp";
} catch (DuplicationException e) {
errors.put("duplicateId", Boolean.TRUE);
return FORM_VIEW;
}
}
}
살펴보아야 할것은 post
방식에서 호출되는 processSubmit()
메서드
JoinRequest
객체는 사용자가 joinForm.jsp
에서 입력할 값들이 유효한지 확인하는 객체이다.
validate()
메서드를 통해 errors
객체에 유효한지 결과를 추가한다.
public class JoinRequest {
private String memberid;
private String name;
private String password;
private String confirmPassword;
public boolean isPasswordEqualToConfirm()
{
return password != null && password.equals(confirmPassword);
}
public void validate(Map<String, Boolean> errors) {
checkEmpty(errors, memberid, "memberid");
checkEmpty(errors, name, "name");
checkEmpty(errors, password, "password");
checkEmpty(errors, confirmPassword, "confirmPassword");
//비어있거나 null값이면 FALSE,
//comfirmPassWord가 TRUE, 서로 일치한다면 notMatch에 TRUE
if (!errors.containsKey("confirmPassword")) {
if(!isPasswordEqualToConfirm()) {
errors.put("notMatch", Boolean.TRUE);
}
}
}
private void checkEmpty(Map<String, Boolean> errors, String value, String fieldName) {
if (value == null || value.isEmpty()) {
errors.put(fieldName, Boolean.TRUE);
}
}
public String getMemberid() {
return memberid;
}
public void setMemberid(String memberid) {
this.memberid = memberid;
}
...
...
}
JoinRequest
는 Member
DTO객체와 유사한데 회원가입을 위한 DTO라고 볼수 있다.
회원데이터는 회원가입 뿐 아니라 다방면에서 쓰여야 하는데 회원가입 때문에 여러 메서드를 Member
DTO객체에 주렁주렁 달고 있을 필요는 없기 때문에 JoinRequest
를 별도로 생성한것.
그리고 JoinRequest
객체에서 ID를 SELECT
해와서 ID중복체크를 하지 않는데 중복체크는 joinService.join()
메서드에서 발생한다.
다시 JoinHandler
의 processSubmit()
메서드를 살펴보면 joinService.join()
메서드에서 중복예외객체인 DuplicationException
를 throw한다.
/* JoinHandler */
...
try {
joinService.join(joinReq);
//INSERT작업 수행
return "/joinSuccess.jsp";
} catch (DuplicationException e) {
errors.put("duplicateId", Boolean.TRUE);
return FORM_VIEW;
}
...
실제 JoinService
가 MemberDAO
사용해 INSERT
쿼리를 수행하기 전 SELECT
쿼리를 수행해 중복체크를 하는데
만약 클라이언트가 넘긴 ID값이 DB에 이미 존재한다면 직접 정의한 예외 DuplicationException
객체가 throw
된다.
public class DuplicationException extends RuntimeException{
}
그냥 구분하기 위해 만들어둔 깡통 예외객체…
즉 JoinService
INSERT
하기 전 memberDao.selectById()
메서드를 통해 이미 ID가 존재하는지 체크하고 DuplicationException
예외를 throw
할지 말지 결정한다.
ID가 존재하지 않을경우 memberDao.insert()
메서드로 입력받은 JoinRequest
객체로 Member
DTO객체를 생성하여 INSERT
하고 끝
public class JoinService {
private MemberDao memberDao = new MemberDao();
public void join(JoinRequest joinReq)
{
Connection conn = null;
try {
conn = ConnectionProvider.getConncection();
conn.setAutoCommit(false);
//자동 커밋 X
Member member = memberDao.selectById(conn, joinReq.getMemberid());
if (member != null) {
// 이미 MemberId값이 존재한다면
JdbcUtil.rollback(conn);
throw new DuplicationException();
}
memberDao.insert(conn, new Member(
joinReq.getMemberid(),
joinReq.getName(),
joinReq.getPassword(),
new Date()
));
conn.commit();
} catch (SQLException | NamingException e) {
System.out.println("joinSerivce");
JdbcUtil.rollback(conn);
throw new RuntimeException(e);
} finally {
JdbcUtil.close(conn);
}
}
}
회원가입 요약
commandHandler.properties
에 JoinHandler
등록
get방식일 때 joinForm.jsp
반환, post방식일 때 processSubmit()
메서드 호출
processSubmit()
에서 errors
객체로 유효값 확인 및 SQL쿼리 수행
joinServiec
에서 SELECT
쿼리 수행 후 ID가 이미 존재한다면 DuplicationException
예외 발생
ID중복 없을경우 INSERT
쿼리 수행
INSERT
쿼리까지 수행되면 JoinHandler
는 joinSuccess.jsp
반환.
로그인 기능 구현
회원가입을 끝냈으니 로그인 기능을 구현해보자.
로그인을 진행하기 전에 메인페이지인 index.jsp
를 소개하겠다.
<!-- index.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>INDEX</title>
</head>
<body>
<h3>index.jsp</h3>
<c:if test="${ !empty authUser }">
${ authUser.name }님 안녕하세요
<br>
<a href="<%= request.getContextPath() %>/logout.do">로그아웃</a>
<a href="<%= request.getContextPath() %>/changePwd.do">비밀번호 변경</a>
</c:if>
<c:if test="${ empty authUser }">
<a href="<%= request.getContextPath() %>/join.do">회원가입</a>
<a href="<%= request.getContextPath() %>/login.do">로그인</a>
<a href="<%= request.getContextPath() %>/changePwd.do">비밀번호 변경</a>
</c:if>
</body>
</html>
아직 로그인 하지 않았기 때문에 로그아웃은 출력되지 않는다.
index.jsp
역시 /WEB-INF/view/board21
폴더 안에 넣을 것 이기 때문에 핸들러가 포워딩을 시켜주어야 index.jsp
로 이동 가능하다.
포워딩 시켜주는 IndexHandler
를 Controller
에 등록시키고 이벤트 처리시키도록 설정하다.
index.do
라는 url패턴이 요청되면 index.jsp로 포워딩 시키는 핸들러이다.
public class IndexHandler implements CommandHandler {
private static final String FORM_VIEW = "/index.jsp";
@Override
public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
return FORM_VIEW;
}
}
commandHandler.properties
파일에 아래와 같이 추가
/index.do=borad21.command.IndexHandler
LoginHandler - 로그인 이벤트 처리 Model
로그인 기능 역시 회원가입과 같은 방향으로 흘러갈 것이다.
이번엔 먼저 로그인 전체과정을 보고 시작하자.
Controller
가 LoginHandler
를 호출하고 get, post방식에 따라 loginForm.jsp
로 바로 이동시킬 것 인지, LoginService
객체의 login과정을 진행시킬 것 인지 결정한다.
LoginService
의 로그인 과정에선 ID, PW가 일치하는지 검사하고 일치한다면 User
객체를 request
객체에 저장하여 index.jsp
로그인 유지는 session
으로 유지할 것이다.
로그인한다면 authUser
라는 세션속성에 인증관련 값을 보관, 관련값은 User라는 객체…
로그아웃시 세션을 종료시킨다.
commandHandler.properties
파일에 아래와 같이 추가
/login.do=board21.auth.command.LoginHandler
public class LoginHandler implements CommandHandler{
private static final String FORM_VIEW = "/loginForm.jsp";
private LoginService loginService = new LoginService();
@Override
public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getMethod().equalsIgnoreCase("GET")) {
System.out.println("LoginHandler preocess GET");
return processForm(request, response);
}
else if (request.getMethod().equalsIgnoreCase("POST")) {
System.out.println("LoginHandler preocess POST");
return processSubmit(request, response);
}
else {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return null;
}
}
private String processForm(HttpServletRequest request, HttpServletResponse response) {
return FORM_VIEW;
}
...
...
여기까지는 다른 핸들러 객체들과 다른점이 없다.
맨 처음 get방식으로 login.do
url패턴이 요청되면 아래 jsp페이지로 포워딩한다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>
<h3>loginForm.jsp</h3>
<form action="/jspPro/login.do" method="post">
<c:if test="${ errors.idOrPwNotMatch }">
아이디와 암호가 일치하지 않습니다.
</c:if>
<p>
아이디: <input type="text" name="id" value="${ param.id }"/>
<c:if test="${ errors.id }">ID를 입력하세요</c:if>
</p>
<p>
암호: <input type="password" name="password"/>
<c:if test="${ errors.password }">암호를 입력하세요</c:if>
</p>
<input type="submit" value="로그인" />
</form>
</body>
</html>
이 상태에서 id
와 password
를 입력후 submit버튼을 누르면 processSubmit
호출과정에서 입력받은 id
, password
가 유효한 값인지 확인하고
loginService.login(memberid, password)
를 통해 맞는 id
와 password
인지 확인하면 된다.
...
...
private String processSubmit(HttpServletRequest request, HttpServletResponse response) {
String memberid = trim(request.getParameter("id"));
String password = trim(request.getParameter("password"));
Map<String, Boolean> errors = new HashMap<>();
request.setAttribute("errors", errors);
if (memberid == null || memberid.isEmpty()) {
errors.put("id", Boolean.TRUE);
}
if (password == null || password.isEmpty()) {
errors.put("password", Boolean.TRUE);
}
if (!errors.isEmpty()) {
return FORM_VIEW;
}
try {
User user = loginService.login(memberid, password);
request.getSession().setAttribute("authUser", user);
response.sendRedirect(request.getContextPath()+"/index.do");
return null;
} catch (LoginFailException e) {
errors.put("idOrPwNotMatch", Boolean.TRUE);
return FORM_VIEW;
} catch (IOException e) {
e.printStackTrace();
return FORM_VIEW;
}
}
private String trim(String str) {
return str == null ? null : str.trim();
}
}
위 jsp페이지의 오류 확인을 위해 여기서도 errors
객체를 초기화한다.
LoginService - 실제 SELECT처리 호출 클래스
LoginService
의 login
메서드를 통해 인증객체인 User
를 가져오는데
MemberDAO
의 selectById
메서드를 통해 Member
객체를 가져오고
Member
객체로 User
객체를 초기화한다.
public class LoginService {
private MemberDao memberDAO = new MemberDao();
public User login(String id, String password) throws LoginFailException
{
System.out.println("login");
try(Connection conn = ConnectionProvider.getConncection();)
{
Member member = memberDAO.selectById(conn, id);
if (member == null) {
System.out.println("login faile");
throw new LoginFailException();
}
if (!member.matchPassword(password)) {
System.out.println("login password failed");
throw new LoginFailException();
}
return new User(member.getMemberid(), member.getName());
} catch (SQLException | NamingException e) {
throw new RuntimeException(e);
}
}
}
selectById()
안의 SELECT
쿼리로 가져온 값이 없다면 LoginFailException
예외를 LoginHandler
에 throw
하여 loginForm.jsp
로 다시 포워딩 된다.
로그인 과정은 SELECT
문 하나로 Member
가 있는지 없는지만 검사하면 되기 때문에 회원가입 과정보다 더 쉽다.
다른 직접 정의한 예외처럼 LoginFailException
역시 깡통 예외객체이다.
public class LoginFailException extends RuntimeException {
}
비밀번호 때문에 틀렸는지, ID 때문인지 ERRCODE
를 필드로 갖는 것 도 나쁘지 않다.
인증 객체 User
는 다음과 같다.
public class User {
private String id;
private String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
}
LoginHandler
에서 login과정이 예외발생 없이 무사히 끝나면 인증객체를 request
에 저장한후 index.do
로 리다이렉션 수행된다.
/* LoginHandler.processSubmit() */
...
User user = loginService.login(memberid, password);
request.getSession().setAttribute("authUser", user);
response.sendRedirect(request.getContextPath()+"/index.do");
...
이번엔 당연히 인증객체인 authUser
가 존재함으로 환영문구와 로그아웃, 비밀번호 변경 링크가 출력된다.
로그아웃 처리하기
로그아웃 링크는 아래와 같다.
<a href="<%= request.getContextPath() %>/logout.do">로그아웃</a>
.../logout.do
를 컨트롤러에 요청하기 때문에 새로운 핸들러 등록을 해주자.
commandHandler.properties
에 아래와 같이 등록
/logout.do=board21.auth.command.LogoutHandler
로그아웃의 경우 세션만 삭제하면 되기 때문에 DB연결 작업이 필요 없다.
public class LogoutHandler implements CommandHandler{
@Override
public String process(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session = request.getSession();
if (session != null) {
session.invalidate();
}
response.sendRedirect(request.getContextPath()+"/index.do");
return null;
}
}
session.invalidate()
를 통해 세션을 삭제하고 index.do
로 리다이렉트 시킨다.
비밀번호 변경 기능 구현
index.jsp에서 비밀번호 변경을 위해 아래와 같은 링크를 추가했다.
<a href="<%= request.getContextPath() %>/changePwd.do">비밀번호 변경</a>
.../changePwd.do
이벤트를 commandHandler.properties
파일에 새로 등록하고
/changePwd.do=board21.member.command.ChangePasswordHandler
이벤트 처리용 객체 ChangePasswordHandler
, ChangePasswordService
를 만들고
MemberDao
에도 비밀번호 변경을 위한 update()
메서드를 구현해야 한다.
LoginCheckFilter - 로그인 하였는지 검사하는 필터
비밀번호 변경, 글 쓰기 등과 같은 작업을 할 때 먼저 로그인 했는지 체크해야 한다.
<filter>
<filter-name>loginCheckFilter</filter-name>
<filter-class>board21.filter.LoginCheckFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loginCheckFilter</filter-name>
<url-pattern>/changePwd.do</url-pattern>
</filter-mapping>
web.xml
에 필터를 등록하고 LoginCheckFilter
클래스 정의
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("LoginCheckFilter doFilter");
HttpServletRequest filterRequest = (HttpServletRequest) request;
HttpSession session = filterRequest.getSession();
if (session == null || session.getAttribute("authUser") == null) {
HttpServletResponse filterResponse = (HttpServletResponse) response;
session.setAttribute("nextLink", filterRequest.getRequestURI());
System.out.println("doFilter nextLink: " + filterRequest.getRequestURI());
filterResponse.sendRedirect(filterRequest.getContextPath() + "/login.do");
}
else
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
만약 session값이 아예 없거나 authUser
라는 속성값이 session에 없다면 /jspPro/login.do
로 리다이렉트 시킨다.
session
에 nextLink
로 기존에 접근하려 했던 url (비밀번호 변경이나 글쓰기 등)를 백업해 두고
따라서 LoginHandler
클래스에도 index.do
로 바로 가지 않고 session
에 nextLink
속성이 있으면 거기로 이동하도록 설정하자.
/* LoginHandler.processSubmit() */
...
...
String nextLink = (String) request.getSession().getAttribute("nextLink");
System.out.println("nextLink: " + nextLink);
if (nextLink == null || nextLink.isEmpty()) {
nextLink = "/jspPro/index.do";
}
response.sendRedirect(nextLink);
return null;
...
...
ChangePasswordHandler - 비밀번호 변경 핸들러
어째건 비밀번호 변경 링크를 클릭히 .../changePwd.do
url을 요청한다면 컨트롤러에 의해 ChangePasswordHandler
객체가 이벤트를 처리한다.
전체적 처리는 위 그림과 같다.
Handler에 get
, post
방식으로 요청이 들어오면
get
방식일 경우 changePasswordForm.jsp
으로 포워딩 시켜 현재비밀번호, 바꿀 비밀번호를 입력받고
post
방식일 경우 현재 비밀번호를 체크, 바꿀 비밀번호로 update
시킨후 changePasswordSuccess.jsp
로 이동시킨다.
<!-- changePasswordSuccess.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비밀번호 변경 완료</title>
</head>
<body>
<h3>changePwdSuccess.jsp</h3>
암호를 변경했습니다.
<a href="/jspPro/index.do">home</a>
</body>
</html>
<!-- changePasswordForm.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비밀번호 변경</title>
</head>
<body>
<h3>changePwdForm.jsp</h3>
<form action="/jspPro/changePwd.do" method="post">
<p>
현재암호: <input type="password" name="curPwd" />
<c:if test="${ errors.curPwd }">현재 암호를 입력하세요</c:if>
<c:if test="${ errors.badCurPwd }">현재 암호가 일치하지 않습니다</c:if>
</p>
<p>
새 암호: <input type="password" name="newPwd" />
<c:if test="${ errors.newPwd }">현재 암호를 입력하세요</c:if>
</p>
<input type="submit" value="암호 변경"/>
</form>
</body>
</html>
errors 객체가 있는 것으로 봐서 각 input태그에 미입력, 현재 비밀번호가 틀릴경우 오류를 출력하는 것을 알 수 있다.
public class ChangePasswordHandler implements CommandHandler{
private static final String FORM_VIEW = "/changePwdForm.jsp";
private ChangePasswordService changePwdSvc = new ChangePasswordService();
@Override
public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (request.getMethod().equalsIgnoreCase("GET")) {
System.out.println("ChangePasswordHandler preocess GET");
return processForm(request, response);
}
else if (request.getMethod().equalsIgnoreCase("POST")) {
System.out.println("ChangePasswordHandler preocess POST");
return processSubmit(request, response);
}
else {
response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
return null;
}
}
private String processForm(HttpServletRequest request, HttpServletResponse response) throws IOException {
return FORM_VIEW;
}
private String processSubmit(HttpServletRequest request, HttpServletResponse response) throws IOException {
User user = (User) request.getSession().getAttribute("authUser");
Map<String, Boolean> errors = new HashMap<>();
request.setAttribute("errors", errors);
String curPwd = request.getParameter("curPwd");
String newPwd = request.getParameter("newPwd");
if (curPwd == null || curPwd.isEmpty()) {
errors.put("curPwd", Boolean.TRUE);
}
if (newPwd == null || newPwd.isEmpty()) {
errors.put("newPwd", Boolean.TRUE);
}
if (!errors.isEmpty()) {
return FORM_VIEW;
}
try {
changePwdSvc.changePassword(user.getId(), curPwd, newPwd);
return "/changePwdSuccess.jsp";
}
catch (InvalidPasswordException e) {
System.out.println("InvalidPasswordException");
errors.put("badCurPwd", Boolean.TRUE);
return FORM_VIEW;
} catch (MemberNotFoundException e) {
System.out.println("MemberNotFoundException");
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return null;
}
}
}
ChangePasswordHandler
도 processSubmit
가 약간 다를뿐 회원가입이나, 로그인 과정과 다를 것 이 없다.
입력값이 유효한지 확인하고 유효하지 않다면 오류를 담은 Map
객체를 가지고 FORM_VIEW
로 다시 이동시키고
입력값이 모두 유효하다면 changePwdSvc.changePassword(user.getId(), curPwd, newPwd)
메서드를 호출한다.
만약 현재비밀번호가 일치하지 않는다면 InvalidPasswordException
라는 예외를 throw
만약 session
에서 가져온 authUser
속성의 User
객체안 id값이 DB에 없는 id라면 MemberNotFoundException
라는 예외를 throw한다.
두 예외 모두 직접 정의한 RunableException
을 상속하는 예외객체이다.
ChangePasswordService - 비밀번호 변경 서비스
실직적으로 MemberDao안의 각종 쿼리를 수행하여 ID가 있는지, 기존 PW가 입력한 PW와 일치하는지 검사후
MemberDao
의 update(conn, member)
메서드를 호출한다.
ChangePasswordService
의 changePassword
메서드는 아래와 같다.
public class ChangePasswordService {
private MemberDao memberDao = new MemberDao();
public void changePassword(String userId, String curPwd, String newPwd)
{
Connection conn = null;
try {
conn = ConnectionProvider.getConncection();
conn.setAutoCommit(false);
Member member = memberDao.selectById(conn, userId);
if (member == null) {
throw new MemberNotFoundException();
}
if (!member.matchPassword(curPwd)) {
throw new InvalidPasswordException();
}
member.changePassword(newPwd);
memberDao.update(conn, member);
conn.commit();
}
catch (SQLException | NamingException e) {
JdbcUtil.rollback(conn);
throw new RuntimeException();
}
finally {
JdbcUtil.close(conn);
}
}
}
새로 만든 update메서드는 Member객체를 매개변수로 받아 UPDATE
sql쿼리를 수행하는 메서드이다.
public void update(Connection conn, Member member) throws SQLException
{
try (PreparedStatement pstmt = conn.prepareStatement(
"UPDATE member21 SET name = ?, password = ? WHERE memberid = ?"
))
{
pstmt.setString(1, member.getName());
pstmt.setString(2, member.getPassword());
pstmt.setString(3, member.getMemberid());
pstmt.executeUpdate();
}
}