Javaのセッションとは?画面を移動してもログイン状態が続く仕組み

最終更新日

Schoo Java入門 中級 第8回 Webアプリケーションにおける状態管理

ログインしたあとに別の画面へ移動しても、毎回パスワードを入力する必要はありません。サーバーが「ログイン済みの利用者は誰か」を保存し、次のリクエストでも取り出せるようにしているためです。JavaのWebアプリケーションでは、そのためにセッションを使えます。

ただし、名前やパスワードを毎回ブラウザから送り直しているわけではありません。この記事では、ブラウザが送るセッションIDと、サーバーに保存した利用者情報を分けて、画面を移動したときの動きを確認します。

本記事は、Schoo「Java入門 中級」第8回「Webアプリケーションにおける状態管理」に対応しています。未受講の方も読めるよう、ログイン成功後に保存する値と、次の画面で取り出すコードを記事内で示します。

なぜ画面を移動すると、ログイン情報の保存が必要になるのか

たとえばタスク管理アプリで、ログイン後に「タスク一覧」、そこから「タスク詳細」を開くとします。リンクをクリックするたびに、ブラウザは新しいリクエストを送ります。

  1. ログインする:IDとパスワードを送信し、サーバーで本人確認する。
  2. タスク一覧を開く:一覧を取得する新しいリクエストを送る。
  3. タスク詳細を開く:選んだタスクを取得する、さらに別のリクエストを送る。

HTTPでは、それぞれのリクエストだけで「この人はさっきログインに成功した人だ」と自動的に判断できるわけではありません。この性質をステートレスといいます。Servletのメソッド内で作った変数も、次のリクエストへ自動で引き継がれるわけではありません。

そこで、本人確認に成功した結果をサーバーに保存し、後のリクエストと結び付けます。「HTTPはステートレスだから、Webアプリは何も覚えられない」という意味ではありません。セッションなどの仕組みを加えることで、複数のリクエストにまたがって状態を扱えます。

セッションとは、複数のリクエストで使うデータの保存先

Servletでは、セッションのデータをHttpSessionというオブジェクトを通して保存・取得します。今回保存するのは、ログイン済みの利用者IDと名前です。パスワードは保存しません。

保存する名前値意味
loginUserId1001認証に成功した利用者のID
loginUserName学習ユーザーその利用者の名前

この保存先はサーバー側にあります。ブラウザには、保存先を識別するためのセッションIDを渡します。Cookieを使う構成では、ブラウザはそのIDを保存して、対象のサイトへの次のリクエストに付けます。

イラスト:次のリクエストでも同じセッションを使う

ログイン成功後、サーバーのセッションA1に利用者ID1001と学習ユーザーという名前を保存する。ブラウザのCookieにはJSESSIONID=A1が入る。別の画面を開くとブラウザがA1を送り、サーバーが同じセッションから名前を取り出し、学習ユーザーさんと表示するHTMLを返す。
A1は説明用の短いセッションIDです。実際にはサーバー側の仕組みが生成した、より長い値を使います。

上段はログインに成功した後、下段は別の画面を開くときです。矢印は、次のように読んでください。

  • 上段の左向きの矢印:サーバーがレスポンスでセッションIDを渡し、ブラウザがCookieに保存する。
  • 下段の青い矢印:ブラウザが次のリクエストにCookieのセッションIDを付ける。
  • 下段の緑の矢印:サーバーが同じセッションから利用者情報を取り出し、その情報を使って作ったHTMLを返す。

Cookieの標準的な名前がJSESSIONIDです。設定によって名前が変わる場合もあります。図では、サーバー内のServletやJSPを省略しています。実際には、Servletが利用者情報を取り出し、JSPなどで表示用のHTMLを作ります。

Cookieの中に、上の表の利用者情報が丸ごと入っているわけではありません。この例でセッション用のCookieに入るのはA1というIDです。名前を画面に表示するため、サーバーが作ったHTMLには「学習ユーザー」という文字が含まれますが、それとCookieへの保存は別です。

セッションIDとユーザーIDは別のもの

値何を区別するか
セッションID:A1サーバー側の、どのセッションを使うか
ユーザーID:1001アプリに登録された、どの利用者か

同じ利用者が別のブラウザでログインすれば、ユーザーIDは同じ1001でも、別のセッションになることがあります。一方、同じブラウザの通常のタブではCookieを共有するため、別タブでも同じログイン状態になるのが一般的です。「1タブに1セッション」「1人に必ず1セッション」ではありません。

セッションIDは、単なる画面表示用の番号ではありません。盗まれるとログイン状態を悪用されるおそれがあるため、公開したり、画面やログへ不用意に出したりしません。図のA1を、そのまま実際のIDとして設定するものでもありません。

1.ログイン成功後に、利用者情報を保存する

ここからは、図の利用者ID1001と名前「学習ユーザー」を使います。次のコードは、ログイン用ServletのdoPostで、IDとパスワードの確認に成功した後の部分です。認証処理全体を示したコードではありません。

値の対応を追えるよう、取得済みの利用者情報を1001と「学習ユーザー」で表しています。実際には、認証処理で確認できた利用者の情報を使います。入力されたIDを確認せず保存するだけでは、ログイン機能になりません。

// 認証に成功した利用者の情報
int loginUserId = 1001;
String loginUserName = "学習ユーザー";

HttpSession session = request.getSession();
request.changeSessionId();
session.setAttribute("loginUserId", loginUserId);
session.setAttribute("loginUserName", loginUserName);

response.sendRedirect(
        request.getContextPath() + "/session-page");

HttpSessionを使うクラスでは、import jakarta.servlet.http.HttpSession;を追加します。各行の役割を整理しましょう。

コード役割
request.getSession()このリクエストに対応するセッションを取得する。なければ新しく作る。
request.changeSessionId()ログイン成功に合わせて、セッションIDを変更する。
session.setAttribute("loginUserId", loginUserId)loginUserIdという名前で、利用者IDをセッションへ保存する。
session.setAttribute("loginUserName", loginUserName)loginUserNameという名前で、名前をセッションへ保存する。
response.sendRedirect(...)ブラウザに、/session-pageへ新しいリクエストを送らせる。

ログイン前から存在していたセッションIDを、そのままログイン後にも使い続けないためにchangeSessionIdを呼んでいます。保存する値を消す処理ではありません。図のA1は、この変更後のIDを表しています。

setAttributeを呼んでも、それだけで画面に文字が出るわけではありません。ここで行っているのは保存です。次の画面で取り出し、HTMLの表示へつなげる処理は別に書きます。

2.次のリクエストで、保存した利用者情報を取り出す

/session-pageへのGETを受け取るServletです。ブラウザが送ったセッションIDと保存先を対応付ける部分は、TomcatなどのServletコンテナが担当します。通常、アプリ側でCookieからIDを読み、保存先を探すコードを毎回書く必要はありません。

SessionPageServlet.java

package example;

import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@WebServlet("/session-page")
public class SessionPageServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        HttpSession session = request.getSession(false);
        if (session == null) {
            response.sendRedirect(request.getContextPath() + "/login.html");
            return;
        }

        Integer loginUserId =
                (Integer) session.getAttribute("loginUserId");
        String loginUserName =
                (String) session.getAttribute("loginUserName");
        if (loginUserId == null || loginUserName == null) {
            response.sendRedirect(request.getContextPath() + "/login.html");
            return;
        }

        request.setAttribute("displayName", loginUserName);
        request.getRequestDispatcher("/session-page.jsp")
                .forward(request, response);
    }
}

最初に使うのはgetSession(false)です。保存済みのセッションを確認したいので、ここでは新しく作りません。有効なセッションがなければnullになり、ログイン画面へ戻します。returnは、その後の処理へ進まないために必要です。

続いて、保存時と同じ名前をgetAttributeに指定します。loginUserIdには1001、loginUserNameには「学習ユーザー」が戻ります。保存した名前が違う、またはまだ保存していない場合はnullです。

getAttributeの戻り値はObject型なので、この例ではIntegerとStringへキャストします。intの1001は保存時にIntegerへ自動変換されます。取り出すときもIntegerで受けることで、値がないnullを確認できます。最初からintで受けると、nullだった場合にエラーになるためです。

セッションがあるだけでは、ログイン済みではない

セッションは、ログイン以外にも、入力途中の情報や買い物かごなどに使えます。そのため、セッションが存在することと、認証済みであることは同じではありません。

状態今回の判断
セッションがないログイン画面へ戻す。
セッションはあるがloginUserIdなどがないログイン画面へ戻す。
認証成功時に保存したloginUserIdとloginUserNameがある画面の表示へ進む。

今回の例では、認証成功時だけ利用者情報を保存するというルールと、各画面でその情報を確認する処理を組み合わせています。「Cookieがあるからログイン済み」「getSessionがnullでないから本人確認済み」とは判断しません。

なお、ログイン済みでも、すべてのデータを閲覧・編集してよいとは限りません。他の利用者のタスクへアクセスできないようにするなど、対象データの権限確認は別に必要です。

3.取り出した名前を、JSPで表示する

Servletの最後では、セッションから読んだ名前を今回の表示用データとしてrequestのdisplayName属性へ入れ、JSPへforwardしています。JSPをアプリ直下に置いた例です。

session-page.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false" %>
<%
String displayName = (String) request.getAttribute("displayName");
if (displayName == null) {
    response.sendError(404);
    return;
}
%>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ログイン後の画面</title>
</head>
<body>
    <p><%= displayName %>さん</p>
    <a href="session-page">もう一度、画面を開く</a>
</body>
</html>

この例では、記事内で固定した「学習ユーザー」を表示するので、画面には「学習ユーザーさん」と出ます。利用者が自由に登録した名前などを表示する場合は、出力時のHTMLエスケープが必要です。<%= ... %>自体に自動エスケープの機能はありません。

先頭のsession="false"は、このJSP自身ではセッションを自動取得・作成しない設定です。ログイン確認はServletで行い、JSPはrequestから表示用の値を受け取ります。displayNameがない直接アクセスでは404で終了します。

「もう一度、画面を開く」を押すと、同じServletへ新しいGETが送られます。前のrequestのdisplayNameは残りません。それでもセッションのloginUserNameは取り出せるので、Servletが新しいrequestへ改めてdisplayNameを設定し、同じ名前を表示できます。

requestとsessionは、保存する期間が違う

保存先今回保存するもの次のリクエスト
requestJSPに渡すdisplayName前の値は引き継がない。Servletで改めて設定する。
sessionログイン中の利用者IDと名前同じ有効なセッションに対応付けば、取り出せる。

リクエストパラメータとも区別します。request.getParameter("id")は、フォームなどでブラウザから送られた値を読む処理です。session.getAttribute("loginUserId")は、サーバーがセッションへ保存した値を読む処理です。名前が似ていても、データの出どころが違います。

一時的なエラーメッセージなど、今回の画面でしか使わない値まで何でもsessionへ入れると、別の画面や後の操作に古い値が残ることがあります。長く使うから便利、ではなく、どのリクエストまで必要かで選びます。

ログイン状態は、いつまでも残るわけではない

セッションには有効期限があります。設定された時間、アクセスがなければ無効になり、次のリクエストでは以前の利用者情報を使えなくなります。ログアウトでも、セッションを無効にして保存した情報を使えなくする方法が取れます。

session.invalidate();

この1行は、有効なsessionを取得したうえで、ログアウト処理から呼ぶ例です。無効化後のsessionでgetAttributeなどを続けて呼び出すものではありません。次の画面でgetSession(false)がnullになったら、ログイン画面へ案内します。

セッションは、DBへの永続保存の代わりではありません。また、ブラウザを閉じたときのCookieの扱いは復元機能などにも左右されるため、「ブラウザを閉じれば必ずログアウトする」とは考えません。ログアウト処理とサーバー側の有効期限を用意します。

公開するアプリでは、HTTPSや、セッション用CookieのSecure・HttpOnly・SameSiteなどの設定も確認します。セッションIDを安全にやり取りする対策は、保存・取得のコードとは別に必要です。この記事はCookieが利用できる構成で、ログイン成功後の情報が引き継がれる流れに絞っています。

確認問題

  1. 図のA1と1001は、それぞれ何のIDですか。今回の例でCookieに保存するものと、サーバーのセッションに保存するものを分けて答えてください。
  2. ログイン後、もう一度/session-pageへアクセスしました。前のrequestに設定したdisplayNameは残っていません。それでも同じ名前を表示できるのはなぜですか。処理の順番を説明してください。
  3. ログインが必要な画面で、getSession()ではなくgetSession(false)を使う理由を答えてください。また、セッションはあるのにloginUserIdがnullだった場合、この記事のServletはどう動きますか。
  4. loginUserNameという名前で文字列を保存しました。String型で取り出す1行を書いてください。代わりにuserNameという名前で取り出した場合はどうなりますか。ただしuserNameという属性は保存していないものとします。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • セッションは、複数のリクエストで使う情報をサーバー側に保存する仕組み。
  • ブラウザがCookieのセッションIDを送り、サーバーが対応する保存先を使う。
  • ログイン成功後に利用者情報を保存し、次のリクエストで同じ名前で取り出す。
  • セッションの存在だけでなく、認証成功時に保存した情報があるかを確認する。
  • requestは今回の表示用、sessionは複数のリクエストにまたがる情報など、必要な期間で使い分ける。

関連記事

シェアする