承接上篇基礎配置,本文深入實戰:啟動時緩存資源授權映射、實現 FilterInvocationDefinitionSource 動態 URL 權限決策、通過 custom-filter 注入自定義攔截器、以及安全上下文編程式訪問模式。
public class SecurityContextLoader implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext sc = event.getServletContext();
SecurityManager mgr = WebApplicationContextUtils
.getWebApplicationContext(sc).getBean("securityManager", SecurityManager.class);
sc.setAttribute("urlAuthorities", mgr.loadUrlAuthorities());
}
@Override
public void contextDestroyed(ServletContextEvent event) {
event.getServletContext().removeAttribute("urlAuthorities");
}
}
web.xml 註冊:
<listener>
<listener-class>com.example.security.SecurityContextLoader</listener-class>
</listener>
@Service("securityManager")
public class SecurityManagerImpl extends HibernateDaoSupport
implements UserDetailsService, SecurityManager {
@Autowired
public void init(SessionFactory sf) { setSessionFactory(sf); }
@Override
public UserDetails loadUserByUsername(String username) {
List users = getHibernateTemplate().find(
"FROM User u WHERE u.name=? AND u.disabled=false", username);
if (users.isEmpty()) throw new UsernameNotFoundException("User not found: " + username);
return users.get(0);
}
@Override
public Map loadUrlAuthorities() {
Map map = new HashMap<>();
List resources = getHibernateTemplate().find(
"FROM Resource r WHERE r.type=?", "URL");
for (Resource r : resources) {
map.put(r.getValue(), r.getRoleAuthorities()); // 如 "/admin/**" → "ROLE_ADMIN"
}
return map;
}
}
public class DynamicResourceFilter implements FilterInvocationDefinitionSource, InitializingBean {
private UrlMatcher urlMatcher = new AntUrlPathMatcher(); // 支援 Ant 路徑模式
private boolean lowercase = true;
@Override
public void afterPropertiesSet() {
if (lowercase) ((AntUrlPathMatcher) urlMatcher).setRequiresLowerCaseUrl(true);
}
@Override
public ConfigAttributeDefinition getAttributes(Object object) {
FilterInvocation fi = (FilterInvocation) object;
String requestUrl = fi.getRequestUrl();
Map urlAuthorities = getUrlAuthorities(fi);
for (Map.Entry entry : urlAuthorities.entrySet()) {
if (urlMatcher.pathMatchesUrl(entry.getKey(), requestUrl)) {
ConfigAttributeEditor editor = new ConfigAttributeEditor();
editor.setAsText(entry.getValue()); // 解析 "ROLE_ADMIN,ROLE_USER"
return (ConfigAttributeDefinition) editor.getValue();
}
}
return null; // 無匹配 → 交由後續決策器 (如默認拒絕)
}
@SuppressWarnings("unchecked")
private Map getUrlAuthorities(FilterInvocation fi) {
return (Map) fi.getHttpRequest()
.getSession().getServletContext().getAttribute("urlAuthorities");
}
// 其餘接口方法...
@Override public Collection getConfigAttributeDefinitions() { return null; }
@Override public boolean supports(Class clazz) { return true; }
}
<beans:beans xmlns="https://www.springframework.org/schema/security"
xmlns:beans="https://www.springframework.org/schema/beans">
<http access-denied-page="/403.jsp">
<intercept-url pattern="/static/**" filters="none"/>
<intercept-url pattern="/login.jsp" filters="none"/>
<form-login login-page="/login.jsp" default-target-url="/index"/>
<logout logout-success-url="/login.jsp"/>
</http>
<authentication-manager alias="authenticationManager"/>
<authentication-provider user-service-ref="securityManager">
<password-encoder hash="md5"/>
</authentication-provider>
<beans:bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
<beans:property name="decisionVoters">
<beans:list>
<beans:bean class="org.springframework.security.vote.RoleVoter"/>
<beans:bean class="org.springframework.security.vote.AuthenticatedVoter"/>
</beans:list>
</beans:property>
</beans:bean>
<!-- 關鍵:自定義 FilterSecurityInterceptor 插入過濾器鏈末尾 -->
<beans:bean id="resourceSecurityInterceptor"
class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
<beans:property name="authenticationManager" ref="authenticationManager"/>
<beans:property name="accessDecisionManager" ref="accessDecisionManager"/>
<beans:property name="objectDefinitionSource" ref="dynamicResourceFilter"/>
<beans:property name="observeOncePerRequest" value="false"/>
<custom-filter after="LAST"/> <!-- 插入位置 -->
</beans:bean>
<beans:bean id="dynamicResourceFilter"
class="com.example.security.DynamicResourceFilter"/>
</beans:beans>
| 需求 | 代碼模式 | 關鍵 API |
|---|---|---|
| 當前登錄用戶 | User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); | SecurityContextHolder |
| 用戶權限字符串 | String auths = StringUtils.join(user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(toList()), ","); | UserDetails.getAuthorities() |
| 用戶可訪問資源 | 在 User 實體中緩存 Map,首次遍歷 Role→Resource 構建 | 領域模型擴展 |
@Entity
public class User implements UserDetails {
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="user_role", ...)
private Set roles;
@Transient
private Map> roleResourcesCache;
public Map> getRoleResources() {
if (roleResourcesCache == null) {
roleResourcesCache = new HashMap<>();
for (Role role : roles) {
for (Resource res : role.getResources()) {
String key = role.getName() + "_" + res.getType();
roleResourcesCache.computeIfAbsent(key, k -> new ArrayList<>()).add(res);
}
}
}
return roleResourcesCache;
}
// UserDetails 方法實現...
public String getAuthoritiesString() {
return roles.stream().map(Role::getName).collect(joining(","));
}
}
FilterInvocationDefinitionSource 運行時匹配 Ant 路徑模式custom-filter after="LAST" 保留默認鏈,追加業務邏輯此架構支援資源授權的熱更新:管理後台修改資源-角色映射後,僅需重新調用 SecurityManager.loadUrlAuthorities() 刷新 ServletContext 屬性,無需重啟應用。