基於 Spring Security 3.2 + Spring 4 构建企業級 RBAC 權限系統,核心組件:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<http auto-config="true" use-expressions="true">
<!-- 靜態資源放行 -->
<http pattern="/js/**" security="none"/>
<http pattern="/css/**" security="none"/>
<http pattern="/images/**" security="none"/>
<!-- 表單登錄 -->
<form-login login-page="/login.jsp"
authentication-success-handler-ref="authSuccessHandler"
authentication-failure-handler-ref="authFailureHandler"/>
<!-- 記住我 -->
<remember-me key="appKey" use-secure-cookie="true"/>
<!-- 會話管理 -->
<session-management>
<concurrency-control max-sessions="1" error-if-maximum-exceeded="false"/>
</session-management>
<!-- 自定義授權過濾器 -->
<custom-filter ref="customFilterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
</http>
<authentication-manager>
<authentication-provider user-service-ref="customUserDetailsService">
<password-encoder hash="md5">
<salt-source user-property="username"/>
</password-encoder>
</authentication-provider>
</authentication-manager>
@Service
public class CustomFilterInvocationSecurityMetadataSource
implements FilterInvocationSecurityMetadataSource {
@Autowired AuthorityService authorityService;
private static Map<String, Collection<ConfigAttribute>> resourceMap;
@PostConstruct
public void loadResourceDefine() {
List<Authority> authorities = authorityService.findAll();
resourceMap = new HashMap<>();
for (Authority auth : authorities) {
String url = auth.getResourceUrl();
ConfigAttribute ca = new SecurityConfig(auth.getCode()); // ROLE_*
resourceMap.computeIfAbsent(url, k -> new ArrayList<>()).add(ca);
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) {
String url = ((FilterInvocation)object).getRequestUrl();
// Ant 路徑匹配,最長路徑優先
return resourceMap.entrySet().stream()
.filter(e -> url.startsWith(e.getKey()))
.max(Comparator.comparingInt(e -> e.getKey().length()))
.map(Map.Entry::getValue)
.orElse(null); // null 表示放行
}
}
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication auth, Object obj, Collection<ConfigAttribute> configAttrs) {
if (configAttrs == null) return; // 無需權限
for (ConfigAttribute ca : configAttrs) {
String needRole = ((SecurityConfig)ca).getAttribute();
for (GrantedAuthority ga : auth.getAuthorities()) {
if (needRole.equals(ga.getAuthority())) return; // 權限匹配
}
}
throw new AccessDeniedException("權限不足: " + configAttrs);
}
}
@Service
public class CustomFilterSecurityInterceptor
extends AbstractSecurityInterceptor implements Filter {
@Resource CustomFilterInvocationSecurityMetadataSource securityMetadataSource;
@Resource CustomAccessDecisionManager accessDecisionManager;
@Resource AuthenticationManager authenticationManager;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
// 未登錄重定向
if (SecurityContextHolder.getContext().getAuthentication() == null) {
((HttpServletResponse)res).sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
FilterInvocation fi = new FilterInvocation(request, res, chain);
InterceptorStatusToken token = beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
afterInvocation(token, null);
}
}
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired UserDao userDao;
@Autowired AuthorityDao authorityDao;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userDao.findByName(username);
if (user == null) throw new UsernameNotFoundException("用戶不存在");
List<GrantedAuthority> authorities = new ArrayList<>();
if (user.getUserType() == UserType.SYS_ADMIN) {
authorityDao.findAll().forEach(a ->
authorities.add(new SimpleGrantedAuthority(a.getCode())));
} else {
user.getAuthoritySet().forEach(a ->
authorities.add(new SimpleGrantedAuthority(a.getCode())));
}
return new UserDetail(
user.getName(), user.getPassword(),
user.getEnabled(), true, true,
user.getUserState() == UserState.ENABLE,
authorities
);
}
}
ROLE_*、資源 URL、父權限、菜單關聯、樹形結構User,擴展真實姓名、用戶組 ID 等業務字段// 登錄成功處理器
public class CustomAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(...) {
// 記錄登錄日誌、加載用戶作物權限到 Session
super.onAuthenticationSuccess(...);
}
}
// 登出成功處理器
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(...) {
// 記錄登出日誌
super.onLogoutSuccess(...);
}
}
BCryptPasswordEncoder) 替代 MD5_csrf 隱藏域session-management session-fixation-protection="migrateSession"本文展示了基於 Spring Security 構建動態 RBAC 系統的完整鏈路:配置 → 元數據加載 → 認證授權核心組件 → 業務實體設計。關鍵在於 FilterInvocationSecurityMetadataSource 動態加載資源權限映射,配合自定義 AccessDecisionManager 實現細粒度 URL 級權限控制,滿足企業級應用複雜授權需求。