8. セッション管理とユーザー認証
HTTPは、リクエストごとに独立した仕組みです。ブラウザが連続してアクセスしても、サーバは何もしなければ「同じユーザーの続きの操作」だと判断できません。
この章では、Cookieとサーバ側の保存領域を使って、ログイン状態を維持する基本を扱います。C言語CGIで実装することで、セッション管理がどのような部品で成り立っているかを確認します。
この章で学ぶこと
- HTTPがステートレスであること
- CookieでセッションIDを受け渡しする仕組み
- 推測されにくいセッションIDの発行
- サーバ側にセッション情報を保存する方法
- ログイン、保護ページ、ログアウトの基本フロー
- セッション管理で最低限注意するセキュリティ要件
セッションとは何か
セッションとは、複数のリクエストをひとつの利用者の操作として扱うための仕組みです。
典型的には、次のように動きます。
ログイン成功
-> サーバがセッションIDを発行
-> セッションIDをCookieでブラウザに渡す
-> サーバ側にセッションIDとユーザー情報を保存する
-> 次回以降、CookieのセッションIDでログイン状態を確認する
Cookieに保存するのは、ユーザー名や権限そのものではなく、推測されにくいセッションIDだけにします。ログイン中のユーザーIDなどの実体はサーバ側に保存します。
セッションIDの要件
セッションIDは、ログイン状態を表す重要な鍵です。次の性質が必要です。
- 十分に長い
- 推測しにくい
- ログイン成功時に新しく発行される
- URLではなくCookieで送る
- ファイル名などに使う場合は安全な文字だけに制限する
rand() や現在時刻だけで生成した値は、推測される可能性があります。この章では、Linuxの /dev/urandom から乱数を読み、16進文字列に変換する例を使います。
#include <stdio.h>
#include <stddef.h>
int generate_session_id(char *out, size_t out_size) {
unsigned char bytes[32];
const char hex[] = "0123456789abcdef";
if (out_size < sizeof(bytes) * 2 + 1) {
return 0;
}
FILE *fp = fopen("/dev/urandom", "rb");
if (fp == NULL) {
return 0;
}
size_t n = fread(bytes, 1, sizeof(bytes), fp);
fclose(fp);
if (n != sizeof(bytes)) {
return 0;
}
for (size_t i = 0; i < sizeof(bytes); i++) {
out[i * 2] = hex[bytes[i] >> 4];
out[i * 2 + 1] = hex[bytes[i] & 0x0f];
}
out[sizeof(bytes) * 2] = '\0';
return 1;
}
この関数は64文字の16進セッションIDを生成します。
セッションIDを検証する
セッションIDをファイル名として使う場合、Cookieから受け取った値をそのままパスに連結してはいけません。../ のような文字列を含められると、意図しないファイルを読まれる可能性があります。
この章のセッションIDは16進文字だけに制限します。
#include <ctype.h>
#include <string.h>
int is_valid_session_id(const char *sid) {
if (sid == NULL || strlen(sid) != 64) {
return 0;
}
for (int i = 0; sid[i] != '\0'; i++) {
if (!isxdigit((unsigned char)sid[i])) {
return 0;
}
}
return 1;
}
この検証を通した値だけを、セッションファイル名として使います。
セッション情報を保存する
学習用として、セッション情報をファイルに保存します。保存先は /tmp よりも、専用ディレクトリの方が扱いやすいです。
/var/lib/c-cgi-book/sessions/
ディレクトリは、Apacheの実行ユーザーだけが読み書きできるようにします。具体的な作成例はAppendixで扱っています。
セッションファイルのパスを組み立てる関数です。
#include <stdio.h>
#define SESSION_DIR "/var/lib/c-cgi-book/sessions"
int build_session_path(char *out, size_t out_size, const char *sid) {
if (!is_valid_session_id(sid)) {
return 0;
}
int n = snprintf(out, out_size, "%s/%s", SESSION_DIR, sid);
return n > 0 && (size_t)n < out_size;
}
セッションファイルには、ここではユーザー名だけを書き込むことにします。
#include <stdio.h>
int save_session(const char *sid, const char *username) {
char path[256];
if (!build_session_path(path, sizeof(path), sid)) {
return 0;
}
FILE *fp = fopen(path, "w");
if (fp == NULL) {
return 0;
}
fprintf(fp, "username=%s\n", username);
fclose(fp);
return 1;
}
実用コードでは、ユーザーID、発行時刻、最終アクセス時刻、CSRF対策用トークンなどを保存することがあります。
CookieでセッションIDを渡す
ログインに成功したら、セッションIDをCookieとしてブラウザに渡します。
printf("Set-Cookie: SESSION_ID=%s; Path=/; HttpOnly; SameSite=Lax\r\n", sid);
HTTPSで運用する場合は Secure も付けます。
printf("Set-Cookie: SESSION_ID=%s; Path=/; HttpOnly; Secure; SameSite=Lax\r\n", sid);
Set-Cookie はHTTPヘッダなので、HTML本文を出す前に出力します。
printf("Set-Cookie: SESSION_ID=%s; Path=/; HttpOnly; SameSite=Lax\r\n", sid);
printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
ログイン処理の流れ
ログイン処理は、次の順序で行います。
フォームから username と password を受け取る
ユーザー情報をDBから取得する
パスワードを検証する
成功したら新しいセッションIDを発行する
セッション情報をサーバ側に保存する
CookieにセッションIDをセットする
ログイン後ページへ案内する
この章では、フォーム解析やDB照合の実装詳細は省略し、セッション開始部分に集中します。
char sid[65];
if (!password_is_valid(username, password)) {
printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
printf("<p>Login failed.</p>\n");
return 0;
}
if (!generate_session_id(sid, sizeof(sid))) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain; charset=UTF-8\r\n\r\n");
printf("failed to generate session id\n");
return 1;
}
if (!save_session(sid, username)) {
printf("Status: 500 Internal Server Error\r\n");
printf("Content-Type: text/plain; charset=UTF-8\r\n\r\n");
printf("failed to save session\n");
return 1;
}
printf("Set-Cookie: SESSION_ID=%s; Path=/; HttpOnly; SameSite=Lax\r\n", sid);
printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
printf("<p>Login succeeded.</p>\n");
printf("<p><a href=\"/cgi-bin/protected.cgi\">protected page</a></p>\n");
password_is_valid() は、DBに保存されたパスワードハッシュと照合する処理を想定しています。パスワードを平文保存したり、Cコードに直接書いたりする構成は避けます。
CookieからセッションIDを読む
CGIでは、Cookieは HTTP_COOKIE 環境変数に入ります。
SESSION_ID=abc123; theme=light
実用的にはCookieパーサを用意しますが、ここでは SESSION_ID= を探す簡易例を示します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int get_session_id_from_cookie(char *out, size_t out_size) {
const char *cookie = getenv("HTTP_COOKIE");
const char *name = "SESSION_ID=";
size_t name_len = strlen(name);
if (cookie == NULL) {
return 0;
}
const char *p = cookie;
while (*p != '\0') {
while (*p == ' ' || *p == ';') {
p++;
}
if (strncmp(p, name, name_len) == 0) {
p += name_len;
break;
}
p = strchr(p, ';');
if (p == NULL) {
return 0;
}
}
if (*p == '\0') {
return 0;
}
size_t i = 0;
while (p[i] != '\0' && p[i] != ';' && i + 1 < out_size) {
out[i] = p[i];
i++;
}
out[i] = '\0';
return is_valid_session_id(out);
}
この簡易実装は、Cookieの値に複雑なエンコードが含まれない前提です。本格的な実装では、Cookieの構文に沿ってより厳密に解析します。
ログイン状態を確認する
CookieからセッションIDを取り出し、対応するセッションファイルを確認します。
#include <stdio.h>
#include <string.h>
int load_session_username(char *username, size_t username_size) {
char sid[65];
char path[256];
char line[256];
if (!get_session_id_from_cookie(sid, sizeof(sid))) {
return 0;
}
if (!build_session_path(path, sizeof(path), sid)) {
return 0;
}
FILE *fp = fopen(path, "r");
if (fp == NULL) {
return 0;
}
if (fgets(line, sizeof(line), fp) == NULL) {
fclose(fp);
return 0;
}
fclose(fp);
const char *prefix = "username=";
if (strncmp(line, prefix, strlen(prefix)) != 0) {
return 0;
}
snprintf(username, username_size, "%s", line + strlen(prefix));
username[strcspn(username, "\r\n")] = '\0';
return username[0] != '\0';
}
保護ページでは、この関数でログイン状態を確認します。
int main(void) {
char username[128];
if (!load_session_username(username, sizeof(username))) {
printf("Status: 302 Found\r\n");
printf("Location: /login.html\r\n\r\n");
return 0;
}
printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
printf("<!DOCTYPE html>\n");
printf("<html lang=\"ja\"><body>\n");
printf("<h1>Protected page</h1>\n");
printf("<p>ログイン中: ");
html_escape_print(stdout, username);
printf("</p>\n");
printf("<p><a href=\"/cgi-bin/logout.cgi\">logout</a></p>\n");
printf("</body></html>\n");
return 0;
}
ユーザー名も外部入力由来の値として扱い、HTMLに出すときはエスケープします。
ログアウト処理
ログアウトでは、サーバ側のセッションファイルを削除し、Cookieを期限切れにします。
#include <stdio.h>
#include <unistd.h>
int main(void) {
char sid[65];
char path[256];
if (get_session_id_from_cookie(sid, sizeof(sid)) &&
build_session_path(path, sizeof(path), sid)) {
unlink(path);
}
printf("Set-Cookie: SESSION_ID=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0\r\n");
printf("Content-Type: text/html; charset=UTF-8\r\n\r\n");
printf("<p>Logged out.</p>\n");
printf("<p><a href=\"/login.html\">login</a></p>\n");
return 0;
}
HTTPS環境では、ログイン時と同じく Secure 属性も付けます。
セッションの有効期限
セッションを無期限に有効にすると、放置されたログイン状態が悪用される可能性があります。
実用コードでは、セッションファイルに次のような情報を保存し、アクセス時に確認します。
- 発行時刻
- 最終アクセス時刻
- ユーザーID
- 必要に応じた権限情報
たとえば、最終アクセスから30分を超えたら無効化する、といったルールを設けます。
セキュリティ上の注意
この章の実装は学習用ですが、セッション管理では次の点を最初から意識しておきます。
- ログイン成功時には必ず新しいセッションIDを発行する
- セッションIDは推測困難な乱数から作る
- Cookieには
HttpOnlyとSameSiteを付ける - HTTPSでは
Secureも付ける - セッションIDをファイルパスに使う前に文字種と長さを検証する
- セッションファイルの保存先は権限を絞る
- パスワードは平文保存せず、ハッシュ化して検証する
- エラー詳細をブラウザに出さない
- HTML出力時はユーザー名などもエスケープする
CGIでは、セッション管理の部品がすべて見えます。そのぶん、Cookie、ファイル、入力検証、出力エスケープの責任をプログラム側で明確に持つ必要があります。
小まとめ
- HTTPはステートレスなので、ログイン状態の維持にはセッションが必要です。
- CookieにはセッションIDだけを入れ、実体はサーバ側に保存します。
- セッションIDは
/dev/urandomなどから推測困難な値として生成します。 - Cookie属性、ファイル権限、ID検証、タイムアウトを組み合わせて安全性を高めます。
- 保護ページとログアウト処理も、セッションIDの検証を通して実装します。
次章では、ここまでのフォーム処理、DB連携、セッション管理を組み合わせて、簡易CMSを作ります。