27.

Java のセッション|HttpSession の使い方とセッション固定化対策

編集
この記事の要点
  • 取得は request.getSession()引数なしだと無ければ新規作成される
  • 読み書きは setAttribute() / getAttribute()戻り値は Object なのでキャストが要る
  • ログイン成功時に request.changeSessionId() を呼ぶ。セッション固定化攻撃を防ぐ
  • 破棄は invalidate()呼んだあとのセッションは触れない
  • 複数台に分散するならメモリではなく Redis や DB に置く(Spring Session)

セッションとは

HTTP はリクエストごとに独立しているため、そのままでは「誰からのアクセスか」が分かりません。サーバーがセッション ID を Cookie(JSESSIONID)で渡し、その ID に紐づく保管場所を用意する仕組みがセッションです。

1 回目のアクセス   ブラウザ → サーバー
                   サーバー: セッションを作り Set-Cookie: JSESSIONID=ABC123
2 回目以降        ブラウザ: Cookie: JSESSIONID=ABC123
                   サーバー: ABC123 の保管場所を取り出す

基本操作

@WebServlet("/mypage")
public class MyPageServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        HttpSession session = request.getSession();          // 無ければ作る
        // HttpSession s2 = request.getSession(false);        // 無ければ null

        // 保存
        session.setAttribute("userId", 42);
        session.setAttribute("user", new User(42, "田中"));

        // 取得(Object で返るのでキャストする)
        Integer userId = (Integer) session.getAttribute("userId");
        User user = (User) session.getAttribute("user");

        // 削除
        session.removeAttribute("userId");

        // 情報
        System.out.println(session.getId());
        System.out.println(session.isNew());
        System.out.println(session.getCreationTime());
        System.out.println(session.getLastAccessedTime());
    }
}

getSession(false) は「あれば返す、無ければ nullです。ログイン判定のように「作りたくない」場面ではこちらを使ってください。引数なしで呼ぶと、未ログインの訪問者にもセッションが作られ、メモリを消費します。

取り出しは null とキャストに注意

HttpSession session = request.getSession(false);

if (session == null) {
    response.sendRedirect("/login");
    return;
}

Object raw = session.getAttribute("userId");
if (raw == null) {
    response.sendRedirect("/login");
    return;
}

// 型が違うと ClassCastException
if (!(raw instanceof Integer userId)) {
    session.invalidate();
    response.sendRedirect("/login");
    return;
}

// ヘルパーを用意しておくと安全
private static <T> Optional<T> attr(HttpSession s, String key, Class<T> type) {
    if (s == null) return Optional.empty();
    Object v = s.getAttribute(key);
    return type.isInstance(v) ? Optional.of(type.cast(v)) : Optional.empty();
}

ログイン処理

protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws IOException {

    User user = authenticate(request.getParameter("id"),
                             request.getParameter("password"));
    if (user == null) {
        request.setAttribute("error", "IDまたはパスワードが違います");
        return;
    }

    // 1. セッション ID を作り直す(セッション固定化攻撃への対策)
    request.changeSessionId();

    // 2. 必要な情報だけ保存する
    HttpSession session = request.getSession();
    session.setAttribute("userId", user.getId());
    session.setMaxInactiveInterval(30 * 60);   // 30 分無操作で失効

    response.sendRedirect("/mypage");
}

changeSessionId() を呼ばないと、攻撃者があらかじめ用意した ID のままログインが成立します。その ID を知っている攻撃者は、そのままログイン済みの状態を使えてしまいます(セッション固定化)。Servlet 3.1 以降で使えます。

ログアウト

HttpSession session = request.getSession(false);
if (session != null) {
    session.invalidate();           // セッションを破棄する
}
// session.getAttribute("x");       // IllegalStateException: 破棄後は触れない

// Cookie も消したいとき
Cookie cookie = new Cookie("JSESSIONID", "");
cookie.setMaxAge(0);
cookie.setPath(request.getContextPath() + "/");
response.addCookie(cookie);

response.sendRedirect("/login");

有効期限

session.setMaxInactiveInterval(1800);      // 秒。この時間アクセスが無ければ失効
session.setMaxInactiveInterval(-1);        // 無期限(推奨しない)
System.out.println(session.getMaxInactiveInterval());
<!-- web.xml で既定値を決める(分単位) -->
<session-config>
  <session-timeout>30</session-timeout>
  <cookie-config>
    <http-only>true</http-only>
    <secure>true</secure>
    <same-site>Lax</same-site>
  </cookie-config>
</session-config>
Cookie 属性意味推奨
HttpOnlyJavaScript から読めなくする必ず有効(XSS でセッションを盗まれない)
SecureHTTPS のときだけ送る本番では有効
SameSite他サイト由来のリクエストで送るかLax。決済系は Strict

Spring MVC での書き方

@Controller
public class MyPageController {

    // HttpSession をそのまま受け取れる
    @GetMapping("/mypage")
    public String mypage(HttpSession session, Model model) {
        Object userId = session.getAttribute("userId");
        if (userId == null) return "redirect:/login";
        model.addAttribute("userId", userId);
        return "mypage";
    }

}

// セッションに載せるモデル属性を宣言する
@Controller
@SessionAttributes("cart")
public class CartController {

    @ModelAttribute("cart")
    public Cart cart() { return new Cart(); }

    @PostMapping("/cart/clear")
    public String clear(SessionStatus status) {
        status.setComplete();          // @SessionAttributes の分だけ破棄する
        return "redirect:/cart";
    }
}

実際の認証には Spring Security を使うのが標準です。セッション固定化対策やタイムアウト、同時ログイン数の制御が設定だけで有効になります。

何を入れて何を入れないか

入れてよい入れない
ユーザー ID・権限などの小さな値パスワード・カード番号
画面の入力途中の状態(ウィザード)検索結果の全件(数 MB になる)
CSRF トークンDB から取れる情報の丸ごとコピー
直列化できるオブジェクトConnectionInputStream などの資源

セッションは利用者ごとにサーバーのメモリを消費します。同時 1,000 人で 1 人 1MB を持てば 1GB です。大きなデータは ID だけ保存し、必要なときに取り直してください。

複数台構成での注意

// Spring Session + Redis を使うと、サーバーをまたいで共有できる
// build.gradle:
//   implementation 'org.springframework.session:spring-session-data-redis'
// application.properties:
//   spring.session.store-type=redis
//   spring.session.timeout=30m

既定のセッションはそのサーバーのメモリ上にあります。ロードバランサで振り分けると、別のサーバーに届いた時点でログイン状態が消えます。対処は次のいずれかです。

  • 外部ストアに置く(Redis / DB)— 台数を増やしても影響がなく、再起動でも消えない
  • 同じ利用者を同じサーバーへ送る(スティッキーセッション)— 簡単だが偏りやすく、再起動で失う
  • セッションを使わず JWT などトークンで持つ — 失効の扱いが別途必要になる

セッションに入れるクラスは直列化できるようにする

public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private int id;
    private String name;
    private transient Connection conn;   // 保存しないものは transient
}

Tomcat は再起動時にセッションをファイルへ書き出します(セッションの永続化)。Serializable でないオブジェクトが入っていると NotSerializableException が出て、セッションが失われます。

関連

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. 基本的なルール
  2. データ型
  3. 変数
  4. 定数
  5. 配列
  6. コレクション(List,Set,Queue)
  7. Map(連想配列)
  8. 演算子
  9. 条件分岐
  10. 繰り返し制御文
  11. クラス
  12. メソッド
  13. インスタンス化
  14. コンストラクタ
  15. staticキーワード
  16. オーバーロード
  17. 継承
  18. オーバーライド
  19. this
  20. super
  21. パッケージ
  22. アクセス修飾子
  23. 抽象クラス・メソッド
  24. インターフェース
  25. カプセル化
  26. データベース接続
  27. セッション
  28. ファイル入出力
  29. ラムダ式

最近更新/作成されたページ