diff --git a/README.md b/README.md index 732dba3d..63984420 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,18 @@ - orElseGet - Supplier로 wrapping한 값을 받는다. - **`null`일 경우에만** 뒤의 로직이 실행된다.(lazy) + +--- + +## Step4 : 웹서버 4단계 - 쿠키를 이용한 로그인 구현 + +- 302, 304 차이 + - 302는 리다이렉트, 304는 Not Modified로 브라우저의 캐시를 사용하므로 절대 response body를 넣으면 안된다. +- response body가 없어도 되는 상태 코드는 302, 304, 204 등이 있다. +- [Set-Cookie] HTTP/1.1 vs. HTTP/2 : 세미콜론(;)으로 결합이 가능한지 아닌지 ... 아닌 것 같음 +- [Set-Cookie] 작성 순서(cookie-name=cookie-value; 옵션들(max-age, path, domain 등등)) +- [Set-Cookie] Path value default 있음 (/user/create -> /user가 기본 값) +- [Set-Cookie] Path를 set-Cookie 했을 때와 동일한 Path 값을 설정해야 정상적인 로그아웃이 가능함 + - 참고 : https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + - 참고2: https://stackoverflow.com/questions/16305814/are-multiple-cookie-headers-allowed-in-an-http-request + diff --git a/src/main/java/controller/Controller.java b/src/main/java/controller/Controller.java new file mode 100644 index 00000000..0949a38d --- /dev/null +++ b/src/main/java/controller/Controller.java @@ -0,0 +1,11 @@ +package controller; + +import java.io.IOException; +import webserver.Request; +import webserver.Response; + +public interface Controller { + + void process(Request request, Response response) throws IOException; + +} diff --git a/src/main/java/controller/FirstController.java b/src/main/java/controller/FirstController.java new file mode 100644 index 00000000..331d8b80 --- /dev/null +++ b/src/main/java/controller/FirstController.java @@ -0,0 +1,39 @@ +package controller; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.ControllerMapper; +import webserver.HttpMethod; +import webserver.Request; +import webserver.Response; + +public class FirstController { + + private static final FirstController instance = new FirstController(); + + private final Map map = new ConcurrentHashMap<>(); + private Logger log = LoggerFactory.getLogger(FirstController.class); + + private FirstController() { + map.put(new ControllerMapper(HttpMethod.POST, "/user/create"), UserJoinController.getInstance()); + map.put(new ControllerMapper(HttpMethod.GET, "/user/logout"), UserLogoutController.getInstance()); + map.put(new ControllerMapper(HttpMethod.POST, "/user/login"), UserLoginController.getInstance()); + } + + public void run(Request request, Response response) throws IOException { + Controller controller = map.get(new ControllerMapper(request.getHttpMethod(), request.getPath())); + if (controller == null) { + controller = HomeController.getInstance(); + } + log.debug("call {}", controller); + controller.process(request, response); + } + + public static FirstController getInstance() { + return instance; + } + +} diff --git a/src/main/java/controller/HomeController.java b/src/main/java/controller/HomeController.java new file mode 100644 index 00000000..5550a98a --- /dev/null +++ b/src/main/java/controller/HomeController.java @@ -0,0 +1,30 @@ +package controller; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import webserver.HttpStatus; +import webserver.Request; +import webserver.Response; + +public class HomeController implements Controller { + + private static final HomeController instance = new HomeController(); + + private Logger log = LoggerFactory.getLogger(HomeController.class); + private HomeController() { + } + + public static HomeController getInstance() { + return instance; + } + + @Override + public void process(Request request, Response response) throws IOException { + byte[] body = Files.readAllBytes(new File("./webapp" + request.getPath()).toPath()); + log.debug("path: {}", request.getPath()); + response.write(body, HttpStatus.OK); + } +} diff --git a/src/main/java/controller/UserJoinController.java b/src/main/java/controller/UserJoinController.java new file mode 100644 index 00000000..79601f06 --- /dev/null +++ b/src/main/java/controller/UserJoinController.java @@ -0,0 +1,57 @@ +package controller; + +import db.DataBase; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import util.Pair; +import webserver.HttpStatus; +import webserver.Request; +import webserver.Response; + +public class UserJoinController implements Controller { + + private static final UserJoinController instance = new UserJoinController(); + + private Logger log = LoggerFactory.getLogger(UserJoinController.class); + + private UserJoinController() { + } + + public static UserJoinController getInstance() { + return instance; + } + + @Override + public void process(Request request, Response response) throws IOException { + Map parsedBody = request.takeParsedBody(); + log.debug("POST BODY: {}", parsedBody); + User user = new User( + parsedBody.get("userId"), + parsedBody.get("password"), + parsedBody.get("name"), + parsedBody.get("email") + ); + saveUser(user, response); + } + + private void saveUser(User user, Response response) { + List pairs = new ArrayList<>(); + if (DataBase.validateDuplicatedId(user)) { + DataBase.addUser(user); + log.debug("SavedUser: {}", user); + pairs.add(new Pair("Location", "http://localhost:8080/index.html")); + response.write(HttpStatus.FOUND, pairs); + return; + } + log.debug("Save Fail: {}", user); + pairs.add(new Pair("Location", "http://localhost:8080/user/form.html")); + response.write(HttpStatus.FOUND, pairs); + } +} diff --git a/src/main/java/controller/UserLoginController.java b/src/main/java/controller/UserLoginController.java new file mode 100644 index 00000000..2b2707cc --- /dev/null +++ b/src/main/java/controller/UserLoginController.java @@ -0,0 +1,50 @@ +package controller; + +import db.DataBase; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import util.Pair; +import webserver.HttpSession; +import webserver.HttpStatus; +import webserver.Request; +import webserver.Response; + +public class UserLoginController implements Controller { + + private static final UserLoginController instance = new UserLoginController(); + + private Logger log = LoggerFactory.getLogger(UserLoginController.class); + + private UserLoginController() { + } + + public static UserLoginController getInstance() { + return instance; + } + + @Override + public void process(Request request, Response response) { + Map parsedBody = request.takeParsedBody(); + log.debug("POST BODY: {}", parsedBody); + + User user = DataBase.findUserById(parsedBody.get("userId")); + List pairs = new ArrayList<>(); + + if (user != null && user.getPassword().equals(parsedBody.get("password"))) { + log.debug("login 성공"); + pairs.add(new Pair("Location", "http://localhost:8080/index.html")); + pairs.add(new Pair("Set-Cookie", "sessionId=" + HttpSession.makeUUID(user.getUserId()) + "; max-age=20; Path=/; HttpOnly")); + response.write(HttpStatus.FOUND, pairs); + return; + } + log.debug("login 실패"); + pairs.add(new Pair("Location", "http://localhost:8080/user/login_failed.html")); + response.write(HttpStatus.FOUND, pairs); + } + + +} diff --git a/src/main/java/controller/UserLogoutController.java b/src/main/java/controller/UserLogoutController.java new file mode 100644 index 00000000..3225009f --- /dev/null +++ b/src/main/java/controller/UserLogoutController.java @@ -0,0 +1,35 @@ +package controller; + +import java.util.ArrayList; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import util.Pair; +import webserver.HttpStatus; +import webserver.Request; +import webserver.Response; + +public class UserLogoutController implements Controller { + + private static final UserLogoutController instance = new UserLogoutController(); + + private Logger log = LoggerFactory.getLogger(UserLogoutController.class); + + private UserLogoutController() { + } + + public static UserLogoutController getInstance() { + return instance; + } + + @Override + public void process(Request request, Response response) { + List pairs = new ArrayList<>(); + pairs.add(new Pair("Location", "http://localhost:8080/index.html")); + pairs.add(new Pair("Set-Cookie", "sessionId=; max-age=-1; Path=/")); + response.write(HttpStatus.FOUND, pairs); + + log.debug("logout 성공"); + } + +} diff --git a/src/main/java/db/DataBase.java b/src/main/java/db/DataBase.java index 956b489b..433bfeae 100644 --- a/src/main/java/db/DataBase.java +++ b/src/main/java/db/DataBase.java @@ -1,14 +1,13 @@ package db; import java.util.Collection; +import java.util.HashMap; import java.util.Map; -import com.google.common.collect.Maps; - import model.User; public class DataBase { - private static Map users = Maps.newHashMap(); + private static Map users = new HashMap<>(); public static void addUser(User user) { users.put(user.getUserId(), user); diff --git a/src/main/java/util/HttpRequestUtils.java b/src/main/java/util/HttpRequestUtils.java index 731966fe..dd2dae64 100644 --- a/src/main/java/util/HttpRequestUtils.java +++ b/src/main/java/util/HttpRequestUtils.java @@ -5,12 +5,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class HttpRequestUtils { - private static final Logger log = LoggerFactory.getLogger(HttpRequestUtils.class); - /** * @param queryString은 URL에서 ? 이후에 전달되는 field1=value1&field2=value2 형식임 * @return @@ -53,63 +49,4 @@ static Pair getKeyValue(String keyValue, String regex) { public static Pair parseHeader(String header) { return getKeyValue(header, ": "); } - - public static class Pair { - private final String key; - private final String value; - - Pair(String key, String value) { - this.key = key.trim(); - this.value = value.trim(); - } - - public String getKey() { - return key; - } - - public String getValue() { - return value; - } - - public boolean isContentLength() { - return key.equals("Content-Length"); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((key == null) ? 0 : key.hashCode()); - result = prime * result + ((value == null) ? 0 : value.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Pair other = (Pair) obj; - if (key == null) { - if (other.key != null) - return false; - } else if (!key.equals(other.key)) - return false; - if (value == null) { - if (other.value != null) - return false; - } else if (!value.equals(other.value)) - return false; - return true; - } - - @Override - public String toString() { - return key + ": " + value; -// return "Pair [key=" + key + ", value=" + value + "]"; - } - } } diff --git a/src/main/java/util/IOUtils.java b/src/main/java/util/IOUtils.java index a0470ee2..0cf4268b 100644 --- a/src/main/java/util/IOUtils.java +++ b/src/main/java/util/IOUtils.java @@ -4,8 +4,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import util.HttpRequestUtils.Pair; public class IOUtils { /** diff --git a/src/main/java/util/Pair.java b/src/main/java/util/Pair.java new file mode 100644 index 00000000..33a25d66 --- /dev/null +++ b/src/main/java/util/Pair.java @@ -0,0 +1,61 @@ +package util; + +public class Pair { + private final String key; + private final String value; + + public Pair(String key, String value) { + this.key = key.trim(); + this.value = value.trim(); + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public boolean isContentLength() { + return key.equals("Content-Length"); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Pair other = (Pair) obj; + if (key == null) { + if (other.key != null) + return false; + } else if (!key.equals(other.key)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + return key + ": " + value; + } +} + + diff --git a/src/main/java/util/PrintUtils.java b/src/main/java/util/PrintUtils.java index 7b2fd0fd..1422fefe 100644 --- a/src/main/java/util/PrintUtils.java +++ b/src/main/java/util/PrintUtils.java @@ -1,7 +1,6 @@ package util; import java.util.List; -import util.HttpRequestUtils.Pair; public class PrintUtils { diff --git a/src/main/java/util/Response.java b/src/main/java/util/Response.java deleted file mode 100644 index 15d300f4..00000000 --- a/src/main/java/util/Response.java +++ /dev/null @@ -1,91 +0,0 @@ -package util; - -import db.DataBase; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.Map; -import model.User; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Response { - private Logger log = LoggerFactory.getLogger(Response.class); - private byte[] body; - private DataOutputStream dos; - private Request request; - - public Response(OutputStream out, Request request) { - this.dos = new DataOutputStream(out); - this.request = request; - } - - public void writeResponse() throws IOException { - log.debug("requestLine: {}", request.getRequestLine()); - if (request.isPOST() && request.getPath().equals("/user/create")) { - - this.body = Files.readAllBytes(new File("./webapp/index.html").toPath()); - - Map parsedBody = request.takeParsedBody(); - log.debug("POST BODY: {}", parsedBody); - User user = new User( - parsedBody.get("userId"), - parsedBody.get("password"), - parsedBody.get("name"), - parsedBody.get("email") - ); - saveUser(user); - return; - } - this.body = Files.readAllBytes(new File("./webapp" + request.getPath()).toPath()); - response200Header(); - responseBody(); - } - - private void saveUser(User user) { - if (DataBase.validateDuplicatedId(user)){ - DataBase.addUser(user); - response302Header("http://localhost:8080/index.html"); - responseBody(); - return; - } - response302Header("http://localhost:8080/user/form.html"); - responseBody(); - return; - } - - private void response302Header(String redirectURL) { - try { - dos.writeBytes("HTTP/1.1 302 Found\r\n"); - dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); - dos.writeBytes("Content-Length: " + body.length + "\r\n"); - dos.writeBytes("Location: " + redirectURL + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.error(e.getMessage()); - } - } - - private void response200Header() { - try { - dos.writeBytes("HTTP/1.1 200 OK \r\n"); - dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); - dos.writeBytes("Content-Length: " + body.length + "\r\n"); - dos.writeBytes("\r\n"); - } catch (IOException e) { - log.error(e.getMessage()); - } - } - - private void responseBody() { - try { - dos.write(body, 0, body.length); - dos.flush(); - } catch (IOException e) { - log.error(e.getMessage()); - } - } - -} diff --git a/src/main/java/webserver/ControllerMapper.java b/src/main/java/webserver/ControllerMapper.java new file mode 100644 index 00000000..d1deab9b --- /dev/null +++ b/src/main/java/webserver/ControllerMapper.java @@ -0,0 +1,32 @@ +package webserver; + +import java.util.Objects; + +public class ControllerMapper { + + private final HttpMethod httpMethod; + private final String path; + + public ControllerMapper(HttpMethod httpMethod, String path) { + this.httpMethod = httpMethod; + this.path = path; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ControllerMapper that = (ControllerMapper) o; + return httpMethod == that.httpMethod && Objects.equals(path, that.path); + } + + @Override + public int hashCode() { + return Objects.hash(httpMethod, path); + } + +} diff --git a/src/main/java/webserver/HttpMethod.java b/src/main/java/webserver/HttpMethod.java new file mode 100644 index 00000000..65755794 --- /dev/null +++ b/src/main/java/webserver/HttpMethod.java @@ -0,0 +1,17 @@ +package webserver; + +public enum HttpMethod { + + GET, POST; + + public static HttpMethod create(String httpMethod) { + if (httpMethod.equals("GET")) { + return GET; + } + if (httpMethod.equals("POST")) { + return POST; + } + return null; + } + +} diff --git a/src/main/java/webserver/HttpSession.java b/src/main/java/webserver/HttpSession.java new file mode 100644 index 00000000..baa68b9f --- /dev/null +++ b/src/main/java/webserver/HttpSession.java @@ -0,0 +1,31 @@ +package webserver; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HttpSession { + + // UUID, userId + private static Map session = new HashMap<>(); + + private static Logger log = LoggerFactory.getLogger(HttpSession.class); + + public static String makeUUID(String userId) { + String uuid = UUID.randomUUID().toString(); + session.put(uuid, userId); + log.debug("uuid, userId: {}, {}", uuid, userId); + return uuid; + } + + public static String checkUser(String uuid) { + return session.get(uuid); + } + + public static void logout(String uuid) { + session.remove(uuid); + } + +} diff --git a/src/main/java/webserver/HttpStatus.java b/src/main/java/webserver/HttpStatus.java new file mode 100644 index 00000000..e0dcee86 --- /dev/null +++ b/src/main/java/webserver/HttpStatus.java @@ -0,0 +1,23 @@ +package webserver; + +public enum HttpStatus { + + OK(200, "200 OK"), + FOUND(302, "302 Found"); + + private final int status; + private final String message; + + HttpStatus(int status, String message) { + this.status = status; + this.message = message; + } + + public int getStatus() { + return status; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/util/Request.java b/src/main/java/webserver/Request.java similarity index 87% rename from src/main/java/util/Request.java rename to src/main/java/webserver/Request.java index 376a6e9e..cbdf65f9 100644 --- a/src/main/java/util/Request.java +++ b/src/main/java/webserver/Request.java @@ -1,4 +1,4 @@ -package util; +package webserver; import java.io.BufferedReader; import java.io.IOException; @@ -9,24 +9,21 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import util.HttpRequestUtils.Pair; +import util.HttpRequestUtils; +import util.IOUtils; +import util.Pair; public class Request { - private Logger log = LoggerFactory.getLogger(Request.class); - private static final int HTTP_METHOD = 0; private static final int REQUEST_TARGET = 1; private static final int PATH = 0; private static final int QUERY_STRING = 1; private final BufferedReader br; - private String requestLine; private String[] parsedRequestLine; - private String httpMethod; + private HttpMethod httpMethod; private String path; private List headerPairs; private Map parsedQueryString; @@ -46,7 +43,7 @@ public void readRequest() throws IOException { private void extractRequestLine(BufferedReader br) throws IOException { this.requestLine = br.readLine(); this.parsedRequestLine = requestLine.split(" "); - this.httpMethod = parsedRequestLine[HTTP_METHOD]; + this.httpMethod = HttpMethod.create(parsedRequestLine[HTTP_METHOD]); this.path = parseRequestURL()[PATH]; this.parsedQueryString = takeParsedQueryString(); } @@ -79,8 +76,8 @@ private String decode(String target) { return URLDecoder.decode(target, StandardCharsets.UTF_8); } - public boolean isPOST() { - return httpMethod.equals("POST"); + public HttpMethod getHttpMethod() { + return httpMethod; } public List getHeaderPairs() { diff --git a/src/main/java/webserver/RequestHandler.java b/src/main/java/webserver/RequestHandler.java index 587e7a29..2052f6ec 100644 --- a/src/main/java/webserver/RequestHandler.java +++ b/src/main/java/webserver/RequestHandler.java @@ -1,21 +1,13 @@ package webserver; -import java.io.BufferedReader; -import java.io.DataOutputStream; -import java.io.File; +import controller.FirstController; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; -import java.nio.file.Files; -import java.util.Map; -import model.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import util.PrintUtils; -import util.Request; -import util.Response; public class RequestHandler extends Thread { private static final Logger log = LoggerFactory.getLogger(RequestHandler.class); @@ -26,6 +18,7 @@ public RequestHandler(Socket connectionSocket) { this.connection = connectionSocket; } + @Override public void run() { log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort()); @@ -35,8 +28,10 @@ public void run() { Request request = new Request(in); request.readRequest(); - Response response = new Response(out, request); - response.writeResponse(); + Response response = new Response(out); + + FirstController firstController = FirstController.getInstance(); + firstController.run(request, response); PrintUtils.printRequestHeaders(request.getHeaderPairs(), request.getRequestLine()); diff --git a/src/main/java/webserver/Response.java b/src/main/java/webserver/Response.java new file mode 100644 index 00000000..8787027a --- /dev/null +++ b/src/main/java/webserver/Response.java @@ -0,0 +1,46 @@ +package webserver; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import util.Pair; + +public class Response { + + private Logger log = LoggerFactory.getLogger(Response.class); + private DataOutputStream dos; + + public Response(OutputStream out) { + this.dos = new DataOutputStream(out); + } + + public void write(byte[] body, HttpStatus httpStatus) { + try { + dos.writeBytes("HTTP/1.1 " + httpStatus.getMessage() + "\r\n"); + dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); + dos.writeBytes("Content-Length: " + body.length + "\r\n"); + dos.writeBytes("\r\n"); + dos.write(body, 0, body.length); + dos.flush(); + } catch (IOException e) { + log.error(e.getMessage()); + } + } + + public void write(HttpStatus httpStatus, List pairs) { + try { + dos.writeBytes("HTTP/1.1 " + httpStatus.getMessage() + "\r\n"); + dos.writeBytes("Content-Type: text/html;charset=utf-8\r\n"); + for (Pair pair : pairs) { + dos.writeBytes(pair.toString() + "\r\n"); + } + dos.writeBytes("\r\n"); + dos.flush(); + } catch (IOException e) { + log.error(e.getMessage()); + } + } +} diff --git a/webapp/index.html b/webapp/index.html index 4ee2a02d..939884b9 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -71,7 +71,7 @@
  • 로그인
  • 회원가입
  • --> -
  • 로그아웃
  • +
  • 로그아웃
  • 개인정보수정