隨著電子商務與政務系統成為社會核心基礎設施,J2EE 憑藉「一次編寫,到處運行」與完善的安全體系(JCA、JSSE、JAAS 等)成為首選平台。但框架提供的機制≠業務邏輯的安全。本文梳理 J2EE 多層架構中五大高風險領域,給出經驗證的解決方案。
| 技術 | 定位 | 安全角色 |
|---|---|---|
| EJB | 業務邏輯層 | 事務、權限、遠程調用邊界 |
| JAAS | 認證授權 | 統一身份驗證、基於角色的訪問控制 (RBAC) |
| JSP/Servlet | 表現層 | 輸入驗證、輸出編碼、會話管理 |
| JDBC/JNDI | 資源訪問 | 數據源隔離、連接池安全配置 |
| JMS | 異步消息 | 消息完整性、發送者身份驗證 |
瀏覽器磁盤緩存可能保留機密頁面(訂單詳情、個人信息)。最徹底的方案是 HTTP 響應頭控制:
public class CacheFilter implements Filter {
private String cacheControlType;
public void init(FilterConfig config) throws ServletException {
this.cacheControlType = config.getInitParameter("CacheControlType");
if (cacheControlType == null) throw new ServletException("缺少 CacheControlType 參數");
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
if (res instanceof HttpServletResponse) {
HttpServletResponse resp = (HttpServletResponse) res;
resp.addHeader("Cache-Control", cacheControlType); // no-store, no-cache, must-revalidate
resp.addHeader("Pragma", "no-cache");
resp.addDateHeader("Expires", 0);
}
chain.doFilter(req, res);
}
public void destroy() {}
}
web.xml 配置:映射至所有業務路徑(`/*`),參數值設為 `no-store`,確保瀏覽器不持久化任何敏感響應。
防止未授權用戶直接訪問 JSP/Action,核心策略:
<security-constraint>
<web-resource-collection>
<url-pattern>/shop/jsp/admin/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>ADMIN</role-name>
</auth-constraint>
</security-constraint>
<!-- 內部頁面僅允許虛擬角色 -->
<security-constraint>
<web-resource-collection>
<url-pattern>/shop/jsp/internal/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>INTERNAL</role-name> <!-- 無用戶擁有 -->
</auth-constraint>
</security-constraint>
反模式:Service 層直接接收 `HttpServletRequest`、返回 `HttpSession`,導致業務邏輯綁定 Web 協議,難以復用、測試、遷移。
正確模式:
這符合《企業應用架構模式》中「數據傳輸對象」與「服務層」模式,也是 Spring MVC、Dubbo 等框架的設計基石。
| 威脅 | 成因 | 對策 |
|---|---|---|
| 會話 Bean 冒充調用 | 網絡嗅探獲取憑證 | 全鏈路 SSL/TLS (RMI/IIOP over SSL) |
| 實體 Bean 未授權訪問 | 缺乏細粒度權限控制 | 本地接口 + JAAS 授權 + 聲明式安全 (method-permission) |
| MDB 惡意消息注入 | 消息來源不可信 | 數字簽名驗證:發送端私鑰簽名,接收端公鑰驗證,包含序列號防重放 |
// 發送端
Message msg = session.createTextMessage();
msg.setString("userId", userId);
msg.setString("item", item);
msg.setInt("seq", seqNum);
msg.setString("certId", certId);
String signData = userId + ":" + item + ":" + seqNum + ":" + certId;
msg.setString("signature", sign(signData, privateKey)); // 私鑰簽名
producer.send(msg);
// 接收端 (MDB onMessage)
String checkData = userId + ":" + msg.getString("item") + ":" + msg.getInt("seq") + ":" + certId;
if (!verify(checkData, msg.getString("signature"), publicKey)) {
throw new SecurityException("簽名驗證失敗,消息被篡改或偽造");
}
// 業務處理...
構建企業級 PKI 體系,核心流程:
// 關鍵步驟:簽發用戶證書
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream(caStore), storePass);
PrivateKey caPriKey = (PrivateKey) ks.getKey(alias, keyPass);
Certificate caCert = ks.getCertificate(alias);
// 提取簽發者 DN
X500Name issuer = extractIssuerDN(caCert);
// 構建用戶證書信息
X509CertInfo info = new X509CertInfo();
info.set(X509CertInfo.VALIDITY, new CertificateValidity(start, end));
info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serial));
info.set(X509CertInfo.ISSUER + ".dn", issuer);
info.set(X509CertInfo.ALGORITHM_ID, new AlgorithmId(AlgorithmId.md5WithRSAEncryption_oid));
info.set(X509CertInfo.SUBJECT + ".dn", userDN);
info.set(X509CertInfo.KEY, new CertificateX509Key(userPubKey));
// 簽名生成
X509CertImpl userCert = new X509CertImpl(info);
userCert.sign(caPriKey, "SHA256withRSA"); // 建議 SHA256+RSA
最佳實踐:證書有效期 1-2 年、CRL/OCSP 廢棄檢測、私鑰 HSM 保護、自動化續簽流程。
J2EE 安全非單點修補,需構建:
將安全內化為架構設計的核心約束,而非事後補丁,才是構建可信 J2EE 系統的正道。