0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JDBC接続でのスーパーユーザー問題と、認証フィルターの実装まとめ

0
Posted at

Java Web開発・データベース連携・RLS学習記録

概要

Java Servlet/JSPを用いたWebアプリケーションの基礎から、JDBCによるPostgreSQLとの連携、Filterを使った認証機能、そしてRLS(行レベルセキュリティ)による高度なデータアクセス制御までを一貫して構築した。

Step 1: Webアプリの基礎と画面表示 (Servlet & JSP)

  • Servletの役割: ブラウザからのリクエストを受け取り、Javaの処理(ロジック)を実行する裏方のコントローラー。
  • JSPの役割: HTMLをベースに、画面の見た目を作るファイル。
  • スコープ (リクエストスコープ): Servletで処理したデータをJSPに渡すための一時的な保管庫。request.setAttribute() を使用。
  • フォワード: ServletからJSPへ、サーバー内部で処理をバケツリレーのように引き継ぐ仕組み。

Step 2: 現場の標準記法 (JSTL & EL式)

  • スクリプトレットの排除: JSP内に <% ... %> で直接Javaコードを書くのは保守性が下がるためNG。
  • EL式 (${...}): スコープに保存されたデータを、Javaコードを書かずにシンプルに画面に出力する仕組み。
  • JSTL (<c:forEach> など): 繰り返し(for文)や条件分岐(if文)をHTMLタグのような形式で書けるようにする標準ライブラリ。WEB-INF/lib.jar ファイルを配置して使用する。

Step 3: データベース構築とCUI操作 (PostgreSQL)

  • psqlコマンド: PostgreSQLを黒い画面(CUI)から操作する強力なツール。環境変数(Path)を通すことで、どこからでも呼び出せるようにした。

  • DBとテーブルの作成:

    CREATE DATABASE sample_db;
    \c sample_db
    CREATE TABLE items (id SERIAL PRIMARY KEY, item_name VARCHAR(50) NOT NULL);
    

Step 4: Javaとデータベースの連携 (JDBC)

  • JDBCドライバ: JavaとPostgreSQLを通訳するためのライブラリ(JARファイル)。
  • 接続の流れ: URL, ユーザー名, パスワードを指定して DriverManager.getConnection() で接続。
  • try-with-resources文: ConnectionPreparedStatement など、使い終わったリソースを自動的にクローズ(切断)する現代Javaの必須構文。

Step 5: セキュリティの壁 (Filter & Session)

  • セッション (HttpSession): ユーザーごとの「通行手形」。ログイン成功時にユーザー名を保存し、ページを移動しても状態を保持する。
  • フィルター (Filter): 全てのURLへのアクセスに割り込み、共通処理を行う関所。
  • 認証フィルターの実装:
    • アクセス時にセッションの有無を確認。
    • request.getSession(false) を使い、無駄なセッション作成を防止。
    • ログインしていない場合は response.sendRedirect() でログイン画面へ強制送還。
    • 無限ループを防ぐため、ログイン画面自身へのアクセスは許可する。

Step 6: 究極のデータ保護 (RLS: 行レベルセキュリティ)

  • RLSの概念: Java側で条件を絞るのではなく、データベース側で「誰がどの行を見れるか」を制御する仕組み。
  • DB側の準備:
    1. テーブルに「所有者」カラムを追加。
    2. ENABLE ROW LEVEL SECURITYFORCE ROW LEVEL SECURITY を適用。
    3. ポリシーを作成(Javaから送られる変数と所有者が一致する行のみ許可)。
  • Java側の連携:
    • DB接続時、SELECT文を発行する直前に、セッションから取得したユーザー名をDBへ送信。
    • SELECT set_config('app.current_user', ?, false) を使用(SETコマンドと ? バインドの相性問題を回避するため)。
    • データ取得のSQLは SELECT * FROM items のように全件検索のままでOK(DB側で勝手に絞り込まれる)。
  • スーパーユーザー問題の罠: Javaから postgres ユーザーで接続すると、神様権限によりRLSが無視され全件見えてしまう。一般ユーザー(app_user など)を作成し、そのユーザーでJDBC接続することで正しくRLSが機能した。

トラブルシューティングの記録と教訓

  1. コマンドが認識されないエラー:
    • 原因: 環境変数(Path)が通っていなかった。
    • 解決: ユーザー環境変数の Path に PostgreSQLの bin フォルダを登録し、コマンドプロンプトを再起動した。
  2. PSQLException 構文エラー (SETコマンド):
    • 原因: JDBCの PreparedStatement (?) は、PostgreSQLの SET コマンドとは相性が悪く構文エラーになる。
    • 解決: PostgreSQL独自の set_config() 関数を SELECT 文の中で呼び出すことで解決。
  3. RLSを設定したのに全件表示される問題:
    • 原因: JDBC接続のユーザーがスーパーユーザー(postgres)だったため、ポリシーが無視された。
    • 解決: 権限を絞った一般ユーザーを作成し、接続情報を書き換えた。
  4. 文字化けエラー (FATAL: [U[...):
    • 原因: Eclipse(UTF-8)とWindows(Shift-JIS)の文字コードの差異により、DBからの日本語エラーメッセージが文字化けした。内容は「パスワード認証失敗」。
    • 解決: psqlで一般ユーザーのパスワードを ALTER ROLE で確実なものに上書きし、Java側の設定と一致させた。

最終作成ソースコード一覧

1. login.jsp (ログイン画面)

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ログイン</title>
</head>
<body>
    <h1>ログイン画面</h1>
    <p>RLSのテスト用に、ユーザー名を入力してログインしてください。</p>
    <p>※例: 「User A」「User B」「Admin」など</p>
    
    <form action="LoginServlet" method="post">
        ユーザー名:<input type="text" name="username" required>
        <input type="submit" value="ログイン">
    </form>
</body>
</html>

2. LoginServlet.java (ログイン処理)

package sample;

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

@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        request.setCharacterEncoding("UTF-8");
        String username = request.getParameter("username");
        
        // セッションを取得し、入力されたユーザー名を保存(通行手形の発行)
        HttpSession session = request.getSession();
        session.setAttribute("loginUser", username);
        
        // ログイン完了後、DatabaseServlet へリダイレクト
        response.sendRedirect(request.getContextPath() + "/DatabaseServlet");
    }
}

3. AuthFilter.java (認証フィルター関所)

package sample;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@WebFilter("/*")
public class AuthFilter implements Filter {
    public void init(FilterConfig fConfig) throws ServletException {}

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        
        String requestURI = httpRequest.getRequestURI();
        HttpSession session = httpRequest.getSession(false); // falseで無駄な生成を防ぐ
        
        // ログイン状態と、ログイン画面へのアクセスかどうかを判定
        boolean isLoggedIn = (session != null && session.getAttribute("loginUser") != null);
        boolean isLoginRequest = requestURI.endsWith("/login.jsp") || requestURI.endsWith("/LoginServlet");
        
        if (isLoggedIn || isLoginRequest) {
            // ログイン済み、またはログイン処理中の場合は通す
            chain.doFilter(request, response);
        } else {
            // 未ログインの場合は、強制的にログイン画面へ飛ばす
            httpResponse.sendRedirect(httpRequest.getContextPath() + "/login.jsp");
        }
    }
    public void destroy() {}
}

4. DatabaseServlet.java (DB接続とRLSの連携)

package sample;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/DatabaseServlet")
public class DatabaseServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        // セッションからログイン中のユーザー名を取得
        javax.servlet.http.HttpSession session = request.getSession(false);
        String loginUser = (String) session.getAttribute("loginUser");
        
        // PostgreSQLの接続情報(一般ユーザーで接続し、神様権限を外す)
        String url = "jdbc:postgresql://localhost:5432/sample_db";
        String user = "app_user";
        // ※注意: 学習用のためパスワードを直接記述していますが、実務では環境変数などから取得します
        String pass = "app_pass"; 
        
        List<String> itemList = new ArrayList<>();

        try {
            Class.forName("org.postgresql.Driver");
            
            try (Connection conn = DriverManager.getConnection(url, user, pass)) {
                
                // 【RLS連携】現在のユーザー名をPostgreSQLに送信(セット)する
                // set_config関数を使用して ? のバインドエラーを回避
                try (PreparedStatement setParamStmt = conn.prepareStatement("SELECT set_config('app.current_user', ?, false)")) {
                    setParamStmt.setString(1, loginUser);
                    setParamStmt.execute();
                }
                
                // データ取得(WHERE句での絞り込みは書かず、RLSに任せる)
                try (PreparedStatement pstmt = conn.prepareStatement("SELECT item_name, owner_name FROM items ORDER BY id");
                     ResultSet rs = pstmt.executeQuery()) {
                    
                    while (rs.next()) {
                        String name = rs.getString("item_name");
                        String owner = rs.getString("owner_name");
                        itemList.add(name + " (所有者: " + owner + ")");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        request.setAttribute("itemList", itemList);
        request.getRequestDispatcher("/WEB-INF/jstl.jsp").forward(request, response);
    }
}

5. jstl.jsp (JSTLを使った結果表示画面)

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%-- JSTLの「コアタグ(c)」を使うための宣言 --%>
<%@ taglib prefix="c" uri="[http://java.sun.com/jsp/jstl/core](http://java.sun.com/jsp/jstl/core)" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>商品一覧 (RLS適用)</title>
</head>
<body>
    <h1>あなたの商品一覧</h1>
    <p>現在のログインユーザーに応じたデータのみ表示されています。</p>
    
    <table border="1">
        <tr>
            <th>商品名</th>
        </tr>
        
        <%-- c:forEach タグを使ってリストをループ表示 --%>
        <c:forEach items="${itemList}" var="item">
            <tr>
                <%-- セキュリティ対策(XSS対策): c:outを使用して出力値をエスケープする --%>
                <td><c:out value="${item}" /></td>
            </tr>
        </c:forEach>
    </table>
    
</body>
</html>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?