diff --git a/README.md b/README.md index bce18962d..8ed936e6e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # 웹 애플리케이션 서버 -## 요구사항 +## 1단계 요구사항 - [x] http://localhost:8080/index.html 로 접속했을 때 webapp 디렉토리의 index.html 파일을 읽어 클라이언트에 응답한다. - [x] “회원가입” 메뉴를 클릭하면 http://localhost:8080/user/form.html 으로 이동하면서 회원가입할 수 있다. - [x] http://localhost:8080/user/form.html 파일의 form 태그 method를 get에서 post로 수정한 후 회원가입 기능이 정상적으로 동작하도록 구현한다. - [x] “회원가입”을 완료하면 /index.html 페이지로 이동하고 싶다. 현재는 URL이 /user/create 로 유지되는 상태로 읽어서 전달할 파일이 없다. 따라서 redirect 방식처럼 회원가입을 완료한 후 “index.html”로 이동해야 한다. - [x] 지금까지 구현한 소스 코드는 stylesheet 파일을 지원하지 못하고 있다. Stylesheet 파일을 지원하도록 구현하도록 한다. +## 2단계 요구사항 +- [x] 다수의 사용자 요청에 대해 Queue 에 저장한 후 순차적으로 처리가 가능하도록 해야 한다. +- [x] 서버가 모든 요청에 대해 Thread를 매번 생성하는 경우 성능상 문제가 발생할 수 있다. Thread Pool을 적용해 일정 수의 사용자 동시에 처리가 가능하도록 한다. +- [x] HTTP 요청 Header/Body 처리, 응답 Header/Body 처리만을 담당하는 역할을 분리해 재사용 가능하도록 한다. +- [x] 다형성을 활용해 클라이언트 요청 URL에 대한 분기 처리를 제거한다. + +## 3단계 요구사항 +- [x] 로그인할 수 있다. 로그인이 성공하면 index.html로 이동하고, 로그인이 실패하면 /user/login_failed.html로 이동해야 한다. +- [x] 앞에서 회원가입한 사용자로 로그인할 수 있어야 한다. +- [x] 로그인이 성공하면 cookie를 활용해 로그인 상태를 유지할 수 있어야 한다. +- [x] 로그인이 성공할 경우 요청 header의 Cookie header 값이 logined=true, 로그인이 실패하면 Cookie header 값이 logined=false로 전달되어야 한다. +- [x] 사용자가 “로그인” 상태일 경우(Cookie 값이 logined=true) 경우 http://localhost:8080/user/list 로 접근했을 때 사용자 목록을 출력한다. 만약 로그인하지 않은 상태라면 로그인 페이지(login.html)로 이동한다. +- [x] 서블릿에서 지원하는 HttpSession API의 일부를 지원해야 한다. +- [x] SessionStore를 통해 백엔드에서 세션에 접근할 수 있어야 한다. +- [x] 로그인 여부 확인을 세션으로 대체한다. ## 우아한테크코스 코드리뷰 * [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) \ No newline at end of file diff --git a/src/main/java/controller/ControllerMapper.java b/src/main/java/controller/ControllerMapper.java index 058a5a491..967dad544 100644 --- a/src/main/java/controller/ControllerMapper.java +++ b/src/main/java/controller/ControllerMapper.java @@ -10,6 +10,8 @@ public class ControllerMapper { static { controllers.put("/user/create", new CreateUserController()); + controllers.put("/user/list", new UserListController()); + controllers.put("/user/login", new LoginController()); controllers.put("/", new IndexController()); } diff --git a/src/main/java/controller/CreateUserController.java b/src/main/java/controller/CreateUserController.java index d6a6a3584..d7cfc8dc9 100644 --- a/src/main/java/controller/CreateUserController.java +++ b/src/main/java/controller/CreateUserController.java @@ -7,7 +7,6 @@ import web.response.HttpResponse; public class CreateUserController extends AbstractController { - public static final String CREATE_USER_LOGGING_MESSAGE = "New User created! -> {}"; public static final String INDEX_HTML_PATH = "/index.html"; diff --git a/src/main/java/controller/LoginController.java b/src/main/java/controller/LoginController.java new file mode 100644 index 000000000..69586fca0 --- /dev/null +++ b/src/main/java/controller/LoginController.java @@ -0,0 +1,40 @@ +package controller; + +import db.DataBase; +import exception.UserNotFoundException; +import model.User; +import web.request.HttpRequest; +import web.response.HttpResponse; +import web.session.HttpSession; +import web.session.SessionStore; +import web.session.WebSession; + +public class LoginController extends AbstractController { + public static final String INDEX_HTML_PATH = "/index.html"; + public static final String LOGIN_FAIL_HTML_PATH = "/user/login_failed.html"; + + @Override + protected void doPost(HttpRequest request, HttpResponse response) { + try { + String userId = request.getRequestBodyByKey("userId"); + User user = DataBase.findUserById(userId) + .orElseThrow(() -> new UserNotFoundException(userId)); + loginValidator(request, user); + HttpSession session = new WebSession(); + session.setAttribute("email", user.getEmail()); + SessionStore.addSession(session); + response.addSession(session); + response.found(INDEX_HTML_PATH); + } catch (IllegalAccessException | UserNotFoundException e) { + logger.error(e.getMessage()); + response.found(LOGIN_FAIL_HTML_PATH); + } + } + + private void loginValidator(HttpRequest request, User user) throws IllegalAccessException { + String password = request.getRequestBodyByKey("password"); + if (!user.checkPassword(password)) { + throw new IllegalAccessException("로그인에 실패하였습니다."); + } + } +} diff --git a/src/main/java/controller/UserListController.java b/src/main/java/controller/UserListController.java new file mode 100644 index 000000000..7ef31abd1 --- /dev/null +++ b/src/main/java/controller/UserListController.java @@ -0,0 +1,35 @@ +package controller; + +import com.github.jknack.handlebars.Template; +import db.DataBase; +import exception.RequestParameterNotFoundException; +import model.User; +import utils.TemplateUtils; +import web.request.HttpRequest; +import web.response.HttpResponse; +import web.session.SessionStore; + +import java.io.IOException; +import java.util.Collection; + +public class UserListController extends AbstractController { + public static final String LOGIN_HTML_PATH = "/user/login.html"; + + @Override + protected void doGet(HttpRequest request, HttpResponse response) { + try { + String sessionId = request.getSession(); + if (!SessionStore.isContains(sessionId)) { + throw new IllegalAccessException("세션을 확인할 수 없습니다."); + } + String path = request.getTarget(); + Template template = TemplateUtils.buildTemplate(path); + Collection users = DataBase.findAll(); + String result = template.apply(users); + response.ok(result); + } catch (IllegalAccessException | RequestParameterNotFoundException | IOException e) { + logger.error(e.getMessage()); + response.found(LOGIN_HTML_PATH); + } + } +} diff --git a/src/main/java/db/DataBase.java b/src/main/java/db/DataBase.java index e3cd8908a..55526583f 100644 --- a/src/main/java/db/DataBase.java +++ b/src/main/java/db/DataBase.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.Map; +import java.util.Optional; public class DataBase { private static final Map users = Maps.newHashMap(); @@ -13,11 +14,19 @@ public static void addUser(User user) { users.put(user.getUserId(), user); } - public static User findUserById(String userId) { - return users.get(userId); + public static Optional findUserById(String userId) { + if (users.containsKey(userId)) { + User foundUser = users.get(userId); + return Optional.of(foundUser); + } + return Optional.empty(); } public static Collection findAll() { return users.values(); } + + public static void deleteAll() { + users.clear(); + } } diff --git a/src/main/java/exception/UserNotFoundException.java b/src/main/java/exception/UserNotFoundException.java new file mode 100644 index 000000000..357a71ad7 --- /dev/null +++ b/src/main/java/exception/UserNotFoundException.java @@ -0,0 +1,9 @@ +package exception; + +public class UserNotFoundException extends RuntimeException { + private static final String USER_NOT_FOUND_MESSAGE = "해당 유저를 찾을 수 없습니다. id : "; + + public UserNotFoundException(String id) { + super(USER_NOT_FOUND_MESSAGE + id); + } +} diff --git a/src/main/java/model/User.java b/src/main/java/model/User.java index a1f2d9b0a..9aec4ae4e 100644 --- a/src/main/java/model/User.java +++ b/src/main/java/model/User.java @@ -32,6 +32,10 @@ public String getEmail() { return email; } + public boolean checkPassword(String password) { + return this.password.equals(password); + } + @Override public String toString() { return "User [userId=" + userId + ", password=" + password + ", name=" + name + ", email=" + email + "]"; diff --git a/src/main/java/utils/TemplateUtils.java b/src/main/java/utils/TemplateUtils.java new file mode 100644 index 000000000..0346e1f80 --- /dev/null +++ b/src/main/java/utils/TemplateUtils.java @@ -0,0 +1,28 @@ +package utils; + +import com.github.jknack.handlebars.Handlebars; +import com.github.jknack.handlebars.Helper; +import com.github.jknack.handlebars.Template; +import com.github.jknack.handlebars.io.ClassPathTemplateLoader; +import com.github.jknack.handlebars.io.TemplateLoader; + +import java.io.IOException; + +public class TemplateUtils { + private static final String PREFIX = "/templates"; + private static final String SUFFIX = ".html"; + private static final Handlebars handlebars; + + static { + TemplateLoader loader = new ClassPathTemplateLoader(); + loader.setPrefix(PREFIX); + loader.setSuffix(SUFFIX); + + handlebars = new Handlebars(loader); + handlebars.registerHelper("increase", (Helper) (number, options) -> number + 1); + } + + public static Template buildTemplate(String path) throws IOException { + return handlebars.compile(path); + } +} diff --git a/src/main/java/web/HttpHeader.java b/src/main/java/web/HttpHeader.java index 1e59a76fe..4321a7f73 100644 --- a/src/main/java/web/HttpHeader.java +++ b/src/main/java/web/HttpHeader.java @@ -8,6 +8,8 @@ public class HttpHeader { public static final String CONTENT_LENGTH = "Content-Length"; public static final String CONTENT_TYPE = "Content-Type"; public static final String LOCATION = "Location"; + public static final String SET_COOKIE = "Set-Cookie"; + public static final String COOKIE = "Cookie"; public static final String HEADER_DELIMITER = ": "; private final Map headers; diff --git a/src/main/java/web/request/Cookies.java b/src/main/java/web/request/Cookies.java new file mode 100644 index 000000000..4e7640b49 --- /dev/null +++ b/src/main/java/web/request/Cookies.java @@ -0,0 +1,32 @@ +package web.request; + +import exception.RequestParameterNotFoundException; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Cookies { + private static final String COOKIES_DELIMITER = "; "; + private static final String COOKIE_DELIMITER = "="; + + private final Map cookieMatcher = new HashMap<>(); + + public Cookies(String cookies) { + if (Objects.isNull(cookies) || cookies.isEmpty()) { + return; + } + String[] tokens = cookies.split(COOKIES_DELIMITER); + for (String token : tokens) { + String[] value = token.split(COOKIE_DELIMITER); + this.cookieMatcher.put(value[0], value[1]); + } + } + + public String getCookieByKey(String key) { + if (cookieMatcher.containsKey(key)) { + return cookieMatcher.get(key); + } + throw new RequestParameterNotFoundException("[COOKIE]" + key); + } +} diff --git a/src/main/java/web/request/HttpRequest.java b/src/main/java/web/request/HttpRequest.java index 17719cbe7..c11f347b8 100644 --- a/src/main/java/web/request/HttpRequest.java +++ b/src/main/java/web/request/HttpRequest.java @@ -17,10 +17,12 @@ public class HttpRequest { private static final Logger logger = LoggerFactory.getLogger(HttpRequest.class); + private static final String SESSION_ID = "JSESSIONID"; private final RequestLine requestLine; private final HttpHeader httpHeader; private final RequestBody requestBody; + private final Cookies cookies; public HttpRequest(InputStream inputStream) { try { @@ -28,6 +30,7 @@ public HttpRequest(InputStream inputStream) { requestLine = new RequestLine(request.readLine()); httpHeader = new HttpHeader(mappingHeaders(request)); requestBody = mappingBodies(request); + cookies = new Cookies(httpHeader.getHeaderByKey(HttpHeader.COOKIE)); } catch (IndexOutOfBoundsException | NullPointerException | IOException e) { throw new InvalidHttpRequestException(); } @@ -91,4 +94,8 @@ public String getRequestHeaderByKey(String key) { public String getRequestBodyByKey(String key) { return requestBody.getParameterByKey(key); } + + public String getSession() { + return cookies.getCookieByKey(SESSION_ID); + } } diff --git a/src/main/java/web/response/HttpResponse.java b/src/main/java/web/response/HttpResponse.java index c7aa88485..0e0243652 100644 --- a/src/main/java/web/response/HttpResponse.java +++ b/src/main/java/web/response/HttpResponse.java @@ -5,6 +5,7 @@ import utils.FileIoUtils; import utils.URIUtils; import web.HttpHeader; +import web.session.HttpSession; import java.io.DataOutputStream; import java.io.IOException; @@ -16,6 +17,7 @@ public class HttpResponse { public static final String BAD_REQUEST_ERROR_MESSAGE = "errorMessage : "; private static final Logger logger = LoggerFactory.getLogger(HttpResponse.class); private static final String HTTP_VERSION = "HTTP/1.1"; + public static final String SESSION_PREFIX = "JSESSIONID="; private final DataOutputStream dataOutputStream; private final HttpHeader httpHeader; @@ -27,10 +29,14 @@ public HttpResponse(OutputStream outputStream) { httpHeader = new HttpHeader(); } - private void addHeader(String key, String value) { + public void addHeader(String key, String value) { httpHeader.addHeader(key, value); } + public void addSession(HttpSession session) { + httpHeader.addHeader(HttpHeader.SET_COOKIE, SESSION_PREFIX + session.getId() + "; Path=/"); + } + public void ok(String path, String contentType) throws IOException, URISyntaxException { String filePath = URIUtils.getFilePath(path); responseLine = new ResponseLine(ResponseStatus.OK, HTTP_VERSION); @@ -39,6 +45,13 @@ public void ok(String path, String contentType) throws IOException, URISyntaxExc write(); } + public void ok(String path) { + responseLine = new ResponseLine(ResponseStatus.OK, HTTP_VERSION); + addHeader(HttpHeader.CONTENT_TYPE, "text/html;charset=UTF-8"); + responseBody = new ResponseBody(path.getBytes()); + write(); + } + public void found(String location) { responseLine = new ResponseLine(ResponseStatus.FOUND, HTTP_VERSION); addHeader(HttpHeader.LOCATION, location); diff --git a/src/main/java/web/session/HttpSession.java b/src/main/java/web/session/HttpSession.java new file mode 100644 index 000000000..92543195e --- /dev/null +++ b/src/main/java/web/session/HttpSession.java @@ -0,0 +1,13 @@ +package web.session; + +public interface HttpSession { + String getId(); + + void setAttribute(String name, Object value); + + Object getAttribute(String name); + + void removeAttribute(String name); + + void invalidate(); +} diff --git a/src/main/java/web/session/SessionStore.java b/src/main/java/web/session/SessionStore.java new file mode 100644 index 000000000..b30661fa5 --- /dev/null +++ b/src/main/java/web/session/SessionStore.java @@ -0,0 +1,28 @@ +package web.session; + +import java.util.HashMap; +import java.util.Map; + +public class SessionStore { + private static final Map sessions = new HashMap<>(); + + public static HttpSession getSession(String id) { + return sessions.get(id); + } + + public static void addSession(HttpSession session) { + addSession(session.getId(), session); + } + + public static void addSession(String id, HttpSession session) { + sessions.put(id, session); + } + + public static boolean isContains(String id) { + return sessions.containsKey(id); + } + + public static void remove(String id) { + sessions.remove(id); + } +} diff --git a/src/main/java/web/session/WebSession.java b/src/main/java/web/session/WebSession.java new file mode 100644 index 000000000..eb60197b1 --- /dev/null +++ b/src/main/java/web/session/WebSession.java @@ -0,0 +1,36 @@ +package web.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class WebSession implements HttpSession { + private final Map attributes = new HashMap<>(); + + private final UUID id = UUID.randomUUID(); + + @Override + public String getId() { + return id.toString(); + } + + @Override + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public void invalidate() { + attributes.clear(); + } +} diff --git a/src/main/resources/templates/user/list.html b/src/main/resources/templates/user/list.html index 3ff40952f..c4bf63462 100644 --- a/src/main/resources/templates/user/list.html +++ b/src/main/resources/templates/user/list.html @@ -27,7 +27,8 @@
- +
@@ -47,19 +48,22 @@