対象ファイル
common/CommonExecute.javacommon/CommonUtil.javacommon/GetUrl.java
はじめに
前回は command.yoyaku パッケージを読み終えた。
今回は common パッケージの中から
プロジェクト全体の設計基盤を支える3つのクラスを読んでいく。
このパッケージを読むことで、
これまで各Commandクラスで「なんとなく使っていた」共通処理の
実装が初めてわかった。
CommonExecute.java(共通インターフェース)
プロジェクト全体のCommandパターンを支える
最もシンプルで最も重要なインターフェースだ。
public interface CommonExecute {
void execute(HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
たった1行のメソッド定義だが、
このインターフェースがあるおかげで
Front Controllerは全Commandを同じ方法で呼び出せる。
// Front Controller側
CommonExecute command = new MemberLogin();
command.execute(request, response); // どのCommandも同じ呼び出し方
インターフェースを使わなかった場合を考えると
その価値がよくわかる。
// インターフェースがない場合
if (gubun.equals("memberLogin")) {
MemberLogin login = new MemberLogin();
login.execute(request, response);
} else if (gubun.equals("memberLogout")) {
MemberLogout logout = new MemberLogout();
logout.doProcess(request, response); // メソッド名がバラバラになる
}
インターフェースで execute() を統一することで、
新しいCommandを追加しても呼び出し側のコードは変わらない。
これがオープン・クローズドの原則の実践だと気づいた。
気になった部分
戻り値が void になっている。
void execute(...); // 処理結果を返せない
成功・失敗・遷移先URLなどをCommandから返せないため、
各CommandがrequestにAttributeをセットして
Front Controllerへ結果を伝える間接的な方法を取っている。
// 改善案:結果オブジェクトを返す
CommandResult execute(HttpServletRequest request, HttpServletResponse response);
戻り値で遷移先やメッセージを返せるようにすると
設計がより明確になる。
CommonUtil.java(共通ユーティリティ)
プロジェクト全体で使われる共通処理をまとめたクラスだ。
各Commandで CommonUtil.〇〇() として何度も呼ばれていたメソッドの
実装がここで初めてわかった。
getToday() / getTodayTime()
public static String getToday() {
LocalDate now = LocalDate.now();
return now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
}
public static String getTodayTime() {
LocalDateTime now = LocalDateTime.now();
return now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));
}
Java 8の LocalDate・LocalDateTime を使っている。
以前は Date や Calendar が主流だったが、
こちらの方がシンプルで扱いやすい。
getSessionInfo()
public static String getSessionInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) return "";
String id = (String) session.getAttribute("sessionId");
return (id != null) ? id : "";
}
getSession(false) を使っているのが重要だ。
true を渡すと存在しない場合に新しいセッションを生成してしまうが、
false なら存在しない場合は null を返すだけで安全だ。
escapeHtml()
public static String escapeHtml(String input) {
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
XSS対策のサニタイズ処理だ。
各Commandで escapeHtml() を呼んでいた理由がここでわかった。
ただ手動で置換しているので
Apache Commons TextやSpring Webの
HtmlUtils.htmlEscape() などのライブラリを使う方が
見落としのリスクが少ない。
encryptSHA256()
public static String encryptSHA256(String input) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
}
パスワードのハッシュ化処理だ。
MessageDigest を使ったSHA-256ハッシュ化の実装が読めた。
ただSHA-256単体ではレインボーテーブル攻撃に弱い。
レインボーテーブル攻撃
= よく使われるパスワードのハッシュ値を事前に計算した表を使って
ハッシュ値から元のパスワードを逆引きする攻撃
ソルト(ランダムな文字列)を加えてからハッシュ化するか、
BCrypt などのパスワードハッシュ専用アルゴリズムを使うべきだ。
// 改善案:BCryptを使う
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String hashed = encoder.encode(rawPassword);
getFileSaveDir()
public static String getFileSaveDir(HttpServletRequest request, String category) {
String realPath = request.getServletContext().getRealPath("/");
return realPath + "attach/" + category + "/";
}
ファイルの保存先パスを動的に取得する処理だ。
getRealPath() でサーバー上の絶対パスを取得しているので
環境が変わっても動的に対応できる。
GetUrl.java(URLパーサー)
リクエストURLから処理に必要な部分だけを取り出す
Front Controller専用のユーティリティクラスだ。
public static String getUrl(HttpServletRequest request) {
String uri = request.getRequestURI(); // /CampReserve/Member
String contextPath = request.getContextPath(); // /CampReserve
// コンテキストパスを除いてServlet名だけ取り出す
String url = uri.substring(contextPath.length());
// 拡張子があれば除去(/Member.do → /Member)
if (url.contains(".")) {
url = url.substring(0, url.lastIndexOf("."));
}
return url.substring(1); // 先頭のスラッシュを除去 → "Member"
}
Front Controllerがどのコマンドを実行すべきか判断するために
URLから "Member" や "Notice" という文字列を取り出している。
substring() を連続して使うので
URLの形式が変わると壊れやすい。
// 改善案:正規表現で取り出す
String url = uri.replaceAll("^" + contextPath + "/|\\.[^.]+$", "");
今回学んだこと
-
CommonExecuteインターフェースが1メソッドだけでも
Front Controllerパターンの核心を支えている -
getSession(false)は既存セッションがない場合に
新しいセッションを生成しない安全な呼び出し方 - SHA-256単体のハッシュ化はレインボーテーブル攻撃に弱いため
ソルトを加えるかBCryptを使うべき - 共通処理を1クラスにまとめることで
変更時の修正箇所が1箇所で済む
次はDB接続・ファイルダウンロードを読んでいく。