メインコンテンツまでスキップ

時刻とタイムゾーン

所要時間: 40分


はじめに

認証・認可システムでは、時刻の扱いが非常に重要です。

  • トークンの有効期限(exp
  • トークンの発行時刻(iat
  • 監査ログのタイムスタンプ
  • セッションのタイムアウト

時刻の扱いを間違えると、「トークンが期限切れ」「認証が通らない」といった問題が発生します。


1. Unix時間(エポック秒)

Unix時間とは

1970年1月1日 00:00:00 UTC からの経過秒数です。

┌─────────────────────────────────────────────────────────────────────┐
│ なぜ 1970年1月1日 なのか │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Unix OS が開発されたのが 1969〜1971年頃(Bell Labs) │
│ │
│ 当初は 1971年1月1日 を基準に 1/60秒 単位でカウントしていた │
│ → 32ビットでは約2.5年でオーバーフロー │
│ │
│ そこで: │
│ ・単位を「秒」に変更(より長期間カウント可能) │
│ ・基準を「1970年1月1日」に変更(キリの良い年) │
│ │
│ 結果: 開発時期に近いキリの良い年として 1970年 が選ばれた │
│ (深い理由はなく、実用的な選択) │
│ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Unix時間(Epoch Time) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 基準点: 1970年1月1日 00:00:00 UTC(Unix Epoch) │
│ │
│ 例: │
│ ・0 = 1970-01-01 00:00:00 UTC │
│ ・1000000000 = 2001-09-09 01:46:40 UTC │
│ ・1704067200 = 2024-01-01 00:00:00 UTC │
│ ・2147483647 = 2038-01-19 03:14:07 UTC(32ビット上限) │
│ │
│ 特徴: │
│ ・タイムゾーンに依存しない(常にUTC基準) │
│ ・整数なので計算・比較が簡単 │
│ ・JWTのexp, iat, nbfで使用 │
│ │
└─────────────────────────────────────────────────────────────────────┘

秒 vs ミリ秒

┌─────────────────────────────────────────────────────────────────────┐
│ 秒とミリ秒の混同に注意 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Unix時間(秒): │
│ 1704067200 │
│ → 2024-01-01 00:00:00 UTC │
│ │
│ Unixミリ秒: │
│ 1704067200000 │
│ → 2024-01-01 00:00:00 UTC(同じ日時) │
│ │
│ 間違えると: │
│ ❌ ミリ秒を秒として解釈 │
│ 1704067200000 秒 = 西暦55,987年頃(遥か未来) │
│ │
│ ❌ 秒をミリ秒として解釈 │
│ 1704067200 ミリ秒 = 1970-01-20(約20日後) │
│ │
│ ✅ 桁数で判断: 10桁 = 秒、13桁 = ミリ秒 │
│ │
└─────────────────────────────────────────────────────────────────────┘

JWTでの使用

┌─────────────────────────────────────────────────────────────────────┐
│ JWTの時刻関連クレーム │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ { │
│ "sub": "user123", │
│ "iat": 1704067200, ← Issued At(発行時刻) │
│ "nbf": 1704067200, ← Not Before(有効開始時刻) │
│ "exp": 1704070800, ← Expiration(有効期限) │
│ "auth_time": 1704067000 ← 最終認証時刻 │
│ } │
│ │
│ 検証ロジック: │
│ ・現在時刻 < nbf → まだ有効ではない │
│ ・現在時刻 > exp → 期限切れ │
│ │
│ ※ 全てUnix時間(秒)、ミリ秒ではない │
│ │
└─────────────────────────────────────────────────────────────────────┘

実践例

# 現在のUnix時間を取得
$ date +%s
1704067200

# Unix時間を人間が読める形式に変換
$ date -r 1704067200
Mon Jan 1 09:00:00 JST 2024

# UTC で表示
$ date -u -r 1704067200
Mon Jan 1 00:00:00 UTC 2024
// Java
import java.time.Instant;

// 現在のUnix時間(秒)
long epochSecond = Instant.now().getEpochSecond();

// Unix時間からInstantを作成
Instant instant = Instant.ofEpochSecond(1704067200);

// ミリ秒の場合
long epochMilli = Instant.now().toEpochMilli();
Instant fromMilli = Instant.ofEpochMilli(1704067200000L);

2. UTCとタイムゾーン

UTC(協定世界時)

┌─────────────────────────────────────────────────────────────────────┐
│ UTC(Coordinated Universal Time) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ・世界の標準時刻 │
│ ・イギリス(グリニッジ)の時刻とほぼ同じ │
│ ・サーバーの標準タイムゾーン │
│ │
│ 表記: │
│ ・2024-01-01T00:00:00Z ← "Z" は UTC を意味する(Zulu time) │
│ ・2024-01-01T00:00:00+00:00 │
│ │
│ なぜ UTC を使うか: │
│ ・タイムゾーンに依存しない一貫した時刻 │
│ ・夏時間(DST)の影響を受けない │
│ ・サーバー間で時刻を比較しやすい │
│ │
└─────────────────────────────────────────────────────────────────────┘

タイムゾーンオフセット

┌─────────────────────────────────────────────────────────────────────┐
│ 主要なタイムゾーン │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ タイムゾーン オフセット 地域 │
│ ───────────────────────────────────────── │
│ UTC +00:00 イギリス(冬) │
│ JST +09:00 日本 │
│ PST -08:00 米国西海岸(冬) │
│ PDT -07:00 米国西海岸(夏) │
│ EST -05:00 米国東海岸(冬) │
│ EDT -04:00 米国東海岸(夏) │
│ CET +01:00 中央ヨーロッパ(冬) │
│ CEST +02:00 中央ヨーロッパ(夏) │
│ │
│ JST の計算: │
│ JST = UTC + 9時間 │
│ 例: UTC 00:00 = JST 09:00 │
│ │
└─────────────────────────────────────────────────────────────────────┘

タイムゾーンID vs オフセット

┌─────────────────────────────────────────────────────────────────────┐
│ タイムゾーンIDとオフセットの違い │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ オフセット(固定): │
│ +09:00 │
│ → 常に UTC+9、夏時間の変動なし │
│ │
│ タイムゾーンID(可変): │
│ America/Los_Angeles │
│ → 冬は PST(-08:00)、夏は PDT(-07:00)に自動切り替え │
│ │
│ Asia/Tokyo │
│ → 日本は夏時間がないので常に +09:00 │
│ │
│ 推奨: │
│ ・保存時: UTC(またはUnix時間) │
│ ・表示時: ユーザーのタイムゾーンに変換 │
│ │
└─────────────────────────────────────────────────────────────────────┘

3. ISO 8601 形式

フォーマット

┌─────────────────────────────────────────────────────────────────────┐
│ ISO 8601 日時形式 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 基本形式: │
│ YYYY-MM-DDTHH:mm:ss.sssZ │
│ │
│ 例: │
│ 2024-01-15T09:30:00Z ← UTC │
│ 2024-01-15T09:30:00+09:00 ← JST │
│ 2024-01-15T09:30:00.123Z ← ミリ秒付き │
│ 2024-01-15T09:30:00.123456Z ← マイクロ秒付き │
│ │
│ "T" = 日付と時刻の区切り │
│ "Z" = UTC(Zulu time) │
│ "+09:00" = UTCからのオフセット │
│ │
└─────────────────────────────────────────────────────────────────────┘

APIでの使用

┌─────────────────────────────────────────────────────────────────────┐
│ API レスポンスでの日時表現 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ パターン1: ISO 8601(人間が読みやすい) │
│ { │
│ "created_at": "2024-01-15T09:30:00Z", │
│ "updated_at": "2024-01-15T10:00:00Z" │
│ } │
│ │
│ パターン2: Unix時間(計算しやすい) │
│ { │
│ "created_at": 1705311000, │
│ "updated_at": 1705312800 │
│ } │
│ │
│ JWT: Unix時間(秒)を使用 │
│ REST API: ISO 8601 が一般的 │
│ │
└─────────────────────────────────────────────────────────────────────┘

4. クロックスキュー

クロックスキューとは

┌─────────────────────────────────────────────────────────────────────┐
│ クロックスキュー問題 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ サーバー間で時刻がずれている状態 │
│ │
│ 認可サーバー クライアント │
│ 12:00:00 12:00:05(5秒進んでいる) │
│ │
│ 問題のシナリオ: │
│ 1. 認可サーバーが 12:00:00 にトークン発行 │
│ iat = 1704067200(12:00:00) │
│ │
│ 2. クライアントが 12:00:05 に受信 │
│ 「iat が未来の時刻?不正なトークン!」 │
│ │
│ 対策: │
│ ・NTP で時刻同期 │
│ ・検証時に許容範囲(clock skew tolerance)を設定 │
│ 例: ±60秒 の誤差を許容 │
│ │
└─────────────────────────────────────────────────────────────────────┘

許容範囲の設定

// JWT検証時のクロックスキュー許容
// nimbus-jose-jwt の例
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();

// 60秒の許容範囲
long clockSkewSeconds = 60;

Date now = new Date();
Date expiration = claimsSet.getExpirationTime();
Date notBefore = claimsSet.getNotBeforeTime();

// exp チェック(許容範囲を考慮)
if (expiration != null) {
Date expirationWithSkew = new Date(expiration.getTime() + clockSkewSeconds * 1000);
if (now.after(expirationWithSkew)) {
throw new JWTExpiredException("Token expired");
}
}

// nbf チェック(許容範囲を考慮)
if (notBefore != null) {
Date notBeforeWithSkew = new Date(notBefore.getTime() - clockSkewSeconds * 1000);
if (now.before(notBeforeWithSkew)) {
throw new JWTNotYetValidException("Token not yet valid");
}
}

5. よくあるバグと対策

ローカル時刻 vs UTC

┌─────────────────────────────────────────────────────────────────────┐
│ ローカル時刻を使ってしまう問題 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ 間違い: サーバーでローカル時刻(JST)を使用 │
│ │
│ // JST で現在時刻を取得 │
│ long now = System.currentTimeMillis() / 1000; │
│ // ↑ これは正しい(Javaは内部でUTC) │
│ │
│ // 問題のあるコード │
│ LocalDateTime ldt = LocalDateTime.now(); // ローカル時刻 │
│ long badEpoch = ldt.toEpochSecond(ZoneOffset.UTC); // UTCとして変換│
│ // → JSTをUTCとして扱い、9時間ずれる! │
│ │
│ ✅ 正しい方法: │
│ Instant now = Instant.now(); // 常にUTC │
│ long epoch = now.getEpochSecond(); // Unix時間 │
│ │
└─────────────────────────────────────────────────────────────────────┘

タイムゾーン変換のミス

┌─────────────────────────────────────────────────────────────────────┐
│ タイムゾーン変換のミス │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ シナリオ: UTC の時刻を JST で表示したい │
│ │
│ UTC: 2024-01-15 00:00:00 │
│ JST: 2024-01-15 09:00:00(正しい) │
│ │
│ ❌ 間違い: 単純に9時間足す │
│ → 夏時間がある地域で破綻 │
│ │
│ ✅ 正しい方法: タイムゾーンAPIを使用 │
│ │
│ Instant instant = Instant.ofEpochSecond(1705276800); │
│ ZonedDateTime utc = instant.atZone(ZoneId.of("UTC")); │
│ ZonedDateTime jst = utc.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));│
│ │
└─────────────────────────────────────────────────────────────────────┘

トークン期限切れのデバッグ

┌─────────────────────────────────────────────────────────────────────┐
│ 「トークンが期限切れ」のデバッグ │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ よくある原因: │
│ │
│ 1. 本当に期限切れ │
│ → exp の値と現在時刻を確認 │
│ │
│ 2. クロックスキュー │
│ → サーバー間の時刻同期を確認 │
│ │
│ 3. 秒とミリ秒の混同 │
│ → exp が 13桁(ミリ秒)になっていないか確認 │
│ │
│ 4. タイムゾーンの問題 │
│ → UTC で比較しているか確認 │
│ │
│ デバッグコマンド: │
│ # JWTのペイロードをデコードして exp を確認 │
│ echo "eyJ..." | base64 -d | jq '.exp' │
│ │
│ # Unix時間を人間が読める形式に変換 │
│ date -r 1704070800 │
│ │
└─────────────────────────────────────────────────────────────────────┘

6. Javaでの時刻操作

java.time パッケージ

┌─────────────────────────────────────────────────────────────────────┐
│ Java 時刻クラスの使い分け │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Instant(推奨) │
│ ・タイムゾーンなしの瞬間(UTC基準) │
│ ・サーバー内部での時刻保持に最適 │
│ ・Unix時間との相互変換が簡単 │
│ │
│ ZonedDateTime │
│ ・タイムゾーン付きの日時 │
│ ・ユーザー向け表示に使用 │
│ ・夏時間を自動処理 │
│ │
│ LocalDateTime(注意が必要) │
│ ・タイムゾーンなしの日時 │
│ ・「どのタイムゾーンか」が曖昧 │
│ ・サーバーでは基本的に使わない │
│ │
│ OffsetDateTime │
│ ・固定オフセット付きの日時 │
│ ・夏時間の概念なし │
│ │
└─────────────────────────────────────────────────────────────────────┘

実装例

import java.time.*;

// 現在時刻(UTC)
Instant now = Instant.now();

// Unix時間(秒)
long epochSecond = now.getEpochSecond();

// Unix時間からInstant
Instant fromEpoch = Instant.ofEpochSecond(1704067200);

// JSTで表示
ZonedDateTime jst = now.atZone(ZoneId.of("Asia/Tokyo"));
System.out.println(jst); // 2024-01-01T09:00:00+09:00[Asia/Tokyo]

// ISO 8601形式で出力
String iso8601 = now.toString(); // 2024-01-01T00:00:00Z

// トークン有効期限の計算
Instant expiration = now.plusSeconds(3600); // 1時間後
long exp = expiration.getEpochSecond();

避けるべきパターン

// ❌ 避けるべき: Date クラス(レガシー)
Date date = new Date();

// ❌ 避けるべき: Calendar クラス(レガシー)
Calendar cal = Calendar.getInstance();

// ❌ 避けるべき: LocalDateTime を UTC として扱う
LocalDateTime ldt = LocalDateTime.now();
long badEpoch = ldt.toEpochSecond(ZoneOffset.UTC); // 危険!

// ✅ 推奨: Instant を使用
Instant instant = Instant.now();
long goodEpoch = instant.getEpochSecond();

7. 年問題

2038年問題

┌─────────────────────────────────────────────────────────────────────┐
│ 2038年問題(Y2K38) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 原因: Unix時間を「32ビット符号付き整数」で保存 │
│ │
│ 32ビット符号付き整数の最大値: 2,147,483,647 │
│ │
│ この値が示す日時: │
│ 2038年1月19日 03:14:07 UTC │
│ │
│ 問題: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 2038-01-19 03:14:07 → 2038-01-19 03:14:08 │ │
│ │ 2,147,483,647 → -2,147,483,648(オーバーフロー) │ │
│ │ → 1901-12-13 として解釈される │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 影響を受けるシステム: │
│ ・32ビットOS(古いLinux、組み込み系) │
│ ・古いデータベース(INT型でUnix時間を保存) │
│ ・古いファイルシステム │
│ ・レガシーアプリケーション │
│ │
└─────────────────────────────────────────────────────────────────────┘

年問題の歴史

┌─────────────────────────────────────────────────────────────────────┐
│ 年問題の一覧 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 問題名 発生日 原因 │
│ ────────────────────────────────────────────────────────────────── │
│ Y2K(2000年問題) 2000-01-01 年を2桁で保存 │
│ "99" → "00" で混乱 │
│ │
│ 2038年問題 2038-01-19 03:14:07 32ビット符号付きint │
│ オーバーフロー │
│ │
│ 2106年問題 2106-02-07 06:28:15 32ビット符号なしint │
│ オーバーフロー │
│ │
│ Y10K(10000年問題) 10000-01-01 年を4桁で保存 │
│ 5桁になると破綻 │
│ │
└─────────────────────────────────────────────────────────────────────┘

対策

┌─────────────────────────────────────────────────────────────────────┐
│ 2038年問題の対策 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 64ビット整数を使用 │
│ ・64ビット符号付きintの上限: 約2920億年後 │
│ ・現代のシステムは基本的に64ビット │
│ │
│ 2. データベース設計 │
│ ❌ INT (32ビット) │
│ ✅ BIGINT (64ビット) │
│ ✅ TIMESTAMP WITH TIME ZONE │
│ │
│ 3. プログラミング言語 │
│ Java: long (64ビット) を使用 → 問題なし │
│ JavaScript: Number は 53ビット精度 → 問題なし │
│ C: time_t が 32ビットの環境は注意 │
│ │
│ 4. 確認方法 │
│ # システムの time_t サイズを確認 │
│ $ getconf LONG_BIT │
│ 64 ← 64ビットなら安全 │
│ │
└─────────────────────────────────────────────────────────────────────┘

8. 夏時間(DST)

夏時間とは

┌─────────────────────────────────────────────────────────────────────┐
│ 夏時間(Daylight Saving Time) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 夏の間、時計を1時間進める制度 │
│ │
│ 米国の例(太平洋時間): │
│ ・冬: PST (Pacific Standard Time) = UTC-8 │
│ ・夏: PDT (Pacific Daylight Time) = UTC-7 │
│ │
│ 切り替えタイミング(米国): │
│ ・3月第2日曜 02:00 → 03:00 に進む(1時間消える) │
│ ・11月第1日曜 02:00 → 01:00 に戻る(1時間重複) │
│ │
│ 日本: │
│ ・夏時間なし(常に JST = UTC+9) │
│ │
│ 問題になるケース: │
│ ・海外サービスとの連携 │
│ ・グローバルユーザー向けサービス │
│ ・スケジュール機能(「毎日9時」が夏時間でずれる) │
│ │
└─────────────────────────────────────────────────────────────────────┘

夏時間の罠

┌─────────────────────────────────────────────────────────────────────┐
│ 夏時間による問題 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 問題1: 存在しない時刻 │
│ ───────────────────── │
│ 3月第2日曜の 02:00〜02:59 は存在しない │
│ 02:00 になった瞬間 03:00 に進むため │
│ │
│ 問題2: 重複する時刻 │
│ ───────────────────── │
│ 11月第1日曜の 01:00〜01:59 は2回存在する │
│ 02:00 になった瞬間 01:00 に戻るため │
│ → 「01:30」がどちらの01:30か区別できない │
│ │
│ 対策: │
│ ・内部では常にUTC(またはUnix時間)を使用 │
│ ・表示時のみローカルタイムゾーンに変換 │
│ ・タイムゾーンIDを使用(America/Los_Angeles) │
│ │
└─────────────────────────────────────────────────────────────────────┘

まとめ

┌─────────────────────────────────────────────────────────────────────┐
│ 覚えておくべきポイント │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Unix時間は「秒」(JWTのexp, iat, nbf) │
│ → ミリ秒と混同しない(桁数で判断: 10桁=秒, 13桁=ミリ秒) │
│ │
│ 2. サーバー内部は常にUTC │
│ → ローカル時刻(JST等)は表示時のみ使用 │
│ │
│ 3. クロックスキューに注意 │
│ → NTPで時刻同期、検証時に許容範囲を設定 │
│ │
│ 4. Javaでは Instant を使用 │
│ → LocalDateTime は避ける │
│ │
│ 5. データベースは BIGINT または TIMESTAMP WITH TIME ZONE │
│ → 2038年問題を回避 │
│ │
│ 6. 夏時間に注意(海外連携時) │
│ → タイムゾーンIDを使用 │
│ │
└─────────────────────────────────────────────────────────────────────┘

参考