diff --git a/README.md b/README.md index e9589184b..53f20015a 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,29 @@ - [x] 다수의 사용자 요청에 대해 Queue에 저장한 후 순차적으로 처리가 가능하도록 한다. - [x] 요청에 대한 Thread를 매번 새로 생성하지 않고 TreadPool을 이용한다. - [x] 정적 파일에 관한 요청을 분리해 처리한다 -- [x] 응답 객체를 구현한다 \ No newline at end of file +- [x] 응답 객체를 구현한다 + +# 3단계 - 로그인 및 세션 구현 + +## 요구사항 1 +- [x] 로그인 할 수 있다. + - [x] 로그인 성공 시, /index.html로 리다이렉트 한다. + - [x] 로그인 성공 시, 성공 쿠키를 전달한다. + - [x] 로그인 실패 시, /user/login_failed.html로 리다이렉트 한다. + - [x] 로그인 실패 시, 실패 쿠키를 전달한다. + +## 요구사항 2 +- [x] 유저 리스트를 볼 수 있다. + - [x] 로그인이 되어있다면 보여준다. + - [x] 로그인이 되어있지 않다면 로그인 페이지로 이동한다. + - [x] handlebar를 사용한다. + +## 요구사항 3 +- [x] HttpSession API의 일부를 구현한다. + - [x] getId() + - [x] setAttribute(String name, Object value) + - [x] getAttribute(String name) + - [x] removeAttribute(String name) + - [x] invalidate() + - [x] 세션 id는 쿠키를 활용해 공유한다. + - [x] 세션 id는 UUID를 사용한다. diff --git a/src/main/java/exception/CreateFailException.java b/src/main/java/exception/CreateFailException.java index 5a2167963..97349a96b 100644 --- a/src/main/java/exception/CreateFailException.java +++ b/src/main/java/exception/CreateFailException.java @@ -1,6 +1,6 @@ package exception; -public class CreateFailException extends RuntimeException { +public class CreateFailException extends RuntimeException { public CreateFailException(String message) { super(message); } diff --git a/src/main/java/http/session/HttpSession.java b/src/main/java/http/session/HttpSession.java new file mode 100644 index 000000000..e8c51057d --- /dev/null +++ b/src/main/java/http/session/HttpSession.java @@ -0,0 +1,53 @@ +package http.session; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class HttpSession { + private UUID id = UUID.randomUUID(); + private Map attributes = new HashMap<>(); + + public void setAttribute(String name, Object value) { + attributes.put(name, value); + } + + public Object getAttribute(String name) { + return attributes.get(name); + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + + public void invalidate() { + attributes.clear(); + } + + public UUID getId() { + return id; + } + + @Override + public String toString() { + String sessionId = String.format("jsessionid=%s; ", id.toString()); + String attributes = this.attributes.entrySet() + .stream() + .filter(this::exclude) + .map(this::parseAttribute) + .collect(Collectors.joining("; ")); + return sessionId + attributes; + } + + private boolean exclude(Map.Entry entry) { + return !entry.getKey().equals("userId") && !entry.getValue().equals(false); + } + + private String parseAttribute(Map.Entry entry) { + if (entry.getValue().equals(true)) { + return entry.getKey(); + } + return String.format("%s=%s", entry.getKey(), entry.getValue().toString()); + } +} diff --git a/src/main/java/http/session/HttpSessionStorage.java b/src/main/java/http/session/HttpSessionStorage.java new file mode 100644 index 000000000..7da9230cd --- /dev/null +++ b/src/main/java/http/session/HttpSessionStorage.java @@ -0,0 +1,29 @@ +package http.session; + +import java.util.*; +import java.util.stream.Stream; + +public class HttpSessionStorage { + private static Map sessions = new HashMap<>(); + + public static HttpSession generate(String userId) { + HttpSession httpSession = new HttpSession(); + httpSession.setAttribute("userId", userId); + sessions.put(httpSession.getId(), httpSession); + return httpSession; + } + + public static boolean isValidSession(String cookie) { + String[] attributes = cookie.split("; "); + Optional jsessionid = Stream.of(attributes) + .filter(it -> it.startsWith("jsessionid")) + .map(it -> it.substring("jsessionid=".length())) + .findAny(); + + try { + return sessions.containsKey(UUID.fromString(jsessionid.get())); + } catch (IllegalArgumentException | NoSuchElementException | NullPointerException e) { + return false; + } + } +} diff --git a/src/main/java/webserver/requestmapping/behavior/UserCreateBehavior.java b/src/main/java/implementedbehavior/UserCreateBehavior.java similarity index 90% rename from src/main/java/webserver/requestmapping/behavior/UserCreateBehavior.java rename to src/main/java/implementedbehavior/UserCreateBehavior.java index 0fde45824..47556773d 100644 --- a/src/main/java/webserver/requestmapping/behavior/UserCreateBehavior.java +++ b/src/main/java/implementedbehavior/UserCreateBehavior.java @@ -1,4 +1,4 @@ -package webserver.requestmapping.behavior; +package implementedbehavior; import db.DataBase; import http.HttpBody; @@ -7,6 +7,7 @@ import http.response.HttpStatus; import http.response.ResponseEntity; import model.User; +import webserver.requestmapping.behavior.RequestBehavior; public class UserCreateBehavior implements RequestBehavior { @Override diff --git a/src/main/java/implementedbehavior/UserListBehavior.java b/src/main/java/implementedbehavior/UserListBehavior.java new file mode 100644 index 000000000..5cf1ab145 --- /dev/null +++ b/src/main/java/implementedbehavior/UserListBehavior.java @@ -0,0 +1,16 @@ +package implementedbehavior; + +import db.DataBase; +import http.request.RequestEntity; +import http.response.HttpStatus; +import http.response.ResponseEntity; +import webserver.requestmapping.DynamicHtmlGenerator; +import webserver.requestmapping.behavior.RequestBehavior; + +public class UserListBehavior implements RequestBehavior { + @Override + public void behave(RequestEntity requestEntity, ResponseEntity responseEntity) { + responseEntity.status(HttpStatus.OK) + .body(DynamicHtmlGenerator.applyHandlebar("user/list", DataBase.findAll())); + } +} diff --git a/src/main/java/implementedbehavior/UserLoginBehavior.java b/src/main/java/implementedbehavior/UserLoginBehavior.java new file mode 100644 index 000000000..8dd40e057 --- /dev/null +++ b/src/main/java/implementedbehavior/UserLoginBehavior.java @@ -0,0 +1,36 @@ +package implementedbehavior; + +import db.DataBase; +import http.request.Params; +import http.request.RequestEntity; +import http.response.HttpStatus; +import http.response.ResponseEntity; +import http.session.HttpSession; +import http.session.HttpSessionStorage; +import model.User; +import webserver.requestmapping.behavior.RequestBehavior; + +import java.util.Objects; + +public class UserLoginBehavior implements RequestBehavior { + @Override + public void behave(RequestEntity requestEntity, ResponseEntity responseEntity) { + Params userInfo = Params.from(requestEntity.getHttpBody().getContent()); + if (isValid(userInfo)) { + HttpSession session = HttpSessionStorage.generate(userInfo.findValueBy("userId")); + responseEntity.status(HttpStatus.FOUND) + .addHeader("Location", "/index.html") + .addHeader("Set-Cookie", session.toString()); + } else { + responseEntity.status(HttpStatus.FOUND) + .addHeader("Location", "/user/login_failed.html"); + } + } + + private boolean isValid(Params userInfo) { + String userId = userInfo.findValueBy("userId"); + String password = userInfo.findValueBy("password"); + User user = DataBase.findUserById(userId); + return Objects.nonNull(user) && user.hasPasswordOf(password); + } +} diff --git a/src/main/java/implementedfilter/AuthFilter.java b/src/main/java/implementedfilter/AuthFilter.java new file mode 100644 index 000000000..4ca808fd7 --- /dev/null +++ b/src/main/java/implementedfilter/AuthFilter.java @@ -0,0 +1,31 @@ +package implementedfilter; + +import http.request.RequestEntity; +import http.response.HttpStatus; +import http.response.ResponseEntity; +import http.session.HttpSessionStorage; +import webserver.filter.Filter; + +import java.util.Arrays; +import java.util.List; + +public class AuthFilter implements Filter { + private static final List PATH_PATTERN = Arrays.asList( + "/user/list" + ); + + @Override + public boolean doFilter(RequestEntity requestEntity, ResponseEntity responseEntity) { + String path = requestEntity.getHttpUrl().getPath(); + String cookie = requestEntity.getHttpHeader().findOrEmpty("Cookie"); + if (PATH_PATTERN.contains(path) && isNotAuthorized(cookie)) { + responseEntity.status(HttpStatus.FOUND).addHeader("Location", "/user/login.html"); + return false; + } + return true; + } + + private boolean isNotAuthorized(String cookie) { + return !HttpSessionStorage.isValidSession(cookie); + } +} diff --git a/src/main/java/model/User.java b/src/main/java/model/User.java index b7abb7304..9b4ebef49 100644 --- a/src/main/java/model/User.java +++ b/src/main/java/model/User.java @@ -13,6 +13,10 @@ public User(String userId, String password, String name, String email) { this.email = email; } + public boolean hasPasswordOf(String password) { + return this.password.equals(password); + } + public String getUserId() { return userId; } diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 44643a7ce..d197f2547 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -28,8 +28,10 @@ public RequestHandler(Socket connectionSocket) { } public void run() { - logger.debug("New Client Connect! Connected IP : {}, Port : {}", - connection.getInetAddress(), connection.getPort()); + logger.debug( + "New Client Connect! Connected IP : {}, Port : {}", + connection.getInetAddress(), connection.getPort() + ); try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); @@ -41,8 +43,8 @@ public void run() { httpEntityProcessing(requestEntity, responseEntity); writeOutResponse(dos, responseEntity); - } catch (IOException e) { - logger.error(e.getMessage()); + } catch (Exception e) { + logger.error("error message", e); } } diff --git a/src/main/java/webserver/filter/FilterStorage.java b/src/main/java/webserver/filter/FilterStorage.java index 52fc36e09..301eb9ef7 100644 --- a/src/main/java/webserver/filter/FilterStorage.java +++ b/src/main/java/webserver/filter/FilterStorage.java @@ -5,6 +5,7 @@ import http.request.RequestEntity; import http.response.ResponseEntity; +import implementedfilter.AuthFilter; public class FilterStorage { @@ -12,6 +13,7 @@ public class FilterStorage { private static final List outputFilters = new ArrayList<>(); static { + inputFilters.add(new AuthFilter()); inputFilters.add(new StaticFileFilter()); outputFilters.add(new ContentLengthFilter()); } diff --git a/src/main/java/webserver/requestmapping/DynamicHtmlGenerator.java b/src/main/java/webserver/requestmapping/DynamicHtmlGenerator.java new file mode 100644 index 000000000..062e18f24 --- /dev/null +++ b/src/main/java/webserver/requestmapping/DynamicHtmlGenerator.java @@ -0,0 +1,24 @@ +package webserver.requestmapping; + +import com.github.jknack.handlebars.Handlebars; +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 DynamicHtmlGenerator { + public static String applyHandlebar(String path, Object model) { + TemplateLoader loader = new ClassPathTemplateLoader(); + loader.setPrefix("/templates"); + loader.setSuffix(".html"); + Handlebars handlebars = new Handlebars(loader); + + try { + Template template = handlebars.compile(path); + return template.apply(model); + } catch (IOException e) { + throw new RuntimeException(); + } + } +} diff --git a/src/main/java/webserver/requestmapping/RequestMappingStorage.java b/src/main/java/webserver/requestmapping/RequestMappingStorage.java index e5d0e4810..9bfa27393 100644 --- a/src/main/java/webserver/requestmapping/RequestMappingStorage.java +++ b/src/main/java/webserver/requestmapping/RequestMappingStorage.java @@ -1,11 +1,13 @@ package webserver.requestmapping; -import java.util.Arrays; -import java.util.List; - import http.request.HttpMethod; import http.request.RequestEntity; -import webserver.requestmapping.behavior.UserCreateBehavior; +import implementedbehavior.UserCreateBehavior; +import implementedbehavior.UserListBehavior; +import implementedbehavior.UserLoginBehavior; + +import java.util.Arrays; +import java.util.List; public class RequestMappingStorage { @@ -13,7 +15,9 @@ public class RequestMappingStorage { static { REQUEST_MAPPINGS = Arrays.asList( - new RequestMapping(HttpMethod.POST, "/user/create", new UserCreateBehavior()) + new RequestMapping(HttpMethod.POST, "/user/create", new UserCreateBehavior()), + new RequestMapping(HttpMethod.POST, "/user/login", new UserLoginBehavior()), + new RequestMapping(HttpMethod.GET, "/user/list", new UserListBehavior()) ); } diff --git a/src/main/resources/templates/user/list.html b/src/main/resources/templates/user/list.html index 3ff40952f..4c77a617a 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 @@