傳統 web.xml 安全約束僅能控制整個 URL/文件級訪問,無法實現「同一頁面中,按鈕 A 管理員可見,按鈕 B 普通用戶可見」這種元素級細粒度權限。硬編碼 if (user.hasRole("admin")) 會導致業務邏輯與視圖耦合、維護成本高。
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ JSP 頁面 │────▶│ 自定義標簽 │────▶│ XML 權限配置 │
│ │ │ JspSecurityTag│ │ security.xml │
└─────────────┘ └──────────────┘ └─────────────────┘
▲ │ ▲
│ 讀取 Session 角色-元素映射
└────────────────────┴──────────────────────┘
JspSecurityTagpackage com.presentation.viewhelper;
import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.*;
import java.util.*;
import org.w3c.dom.*;
import javax.xml.parsers.*;
public class JspSecurityTag extends BodyTagSupport {
// 靜態緩存:角色-元素映射表
private static List roleList;
// 標簽屬性:受保護元素唯一名稱
private String elementName;
public void setElementName(String name) { this.elementName = name; }
@Override
public int doAfterBody() throws JspException {
// 懶加載配置
if (roleList == null) roleList = loadRoleMapping();
try {
// 從 Session 獲取當前用戶角色
String role = (String) pageContext.getSession().getAttribute("rolename");
// 驗證權限
if (isAuthorized(elementName, role)) {
// 輸出標簽體內容(受保護的 HTML 片段)
if (bodyContent != null) {
bodyContent.writeOut(bodyContent.getEnclosingWriter());
}
}
} catch (Exception e) {
throw new JspException(e);
}
return SKIP_BODY;
}
// 解析 XML 配置
private List loadRoleMapping() {
List list = new ArrayList<>();
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("security.xml")); // 生產環境建議用 getResourceAsStream
NodeList elements = doc.getElementsByTagName("htmlElement");
for (int i = 0; i < elements.getLength(); i++) {
Element el = (Element) elements.item(i);
String elemName = el.getAttribute("name");
NodeList roles = el.getElementsByTagName("roleName");
for (int j = 0; j < roles.getLength(); j++) {
String roleName = ((Element) roles.item(j)).getAttribute("name");
list.add(new ElementAndRole(elemName, roleName));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
// 權限判斷
private boolean isAuthorized(String elem, String role) {
return roleList.contains(new ElementAndRole(elem, role));
}
// 內部類:實現 equals/hashCode 供 contains 使用
static class ElementAndRole {
final String elementName, roleName;
ElementAndRole(String e, String r) { elementName = e; roleName = r; }
@Override public boolean equals(Object o) {
if (!(o instanceof ElementAndRole)) return false;
ElementAndRole other = (ElementAndRole) o;
return elementName.equals(other.elementName) && roleName.equals(other.roleName);
}
@Override public int hashCode() { return Objects.hash(elementName, roleName); }
}
}
security.xml<?xml version="1.0" encoding="GB2312"?>
<security>
<htmlElement name="employeedetail">
<roleName name="common"/>
<roleName name="admin"/>
</htmlElement>
<htmlElement name="employeemodify">
<roleName name="admin"/>
</htmlElement>
</security>
mytag.tld<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN"
"https://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
<taglib>
<tlibversion>1.0</tlibversion>
<jspversion>1.1</jspversion>
<shortname>myTag</shortname>
<tag>
<name>JspSecurity</name>
<tagclass>com.presentation.viewhelper.JspSecurityTag</tagclass>
<bodycontent>JSP</bodycontent>
<attribute>
<name>elementName</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
</taglib>
<%@ taglib uri="https://mytag" prefix="custTag" %>
<html><body>
<form>
<custTag:JspSecurity elementName="employeedetail">
<input type="button" value="詳細" />
</custTag:JspSecurity>
<custTag:JspSecurity elementName="employeemodify">
<input type="button" value="修改" />
</custTag:JspSecurity>
</form>
</body></html>
<taglib>
<taglib-uri>https://mytag</taglib-uri>
<taglib-location>/WEB-INF/mytag.tld</taglib-location>
</taglib>
security.xml 放置於類路徑根目錄(或改用 getResourceAsStream 讀取)。WEB-INF/classes 或打包進 JAR。mytag.tld 置於 WEB-INF/。session.setAttribute("rolename", userRole)。roleList 改為 Map> (元素→角色集合),查詢 O(1)。