時刻とタイムゾーン
所要時間: 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時間を保存) │
│ ・古いファイルシステム │
│ ・レガシーアプリケーション │
│ │
└─────────────────────────────────────────────────────────────────────┘