Struts 2 內建 RolesInterceptor 基於 J2EE 容器授權,粒度較粗。本文示範如何利用攔截器機制,結合 Hibernate+Spring 實現應用層面的細粒度角色基礎存取控制(RBAC)。
admin,在日曆 #456 是 reader
@Entity
public class UserRole {
private Long id;
private User user;
private String objectType; // 資源類型,如 "calendar"
private Long objectId; // 資源 ID
private String role; // 角色名,如 "admin", "owner", "reader"
// JPA 註解映射...
@ManyToOne @JoinColumn(name="userId")
public User getUser() { ... }
}
複合鍵 (userId, objectType, objectId) 唯一決定角色。
public interface UserRoleDao {
void create(UserRole ur);
void update(UserRole ur);
UserRole find(Long userId, String objectType, Long objectId);
}
public class UserRoleDaoImpl implements UserRoleDao {
@PersistenceContext EntityManager em;
public UserRole find(Long userId, String objectType, Long objectId) {
Query q = em.createQuery(
"FROM UserRole ur WHERE ur.user.id=?1 AND ur.objectType=?2 AND ur.objectId=?3"
).setParameter(1, userId).setParameter(2, objectType).setParameter(3, objectId);
List r = q.getResultList();
return r.isEmpty() ? null : (UserRole)r.get(0);
}
// create/update 省略...
}
public interface RoleService {
void setUserRole(Long userId, String role, String objectType, Long objectId);
String findRole(Long userId, String objectType, Long objectId);
}
@Transactional(readOnly=true)
public class RoleServiceImpl implements RoleService {
private UserRoleDao userRoleDao;
@Transactional(readOnly=false)
public void setUserRole(Long userId, String role, String objectType, Long objectId) {
UserRole ur = new UserRole(userId, role, objectType, objectId);
UserRole existing = userRoleDao.find(userId, objectType, objectId);
if (existing == null) userRoleDao.create(ur);
else { ur.setId(existing.getId()); userRoleDao.update(ur); }
}
public String findRole(Long userId, String objectType, Long objectId) {
UserRole ur = userRoleDao.find(userId, objectType, objectId);
return ur == null ? null : ur.getRole();
}
}
public class RBACInterceptor implements Interceptor {
public static final String FORBIDDEN = "forbidden";
private List allowedRoles = new ArrayList<>();
private List disallowedRoles = new ArrayList<>();
private RoleService roleService;
private String objectType; // 資源類型
private String objectIdKey; // HTTP 參數名
// Setter 供 Spring 注入與 Struts 配置注入...
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest req = ServletActionContext.getRequest();
Long objectId = Long.valueOf(req.getParameter(objectIdKey));
Map session = invocation.getInvocationContext().getSession();
Long userId = (Long) session.get(Constant.KEY_CURRENT_USER);
String userRole = roleService.findRole(userId, objectType, objectId);
if (!isAllowed(userRole)) return FORBIDDEN;
return invocation.invoke(); // 繼續執行 Action
}
protected boolean isAllowed(String userRole) {
if (!allowedRoles.isEmpty()) {
return userRole != null && allowedRoles.contains(userRole);
} else if (!disallowedRoles.isEmpty()) {
return userRole == null || !disallowedRoles.contains(userRole);
}
return true; // 無限制
}
}
關鍵點:
id=123)objectType 查詢角色forbidden,由 Result 處理錯誤頁
<bean id="userRoleDao" class="...UserRoleDaoImpl"/>
<bean id="roleService" class="...RoleServiceImpl">
<property name="userRoleDao" ref="userRoleDao"/>
</bean>
<bean id="RBACInterceptor" scope="prototype" class="...RBACInterceptor">
<property name="roleService" ref="roleService"/>
</bean>
<interceptors>
<interceptor name="RBAC" class="RBACInterceptor"/>
</interceptors>
<action name="deleteMeeting" class="...DeleteMeetingAction">
<result>/deleteMeetingResult.jsp</result>
<result name="forbidden">/forbidden.jsp</result>
<interceptor-ref name="RBAC">
<param name="allowedRoles">admin, owner</param>
<param name="objectType">calendar</param>
<param name="objectIdKey">id</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
</action>
<action name="assignCalendarRole" class="...AssignCalendarRoleAction">
<result>/assignResult.jsp</result>
<result name="forbidden">/forbidden.jsp</result>
<interceptor-ref name="RBAC">
<param name="allowedRoles">owner</param>
<param name="objectType">calendar</param>
<param name="objectIdKey">id</param>
</interceptor-ref>
<interceptor-ref name="defaultStack"/>
</action>
deleteMeeting 允許 admin 或 owner;assignCalendarRole 僅允許 owner 分配角色。
public class AssignCalendarRoleAction extends ActionSupport {
private RoleService roleService;
private Long userId, id;
private String userRole = "reader";
public AssignCalendarRoleAction(RoleService roleService) { this.roleService = roleService; }
public String execute() {
roleService.setUserRole(userId, userRole, "calendar", id);
return SUCCESS;
}
}
此實現展示了應用層 RBAC 的核心架構,優於容器授權於:資源實例級粒度、動態角色解析、與業務邏輯整合。可擴展方向:用戶群組支援、權限繼承、快取優化、審計日誌等。