Javaのrequest.getSession(false); がなぜかnullになってしまう。

JavaでServletプログラムを作成していてセッションを使っていたのですが、なぜか request.getSession(false); がnullになってしまう事象にあいました。

初回アクセス時
HttpSession session = request.getSession( true );

二回目以降
HttpSession session = request.getSession( false );

session.getAttribute("xxx"); でセッション属性を取得したいのだけれど、sessionがnullのため取得できない。。アレコレ悩みました。

今回のウェブサーバーは Nginx + Tomcat の構成(同一サーバーに相乗り)なのですが、Nginxの proxy_pass ディレクティブでTomcatへの転送先URLを指定しています。

proxy_pass http://localhost:8080/test/;

末尾の /test/ はウェブアプリケーションのコンテキストパスです。実際にServletにアクセスするURLは、以下のようにしています。

http://tomcat/sample/fwservlet

URL中の tomcat がドメインで fwservlet はServlet名です。Nginxを介さずに直接Tomcatにアクセスする場合は、このようになります(localhost はIPアドレスに読み替えてください)。

http://localhost:8080/test/sample/fwservlet

ここでNginxに向けてリクエストをするとダメなのだけれど、直接Tomcatに向けてリクエストをするとうまくいくことに気づきました。原因はNginxにあるのか?と思いつつも、Firefoxのデバッグモードでクッキーを見てみました。リクエストはNginxに向けて投げています。

これを見るとクッキーには JSESSIONID が設定されていることが確認できました。なので初回アクセス時に新規セッションは問題なく作成されているようです。そうすると、なぜ二回目以降のアクセスでこのセッションが取得できないのか?になるのですが、クッキーのPath属性を見ると /test となっています。

Path属性は「送信するURLにここで指定するパスが含まれている場合のみクッキーが送信される」ということになるのですが、Nginx向けのリクエストURLには /test はないです。直接Tomcatに向ける場合は含まれています。つまり、Nginxに向けて投げた場合、クッキーが送信されていないから request.getSession( false ); で JSESSIONID が取得できず、nullになってしまっていることがわかりました。

この解決策ですが、/test/ のコンテキストパスを変えるのはいまさらだとインパクトが大きい。なので web.xml のサーブレットマッピングをいじることで対応しました。リクエストURLに強引に /test のパスを含めるようなやり方です。

こんな感じです。


<servlet>
    <servlet-name>fwservlet</servlet-name>
    <servlet-class>sample.fwservlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>fwservlet</servlet-name>
    <url-pattern>/test/fwservlet</url-pattern>
</servlet-mapping>

JSPも同様に対応します。


<servlet>
    <servlet-name>nolist</servlet-name>
    <jsp-file>/sample/nolist.jsp</jsp-file>
</servlet>
<servlet-mapping>
    <servlet-name>nolist</servlet-name>
    <url-pattern>/test/nolist.jsp</url-pattern>
</servlet-mapping>

この設定をするとNginxへのリクエストURLは以下のように変わります。

http://tomcat/test/fwservlet

これでServletを動かすと request.getSession( false ); でセッションを取得することができるようになりました。

Nginxのバーチャルサーバーを設定した時の直IP接続を拒否する方法

Nginxにバーチャルサーバーの設定をすると複数のドメインのサイトを1台のウェブサーバーで運用することができます。ただDNSでの名前解決をせずIPを直指定してウェブサーバーにアクセスすることもできてしまうので、これを防ぐ方法です。ベストな方法ではないのかもしれませんので、参考程度に見てください。

ドメインが example01.com と example02.com である2つのサイトを運用しているサーバー(IPアドレスは 210.100.1.1 )の例です。

この場合のNginxのconfファイルは以下のように設定をします。


server {
    listen       80;
    server_name  www.dummy.com;
     :
    location / {
        return 444;
    }
}

server {
    listen 80;
    server_name www.example01.com;
    return 301 https://$host$request_uri;
}

server {
    listen 80;
    server_name www.example02.com;
    return 301 https://$host$request_uri;
}

server {
    listen       443 ssl;
    server_name  www.dummy.com;

    ssl_certificate ・・
    ssl_certificate_key ・・

    location / {
        return 444;
    }
     :
}

server {
    listen       443 ssl;
    server_name  www.example01.com;

    ssl_certificate ・・
    ssl_certificate_key ・・
     :
}

server {
    listen       443 ssl;
    server_name  www.example02.com;

    ssl_certificate ・・
    ssl_certificate_key ・・
     :
}

example01.com と example02.com のドメイン以外にダミーのドメイン www.dummy.com の設定をします。ダミードメインは example01.com と example02.com より先に記載しておきます。これはリクエストがどのバーチャルサーバーにも合致しない際にダミードメインがデフォルトサーバーとして振る舞うためです。デフォルトサーバーはlistenディレクティブにdefault_serverパラメータで明示的に指定できるのですが、今回は記載順で対応しました。

ダミードメインのlocationディレクティブには return 444 を設定します。このリターンコードはNginx独自のものでウェブブラウザとの切断になります。

HTTPSのダミードメインの部分ですが証明書の記載が必要です。これを記載しないとコンフィグチェック nginx -t でエラーになってしまいます。ですが証明書などはないので ssl_certificate と ssl_certificate_key には自己署名証明書(オレオレ証明書)を設定しました。

この設定で実際に直IPを指定してFirefoxでウェブサイトにアクセスしてみると「警告:潜在的なセキュリティリスクあり」と表示されます。

①詳細へ進む.. を押下すると「自己署名をしているためこの証明書は信頼されません」と表示されます。さらに、②危険性を承知の上で使用 を押下すると該当のサイトのページに遷移するのですが、locationディレクティブには return 444 を設定しているのでサーバーとの通信は切断されます。そのため「安全な接続ができませんでした」の画面になります。

なお先ほどの警告の画面で ③証明書を確認 を押下すると証明書の確認ができます。自己署名証明書なのでコモンネームが www.dummy.com のものとなっています。

Firefoxの場合ですが、サーバー証明書のエラー例外は設定から「プライバシーとセキュリティ」→「証明書を表示..」で確認できます。

Let’s Encryptのサーバー証明書を使っている場合の話となりますが、今回のNginxのconfファイルの設定だと certbot renew コマンドが失敗します。バーチャルサーバーの設定の影響で失敗するのかはわかりません。単一ドメインで certbot renew を実行すれば成功するのでやり方を書いておきます。

/etc/letsencrypt/renewal 配下には各ドメインのconfファイルがあるかと思います。このうち証明書を更新するほうはそのままで、更新しないほうのconfファイルをリネームします。この作業はroot権限が必要です。example01.com を更新する場合です。

変更前
example01.com.conf
example02.com.conf

変更後
example01.com.conf
example02.com.conf.bk

Nginxのconfファイルですがバーチャルサーバーの設定を全部なくします。example01.com を更新する場合です。


server {
    listen 80;
    server_name www.example01.com;
    return 301 https://$host$request_uri;
}

server {
    listen       443 ssl;
    server_name  www.example01.com;

    ssl_certificate ・・
    ssl_certificate_key ・・
     :
}

Nginxでconfファイルのリロードを行ってから certbot renew をすれば、うまくいきます。証明書の更新が終わったら、もとに戻しておきましょう。