概要
HTTP サーバー自作連載の第6回では、Cookie とセッションの最小実装を扱います。HTTP がステートレスであることは理解していても、ログイン状態のような『リクエストをまたいで残る値』をどう扱うのかは、実際に一度書いてみると見えやすくなります。この記事では `Set-Cookie` と `Cookie` ヘッダーを自分で組み立て、サーバー側の `Map` をセッションストアとして使うことで、状態管理の最小形を確認します。
使いどころ
Cookie とサーバー側セッションがどう対応しているかを整理したい
ログイン状態のような値を、最小構成でどう保持するかを見たい
状態管理を自分で書くと何が増えるかを把握したい
コード例
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CookieSessionServerSample {
static final int PORT = 8085;
static final Map<String, String> SESSIONS =
new ConcurrentHashMap<>();
static void sendResponse(OutputStream out, int statusCode,
String statusMessage, String contentType,
String body, String setCookie) throws IOException {
var writer = new PrintWriter(
new OutputStreamWriter(out, "UTF-8"), true);
var bodyBytes = body.getBytes("UTF-8");
writer.print("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n");
writer.print("Content-Type: " + contentType + "; charset=UTF-8\r\n");
writer.print("Content-Length: " + bodyBytes.length + "\r\n");
if (setCookie != null) {
writer.print("Set-Cookie: " + setCookie + "\r\n");
}
writer.print("Connection: close\r\n\r\n");
writer.flush();
out.write(bodyBytes);
out.flush();
}
static void sendRedirect(OutputStream out, String location,
String setCookie) throws IOException {
var writer = new PrintWriter(
new OutputStreamWriter(out, "UTF-8"), true);
writer.print("HTTP/1.1 303 See Other\r\n");
writer.print("Location: " + location + "\r\n");
if (setCookie != null) {
writer.print("Set-Cookie: " + setCookie + "\r\n");
}
writer.print("Content-Length: 0\r\n");
writer.print("Connection: close\r\n\r\n");
writer.flush();
}
static Map<String, String> parseFormBody(String body)
throws UnsupportedEncodingException {
var params = new HashMap<String, String>();
if (body == null || body.isEmpty()) {
return params;
}
for (var pair : body.split("&")) {
var kv = pair.split("=", 2);
var key = URLDecoder.decode(kv[0], "UTF-8");
var value = kv.length > 1
? URLDecoder.decode(kv[1], "UTF-8") : "";
params.put(key, value);
}
return params;
}
static Map<String, String> parseCookies(String cookieHeader) {
var cookies = new HashMap<String, String>();
if (cookieHeader == null || cookieHeader.isEmpty()) {
return cookies;
}
for (var pair : cookieHeader.split(";")) {
var kv = pair.trim().split("=", 2);
if (kv.length == 2) {
cookies.put(kv[0], kv[1]);
}
}
return cookies;
}
static String buildTopPage(String username) {
if (username == null) {
return """
<html><body>
<h1>ログイン</h1>
<form method='POST' action='/login'>
<input name='name' placeholder='名前' required>
<button>ログイン</button>
</form>
</body></html>
""";
}
return """
<html><body>
<h1>ようこそ %s さん</h1>
<form method='POST' action='/logout'>
<button>ログアウト</button>
</form>
</body></html>
""".formatted(username);
}
static void handleRequest(Socket clientSocket) throws IOException {
try (var in = clientSocket.getInputStream();
var out = clientSocket.getOutputStream()) {
var reader = new BufferedReader(
new InputStreamReader(in, "UTF-8"));
var requestLine = reader.readLine();
if (requestLine == null || requestLine.isEmpty()) {
return;
}
var parts = requestLine.split(" ");
var method = parts[0];
var path = parts.length > 1 ? parts[1] : "/";
var headers = new HashMap<String, String>();
String line;
while ((line = reader.readLine()) != null && !line.isEmpty()) {
var idx = line.indexOf(':');
if (idx > 0) {
headers.put(line.substring(0, idx).trim().toLowerCase(),
line.substring(idx + 1).trim());
}
}
var body = "";
if ("POST".equals(method)) {
var contentLength = headers.get("content-length");
if (contentLength != null) {
var length = Integer.parseInt(contentLength);
var buf = new char[length];
reader.read(buf, 0, length);
body = new String(buf);
}
}
var cookies = parseCookies(headers.get("cookie"));
var sessionId = cookies.get("SID");
var username = sessionId == null ? null : SESSIONS.get(sessionId);
if ("GET".equals(method) && "/".equals(path)) {
sendResponse(out, 200, "OK", "text/html",
buildTopPage(username), null);
} else if ("POST".equals(method) && "/login".equals(path)) {
var params = parseFormBody(body);
var name = params.getOrDefault("name", "").trim();
if (name.isEmpty()) {
sendResponse(out, 400, "Bad Request",
"text/plain", "name is required", null);
return;
}
var newSessionId = URLEncoder.encode(
UUID.randomUUID().toString(), "UTF-8");
SESSIONS.put(newSessionId, name);
sendRedirect(out, "/", "SID=" + newSessionId
+ "; Path=/; HttpOnly");
} else if ("POST".equals(method) && "/logout".equals(path)) {
if (sessionId != null) {
SESSIONS.remove(sessionId);
}
sendRedirect(out, "/", "SID=deleted; Path=/; Max-Age=0");
} else {
sendResponse(out, 404, "Not Found",
"text/plain", "404 Not Found", null);
}
}
}
public static void main(String[] args) throws IOException {
ExecutorService executor = Executors.newFixedThreadPool(10);
System.out.println("Cookie Session Server 起動中... http://localhost:" + PORT);
try (var serverSocket = new ServerSocket(PORT)) {
while (true) {
var clientSocket = serverSocket.accept();
executor.submit(() -> {
try {
handleRequest(clientSocket);
} catch (IOException e) {
System.out.println("エラー: " + e.getMessage());
} finally {
try {
clientSocket.close();
} catch (IOException ignored) {
}
}
});
}
}
}
}Version Coverage
テキストブロックと `formatted` により、画面 HTML やレスポンス生成を整理しやすい。
var cookies = parseCookies(headers.get("cookie"));
var sessionId = cookies.get("SID");
var username = sessionId == null ? null : SESSIONS.get(sessionId);Library Comparison
注意点
この実装はメモリ上の Map を使うため、再起動や複数台構成は前提にしていない
セッション ID は予測しにくい値で生成し、Cookie の属性も意識しておきたい
HttpOnly、Secure、SameSite などの属性は、学習用でも存在だけは把握しておくと後でつながりやすい
認証やセッション失効を実務で扱うには、CSRF や有効期限管理まで含めて考える必要がある
FAQ
そのまま使う前提ではありません。この記事は Cookie とセッションの関係を理解するための学習用であり、実運用では有効期限や分散環境対応まで含めて別途設計が必要です。
違います。Cookie はブラウザに保存される小さなデータで、セッションは通常サーバー側で管理されます。ブラウザは Cookie でセッション ID を送り返します。
URL に載せるとログやリファラに残りやすく、共有や漏洩のリスクが高まるためです。Cookie で渡すほうが扱いやすくなります。
本番向けの構成とは考えないほうがよいです。Secure 属性、SameSite、CSRF 対策、期限切れ管理、分散環境対応まで含めると、別の基盤を前提にしたほうが整理しやすくなります。