8. セッション管理とユーザー認証

HTTPは、リクエストごとに独立した仕組みです。ブラウザが連続してアクセスしても、サーバは何もしなければ「同じユーザーの続きの操作」だと判断できません。

この章では、Cookieとサーバ側の保存領域を使って、ログイン状態を維持する基本を扱います。C言語CGIで実装することで、セッション管理がどのような部品で成り立っているかを確認します。


この章で学ぶこと


セッションとは何か

セッションとは、複数のリクエストをひとつの利用者の操作として扱うための仕組みです。

典型的には、次のように動きます。

ログイン成功
  -> サーバがセッションIDを発行
  -> セッションIDをCookieでブラウザに渡す
  -> サーバ側にセッションIDとユーザー情報を保存する
  -> 次回以降、CookieのセッションIDでログイン状態を確認する

Cookieに保存するのは、ユーザー名や権限そのものではなく、推測されにくいセッションIDだけにします。ログイン中のユーザーIDなどの実体はサーバ側に保存します。


セッションIDの要件

セッションIDは、ログイン状態を表す重要な鍵です。次の性質が必要です。

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 属性も付けます。


セッションの有効期限

セッションを無期限に有効にすると、放置されたログイン状態が悪用される可能性があります。

実用コードでは、セッションファイルに次のような情報を保存し、アクセス時に確認します。

たとえば、最終アクセスから30分を超えたら無効化する、といったルールを設けます。


セキュリティ上の注意

この章の実装は学習用ですが、セッション管理では次の点を最初から意識しておきます。

CGIでは、セッション管理の部品がすべて見えます。そのぶん、Cookie、ファイル、入力検証、出力エスケープの責任をプログラム側で明確に持つ必要があります。


小まとめ

次章では、ここまでのフォーム処理、DB連携、セッション管理を組み合わせて、簡易CMSを作ります。