요구사항 | "로그인" 메뉴를 클릭하면 http://localhost:8080/user/login.html로 이동해 로그인할 수 있다. 로그인이 성공하면 /index.html로 이동하고, 로그인이 실패하면 /user/login_failed.html로 이동해야 한다. 앞에서 회원가입한 사용자로 로그인할 수 있어야 한다. 로그인이 성공하면 로그인 상태를 유지할 수 있어야 한다. 로그인이 성공할 경우 요청 헤더의 Cookie 헤더 값이 logined=true, 로그인이 실패하면 Cookie 헤더 값이 logined=false로 전달되어야 한다. |
HTTP는 요청을 보내고 응답을 받으면 클라이언트와 서버 간의 연결을 끊는다.
※ HTTP가 클라이언트와 서버의 연결을 매 요청마다 맺고, 끊는다면 성능이 많이 떨어질 것이다. 이 같은 단점을 보완하고 성능을 높이기 위해 HTTP 1.1부터는 한 번 맺은 연결을 재사용한다. 연결은 재사용하지만 각 요청 간의 상태 데이터를 공유할 수는 없는 무상태 프로토콜의 특성을 가진다. HTTP의 무상태 프로토콜에 대해 좀 더 명확히 이해하도록 하기 위해 이와 같이 설명했다.
이와 같이 클라이언트와 서버 간의 연결을 끊기 때문에 각 요청 사이에 상태를 공유할 수 없다. 이 때문에 HTTP를 무상태 프로토콜이라고 한다.
HTTP가 무상태 프로토콜이기 때문에 서버는 클라이언트가 누구인지 식별할 수 있는 방법이 없다는 문제가 발생한다. 서버가 클라이언트를 식별할 수 없기 때문에 앞에서 클라이언트가 한 행위를 기억할 수 없다. 웹 애플리케이션에서 대표적인 기능 중의 하나인 로그인 기능을 예로 들어보자. 로그인을 완료하면 매 요청마다 로그인을 다시하지 않는다. 분명 HTTP는 클라이언트가 한 행위를 기억할 수 없다고 했는데 로그인한 결과는 어떻게 기억할 수 있을까?
HTTP는 로그인과 같이 클라이언트의 행위를 기억하기 위한 목적으로 지원하는 것이 쿠키(Cookie) 이다. HTTP가 쿠키를 지원하는 방법은 다음과 같다. 먼저 서버(클라이언트에서도 가능하지만 이 예는 서버로부터 시작한다)에서 로그인 요청을 받으면 로그인 성공/실패 여부에 따라 응답 헤더에 Set-Cookie로 결과 값을 저장할 수 있다. 클라이언트는 응답 헤더에 Set-Cookie가 존재할 경우 Set-Cookie의 값을 읽어 서버에 보내는 요청 헤더의 Cookie 헤더 값으로 다시 전송한다. 즉, HTTP는 각 요청 간에 데이터를 공유할 방법이 없기 때문에 헤더를 통해 공유할 데이터를 매번 다시 전송하는 방식으로 데이터를 공유한다.
이 과정을 코드로 구현하면 다음과 같다.
- RequestHandler.java
package webserver;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
import java.nio.file.Files;
import java.net.Socket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RequestHandler extends Thread {
private static final Logger log = LoggerFactory.getLogger(RequestHandler.class);
private Socket connection;
public RequestHandler(Socket connectionSocket) {
this.connection = connectionSocket;
}
public void run() {
log.debug("New Client Connect! Connected IP : {}, Port : {}", connection.getInetAddress(), connection.getPort());
try (InputStream in = connection.getInputStream(); OutputStream out = connection.getOutputStream()) {
// TODO 사용자 요청에 대한 처리는 이 곳에 구현하면 된다.
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String line = br.readLine();
log.debug("request line : {}", line);
if (line == null) {
return;
}
String[] tokens = line.split(" ");
int contentLength = 0;
while (!line.equals("")) {
line = br.readLine();
log.debug("header : {}", line);
if (line.contains("Content-Length")) {
contentLength = getContentLength(line);
}
}
String url = tokens[1];
// POST
if ("/user/create".equals(url)) {
String body = IOUtils.readData(br, contentLength);
Map<String, Object> params = HttpRequestUtils.parseQueryString(body);
User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email"));
/*
log.debug("User : {}", user);
DataOutputStream dos = new DataOutputStream(out);
response302Header(dos, "/index.html");
*/
DataBase.addUser(user);
} else if ("/user/login".equals(url)) {
String body = IOUtils.readData(br, contentLength);
Map<String, String> params = HttpRequestUtils.parseQueryString(body);
User user = DataBase.findUserById(params.get("userId"));
if (user == null) {
responseResource(out, "/user/login_failed.html");
return;
}
if (user.getPassword().equals(params.get("password"))) {
DataOutputStream dos = new DataOutputStream(out);
response302LoginSuccessHeader(dos);
} else {
responseResource(out, "/user/login_failed.html");
}
}
/*
// GET
if (url.startsWith("/user/create")) {
int index = url.indexOf("?");
String queryString = url.substring(index + 1);
Map<String, Object> params = HttpRequestUtils.parseQueryString(queryString);
User user = new User(params.get("userId"), params.get("password"), params.get("name"), params.get("email"));
log.debug("User : {}", user);
}
*/
else {
/*
DataOutputStream dos = new DataOutputStream(out);
// byte[] body = "Hello World".getBytes();
byte[] body = Files.readAllBytes(new File("./webapp" + tokens[1]).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
*/
responseResource(out, url);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
private void responseResource(OutputStream out, String url) throws IOException {
DataOutputStream dos = new DataOutputStream(out);
byte[] body = Files.readAllBytes(new File("./webapp" + url).toPath());
response200Header(dos, body.length);
responseBody(dos, body);
}
private void response302LoginSuccessHeader(DataOutputStream dos) {
try {
dos.writeBytes("HTTP/1.1 302 Redirect \r\n");
dos.writeBytes("Set-Cookie: logined=true \r\n");
dos.writeBytes("Location: /index.html \r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
private int getContentLength(String line) {
String[] headerTokens = line.split(":");
return Integer.parseInt(headerTokens[1].trim());
}
private void response302Header(DataOutputStream dos, String url) {
try {
dos.writeBytes("HTTP/1.1 302 Redirect \r\n");
dos.writeBytes("Location: " + url + " \r\n");
dos.writeBytes("\r\n");
} catch {
log.error(e.getMessage());
}
}
private void response200Header(DataOutputStream dos, int lengthOfBodyContent) {
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: " + lengthOfBodyContent + "\r\n");
dos.writeBytes("\r\n");
} catch (IOException e) {
log.error(e.getMessage());
}
}
private void responseBody(DataOutputStream dos, byte[] body) {
try {
dos.write(body, 0, body.length);
dos.flush();
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
로그인 기능을 구현하려면 먼저 회원가입한 사용자를 어딘가에 저장하고 있어야 하는데 이 작업을 DataBase 클래스가 담당하고 있다.
로그인이 성공하면 응답 헤더에 Set-Cookie 헤더의 값으로 logined=true를 전달했다. 위와 같이 구현을 완료한 후 서버를 재시작하고 회원가입, 로그인 순으로 테스트를 진행한다. 로그인에 성공한 다음부터의 요청 헤더를 살펴본다. 요청 헤더에 다음과 같이 Cookie라는 이름의 헤더가 추가되었으며 그 값이 logined=true라는 것을 확인할 수 있다.
Cookie: logined=true
이와 같이 모든 요청에 로그인 성공 유무에 대한 정보가 전달되기 때문에 서버는 클라이언트의 Cookie 요청 헤더를 확인해 logined 값이 true인지 여부로 판단하면 로그인 상태 유무를 확인할 수 있다.
참고도서 : https://roadbook.co.kr/169
[신간안내] 자바 웹 프로그래밍 Next Step
● 저자: 박재성 ● 페이지: 480 ● 판형: 사륙배변형(172*225) ● 도수: 1도 ● 정가: 30,000원 ● 발행일: 2016년 9월 19일 ● ISBN: 978-89-97924-24-0 93000 [강컴] [교보] [반디] [알라딘] [예스24] [인터파크] [샘
roadbook.co.kr
'교재 실습 > 자바 웹 프로그래밍 Next Step' 카테고리의 다른 글
4.2.1.7 요구사항 7 - CSS 지원하기 (6) | 2025.03.12 |
---|---|
4.2.1.6 요구사항 6 - 사용자 목록 출력 (3) | 2025.03.11 |
4.2.1.4 요구사항 4 - 302 status code 적용 (2) | 2025.02.21 |
4.2.1.3 요구사항 3 - POST 방식으로 회원가입 하기 (2) | 2025.02.20 |
4.2.1.2 요구사항 2 - GET 방식으로 회원가입하기 (0) | 2025.02.17 |
댓글