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文:
ConnectionやPreparedStatementなど、使い終わったリソースを自動的にクローズ(切断)する現代Javaの必須構文。
Step 5: セキュリティの壁 (Filter & Session)
- セッション (HttpSession): ユーザーごとの「通行手形」。ログイン成功時にユーザー名を保存し、ページを移動しても状態を保持する。
- フィルター (Filter): 全てのURLへのアクセスに割り込み、共通処理を行う関所。
-
認証フィルターの実装:
- アクセス時にセッションの有無を確認。
-
request.getSession(false)を使い、無駄なセッション作成を防止。 - ログインしていない場合は
response.sendRedirect()でログイン画面へ強制送還。 - 無限ループを防ぐため、ログイン画面自身へのアクセスは許可する。
Step 6: 究極のデータ保護 (RLS: 行レベルセキュリティ)
- RLSの概念: Java側で条件を絞るのではなく、データベース側で「誰がどの行を見れるか」を制御する仕組み。
-
DB側の準備:
- テーブルに「所有者」カラムを追加。
-
ENABLE ROW LEVEL SECURITYとFORCE ROW LEVEL SECURITYを適用。 - ポリシーを作成(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が機能した。
トラブルシューティングの記録と教訓
-
コマンドが認識されないエラー:
- 原因: 環境変数(Path)が通っていなかった。
- 解決: ユーザー環境変数の
Pathに PostgreSQLのbinフォルダを登録し、コマンドプロンプトを再起動した。
-
PSQLException 構文エラー (SETコマンド):
- 原因: JDBCの
PreparedStatement(?) は、PostgreSQLのSETコマンドとは相性が悪く構文エラーになる。 - 解決: PostgreSQL独自の
set_config()関数をSELECT文の中で呼び出すことで解決。
- 原因: JDBCの
-
RLSを設定したのに全件表示される問題:
- 原因: JDBC接続のユーザーがスーパーユーザー(
postgres)だったため、ポリシーが無視された。 - 解決: 権限を絞った一般ユーザーを作成し、接続情報を書き換えた。
- 原因: JDBC接続のユーザーがスーパーユーザー(
-
文字化けエラー (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>