Javaのログイン・ログアウトの仕組みとは?セッションで利用者を管理する方法

最終更新日

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

ログインは、画面を切り替えるだけの処理ではありません。本人確認に成功した結果をサーバーに保存し、その後のアクセスでも「誰が利用しているか」を確認できるようにします。ログアウトでは、そのログイン状態を使えなくします。

そのため、ログイン画面へ戻すリンクを作っただけでは、ログアウトしたことになりません。この記事では、JavaのServletとHttpSessionを使い、保存する・確認する・無効にするという3つの処理を、マイページの例でつなげます。

本記事は、Schoo「Java入門 中級」第8回「Webアプリケーションにおける状態管理」を学んだあとに理解を深める記事です。未受講の方にも、フォーム送信からログアウト後の再アクセスまで、順番に説明します。

ログイン成功・画面表示・ログアウトは、別々の処理

処理サーバー側で行うこと
ログインIDとパスワードなどで本人確認し、成功した利用者の情報をセッションへ保存する。
ログイン後の画面表示有効なセッションと、認証成功時に保存した利用者情報を確認する。
ログアウトセッションを無効にして、以前のログイン状態を使えなくする。

セッションはサーバー側の情報置き場です。Cookieは、その置き場所を識別するセッションIDをブラウザから送るために使います。基本の関係は、Cookieとセッションの違いとは?保存する場所と使い分けをわかりやすく解説で図解しています。

今回の例で使うURLとファイル

アクセス先担当するファイル・役割
GET /login.htmllogin.html:IDとパスワードを入力する。
POST /loginLoginServlet.java:本人確認とセッションへの保存。
GET /mypageMyPageServlet.java:ログイン状態を確認して、mypage.jspへ表示を渡す。
POST /logoutLogoutServlet.java:セッションを無効にする。
GET /login-error.htmllogin-error.html:ログイン失敗時の案内。

表のURLはアプリ内のパスです。たとえばアプリのパスが/learning-appなら、マイページは/learning-app/mypageです。Javaの3ファイルはexampleパッケージ、HTMLとJSPはWeb公開フォルダーの直下に置く構成です。JSPを直接リンク先にせず、Servletを経由させます。

コードはJakarta Servletを使うWebアプリの例です。通常のmainメソッドだけでは動かず、TomcatなどのServlet実行環境が必要です。ここでは環境構築ではなく、各ファイルの役割とリクエストの流れを扱います。Schoo専用のファイルや会員登録は前提にしていません。

本人確認は、ローカル学習用にID「1001」、パスワード「practice-only」の固定値で代用します。公開するアプリの認証実装ではありません。実際のパスワードをコードに書いたり、平文でDBへ保存したりする方法として使わないでください。

1.ログインフォームからIDとパスワードを送る

ファイル名:login.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
</head>
<body>
    <h1>ログイン</h1>
    <form action="login" method="post">
        <p><label>ID <input name="userId" required></label></p>
        <p><label>パスワード
            <input type="password" name="password" required>
        </label></p>
        <button type="submit">ログイン</button>
    </form>
</body>
</html>

action="login"で/loginへ、method="post"でPOSTリクエストを送ります。入力欄のnameと、ServletがgetParameterで読む名前を合わせます。IDを数字で入力しても、getParameterでは文字列として受け取ります。

POSTやpassword型の入力欄を使うだけで通信が暗号化されるわけではありません。公開環境ではHTTPSが必要です。

2.本人確認に成功した場合だけ、セッションへ保存する

ファイル名:LoginServlet.java

package example;

import java.io.IOException;
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("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("UTF-8");
        String userId = request.getParameter("userId");
        String password = request.getParameter("password");

        // ローカル学習用の固定値。公開するアプリでは使いません。
        boolean authenticated = "1001".equals(userId)
                && "practice-only".equals(password);
        if (!authenticated) {
            HttpSession previous = request.getSession(false);
            if (previous != null) {
                previous.invalidate();
            }
            response.sendRedirect(
                    request.getContextPath() + "/login-error.html");
            return;
        }

        HttpSession session = request.getSession();
        request.changeSessionId();
        session.setAttribute("loginUserId", Integer.valueOf(1001));
        session.setMaxInactiveInterval(30 * 60);
        response.sendRedirect(request.getContextPath() + "/mypage");
    }
}

authenticatedは「本人確認に成功したか」を表すbooleanの変数です。この例では固定のIDとパスワードを比較しています。入力されたIDをそのままログイン済みとして保存しているのではありません。成功を確認した後で、確認済みの利用者ID1001を保存します。

  1. 本人確認に失敗した場合:以前のセッションがあれば無効にし、失敗画面へ移動して処理を終える。今回は、失敗後に以前の利用者としてログインしたままにしない方針。
  2. getSession():セッションを取得する。なければ新しく作る。
  3. changeSessionId():本人確認に成功したタイミングでセッションIDを変更する。ログイン前のIDをそのまま使い続けないための処理。
  4. setAttribute:loginUserIdという名前で、Integer型の1001を保存する。パスワードは保存しない。
  5. setMaxInactiveInterval:セッションへのアクセスが30分間なければ、無効になるようにする。
  6. sendRedirect:ブラウザに/mypageへアクセスし直してもらう。

セッションへの保存が先、マイページへの移動が後です。リダイレクト後は別のリクエストですが、Cookieで同じセッションを識別できるため、保存した利用者IDを取り出せます。

失敗時に表示するページには、入力されたパスワードなどを表示しません。

ファイル名:login-error.html

<!DOCTYPE html>
<html lang="ja">
<head><meta charset="UTF-8"><title>ログインできませんでした</title></head>
<body>
    <p>IDまたはパスワードが違います。</p>
    <a href="login.html">ログイン画面へ戻る</a>
</body>
</html>

3.画面を開くたびにログイン状態を確認する

「ログインに成功したときだけマイページへのリンクを表示する」では不十分です。利用者はアドレス欄にURLを直接入力できます。マイページを返す側で、毎回ログイン状態を確認します。

ファイル名:MyPageServlet.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("/mypage")
public class MyPageServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        response.setHeader("Cache-Control", "no-store");
        HttpSession session = request.getSession(false);
        if (session == null) {
            response.sendRedirect(request.getContextPath() + "/login.html");
            return;
        }

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

        request.setAttribute("displayUserId", loginUserId);
        request.getRequestDispatcher("/mypage.jsp")
                .forward(request, response);
    }
}
確認するもの確認する理由
session == null有効なセッションがない。初回アクセスや、無効化・期限切れの後など。
loginUserId == nullセッションはあっても、今回のアプリが認証成功時に保存する情報がない。セッションの存在だけではログイン済みとはいえない。
両方の確認を通過認証済みの利用者IDを今回のrequestへ渡し、JSPで表示する。

ここでは既存の状態を調べるので、getSession(false)を使います。getSession()にすると、セッションがない場合に空のものを新しく作ってしまいます。どちらを呼んでも、それだけで本人確認が行われるわけではありません。

sendRedirectは、Javaメソッドの実行を自動で終了しません。ログインしていない場合の分岐ではreturnも書き、後ろの表示処理へ進まないようにします。

Cache-Controlのno-storeは、このレスポンスをキャッシュに保存しないよう求める指定です。ログアウト後に古い画面が残ることへの対策の一つですが、ログイン判定の代わりにはなりません。

4.JSPでは、Servletから渡された表示用データを使う

ファイル名:mypage.jsp

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" session="false" %>
<%
response.setHeader("Cache-Control", "no-store");
Integer displayUserId = (Integer) request.getAttribute("displayUserId");
if (displayUserId == null) {
    response.sendError(404);
    return;
}
%>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>マイページ</title>
</head>
<body>
    <h1>マイページ</h1>
    <p>ログイン中の利用者ID:<%= displayUserId %></p>
    <a href="mypage">もう一度マイページを開く</a>
    <form action="logout" method="post">
        <button type="submit">ログアウト</button>
    </form>
</body>
</html>

セッションに保存したloginUserIdを、ServletがrequestのdisplayUserIdへ渡しています。ログイン状態の保存先はセッション、今回のJSPの表示用データはrequestと役割を分けています。表示するのはサーバーで確認した数値のIDです。

先頭のsession=”false”は、JSPが自動でセッションを作らないようにする指定です。また、mypage.jspを直接開いた場合はdisplayUserIdがないので、404を返して表示しません。画面への入口は/mypageにそろえます。

「もう一度マイページを開く」も/mypageへのGETです。クリックするたびに新しいrequestになりますが、MyPageServletがセッションの情報を取り出して表示用データを作り直します。

5.ログアウトではセッションを無効にする

ファイル名:LogoutServlet.java

package example;

import java.io.IOException;
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("/logout")
public class LogoutServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        response.sendRedirect(request.getContextPath() + "/login.html");
    }
}

JSP内のログアウトフォームは、/logoutへPOSTを送ります。LogoutServletは既存のセッションがある場合だけinvalidate()を呼び、最後にログイン画面へリダイレクトします。セッションが既に切れていても、新しく作り直さずログイン画面へ戻れます。

画面を戻す処理と、ログイン状態を消す処理は別です。sendRedirectだけではログアウトになりません。また、invalidate後のsessionから値を取り出す処理は続けません。必要な情報があれば、無効化する前に扱います。

ログイン画面へ戻るだけでは、なぜログアウトにならない?

違いは、ログイン画面が見えているかではなく、サーバー側で以前のログイン状態を使えるかです。ログアウト後に/mypageへアクセスすると、有効なログイン情報がないため、MyPageServletが表示を止めます。

ブラウザの「戻る」で過去の画面が見えることと、サーバーが新しいアクセスを受け付けることも別です。履歴の見え方だけで確認を終えず、再読み込みやURLの直接入力でも保護された画面に入れないことを確認します。既にブラウザへ渡した内容を、セッションの無効化で取り消せるわけではありません。

どの操作を確認すれば、ログイン処理を確かめられる?

操作期待する動き
ログインせずに/mypageを直接開くログイン画面へ移動する。
誤ったID・パスワードで送信する失敗画面になる。その後の/mypageも表示できない。
正しい学習用の値でログインするマイページに利用者ID1001が表示される。
ログイン後にlogin.htmlを開き、/mypageを再度開くログアウトしていないためマイページを表示できる。
ログアウトボタンを押し、/mypageを直接開くログイン画面へ戻される。
同じCookieを共有しない別ブラウザで/mypageを開くそのブラウザでログインしていなければ、ログイン画面へ移動する。
セッションが期限切れになった後に/mypageを開く再ログインが必要になる。
mypage.jspを直接開く表示用request属性がないため404になる。

同じブラウザの別タブではCookieを共有することが一般的なので、「別タブなら別のログイン状態」とは考えません。この例で同じセッションを使っていれば、片方でログアウトした後、もう片方の新しいリクエストでもログイン状態を使えなくなります。

ログインできたことと、すべての操作を許可することは別

今回のコードが確認するのは、認証に成功した利用者情報があるかどうかです。タスク編集や管理者画面では、さらに「その利用者が、このデータを編集してよいか」を確認します。本人確認が認証、操作を許可する判断が認可です。

また、この例を公開用のログイン機能にするには、固定値の比較を置き換えるだけでは足りません。HTTPS、パスワードの適切なハッシュ化と照合、ログイン試行回数の制限、セッション用Cookieの設定、ログイン・ログアウトなどのCSRF対策が必要です。POSTにしただけではCSRFを防げません。この記事のコードは、状態管理をローカルで学ぶための例です。

確認問題

  1. ログアウトボタンの代わりに、login.htmlへのリンクを置きました。ログイン画面は表示されますが、本当にログアウトしたといえますか。足りない処理と、その確認方法を答えてください。
  2. 有効なセッションはありますが、loginUserIdがありません。MyPageServletはどう動きますか。また、ここでgetSession(false)を使う理由を答えてください。
  3. 未ログイン時のsendRedirectの直後からreturnを削除すると、何が問題になりますか。今回のMyPageServletの処理順に沿って説明してください。
  4. ログイン後にログアウトし、/mypageを直接開く場合と、mypage.jspを直接開く場合はそれぞれどうなりますか。どこで表示を止めるかも答えてください。

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

まとめ

  • ログイン成功時だけ、本人確認済みの利用者情報をセッションへ保存する。
  • 保護された画面は、リンクの有無に頼らずサーバー側で毎回ログイン状態を確認する。
  • セッションがあることと、ログイン済みであることは同じではない。
  • ログアウトはセッションを無効にする処理。画面を移動するだけではない。
  • 成功時の表示だけでなく、失敗・直接アクセス・ログアウト後・期限切れも確認する。

関連記事

シェアする