画像も出ない、ログインも保たない。原因は一つだった話

画像も出ない、ログインも保たない。原因は一つだった話

壊れた画像アイコンが並んでいた。

しろまえ殿は、アプリの実機テストでその画面を見た。

アップロードした写真が、一覧に出てこない。

サーバのログを見ると、アップロード自体は成功していた。

おかしいのは、その後だ。

「取りに行っていない」ことを、ログで立証する

しろまえ殿はクルーに、憶測ではなく証拠で切り分けるよう指示した。

サーバのアクセスログを解析させたところ、興味深い事実が出てきた。

アップロード用のリクエストは、200(成功)で記録されている。ところが、画像を取得するためのリクエストが、1件も記録されていなかった。

つまり、アプリは画像を「取りに行くことを、そもそも試みていなかった」。

原因は、サーバが返すURL(サイト上の場所を示す文字列)の形にあった。

サーバは /uploads/ユーザーID/ファイル名 という、先頭にドメイン(サイトの住所部分)の付かない相対パス(自分の今いる場所を基準にした簡略な書き方)を返していた。

Webブラウザで動くバージョンは、これで問題なく表示できていた。裏側で、通信を中継する仕組みが吸収してくれていたからだ。

ところが、アプリのWebView(アプリの中でWebページを表示する部品)は、自分自身の内部アドレスを基準にこの相対パスを解決しようとする。その結果、本物のサーバには一生たどり着けない。

一つの思い込みが、姿を変えて2度出てきた

しろまえ殿は、この画像バグを直させながら、根っこにある構造を確認させた。

アプリのWebViewは、サーバとは別の場所にいる。

この一点さえ押さえれば、実は別の不具合とも同じ根っこだと分かった。

もう一つの不具合は、ログイン状態が保たれないというものだった。

ログイン後に発行されるセッションCookie(サーバがブラウザに預ける、ログイン状態の合言葉)には、SameSite=Lax(別の場所からの通信にはCookieを付けて送らない、という制限)がついていた。

WebViewとサーバは別の場所にいるので、ログイン後の通信からCookieが抜け落ち、保護されたAPIが軒並み401(未認証という応答)になっていた。

「サーバ側の設定を許可すれば直るのでは」という声もあった。

しろまえ殿は、そこに乗らなかった。

CookieのSameSite制限は、通信の許可設定であるCORS(異なる場所同士の通信を許可する仕組み)とは別の層の話だ。CORSをいくら緩めても、Cookie側の制限は解けない。

無理に押し通そうとすると、Cookieの制限そのものを緩める必要が出てくる。それは、せっかく積み上げたセキュリティの土台を後退させることになる。

たとえば、玄関の鍵を開けても、金庫の鍵は別に必要なようなものだ。玄関だけ開けて「入れないのはおかしい」と押し込んでも、金庫は開かない。

ブラウザの前提を、まるごと迂回する

しろまえ殿が選んだ解法は、通信の仕組みそのものを、アプリのネイティブ機能に肩代わりさせることだった。

Capacitor(Webの技術でネイティブアプリを作る枠組み)には、通信をOS標準のネットワーク機能へ橋渡しする設定と、Cookieをネイティブ側で保持する設定がある。

これを有効にすると、通信はブラウザのCORSやSameSiteの制約を受けなくなる。Cookieは、ログイン後もアプリ内部でずっと保持され、次のリクエストへ自動で付いていく。

サーバ側は、一切手を加える必要が無かった。

画像のほうは、URLを組み立てる箇所に、実行環境に応じて先頭を補うヘルパーを差し込んだ。Web版では何も足さず、アプリ版だけ絶対パスに変換する。Web版の挙動を壊さないための工夫だった。

気をつけたいこと

  • アプリのWebViewは、サーバと同じ場所にいるとは限らない。相対パスや、Cookieの制限は、まずここを疑う。
  • <img><video>のようなタグは、JavaScriptの通信処理を横取りする仕組みでは救えない。読み込みの経路そのものが別だからだ。
  • CookieのSameSite制限は、CORS設定では解決しない。層が違う問題だと理解してから対処法を選ぶ。
  • 通信の許可を無理に緩めるより、ネイティブの通信機能に任せる方が、安全な土台を保ったまま解決できることがある。

一つの思い込みに気づけば、別々に見えていた不具合が、実は同じ根っこだったと分かることがある。

しろまえ殿が「証拠で切り分けろ」と最初に言ったことが、遠回りに見えて一番の近道だった。