Spring 框架雖提供優秀的 IoC/AOP 基礎設施,但本身不包含安全模塊。早期社區通過 Acegi Security(現已更名為 Spring Security)填補此空白。本文基於 Spring Security 現代架構(以 5.x/6.x 為準),系統闡述認證授權核心流程、數據庫擴展設計、攔截器配置、方法級安全、以及遷移建議,助你構建生產級權限控制體系。
┌─────────────────────────────────────┐
│ Security Filter Chain │ (Servlet Filter 鏈,入口防護)
├─────────────────────────────────────┤
│ AuthenticationManager │ 認證管理器:驗證身份
│ └─ ProviderManager │ ├─ DaoAuthenticationProvider (DB)
│ │ ├─ LdapAuthenticationProvider
│ │ └─ JwtAuthenticationProvider (自定義)
├─────────────────────────────────────┤
│ AccessDecisionManager │ 授權決策管理器:判定權限
│ ├─ AffirmativeBased (一票通過) │
│ ├─ ConsensusBased (少數服從多數) │
│ └─ UnanimousBased (全票通過) │
└─────────────────────────────────────┘
Acegi 原始 users/authorities 兩表過於簡陋,引入 角色 與 資源 概念,支持細粒度控制。
sys_user (id, username, password, enabled, ...)
│
├─ sys_user_role (user_id, role_id)
│ │
│ └─ sys_role (id, code, name, description)
│ │
│ └─ sys_role_permission (role_id, perm_id)
│ │
│ └─ sys_permission (id, code, name, type[URL/FUNC], pattern, method)
│
└─ sys_user_permission (user_id, perm_id) -- 用戶直授權(補充)
| 表名 | 核心字段 | 說明 |
|---|---|---|
| sys_user | id, username, password, salt, enabled, locked, expire_time | 用戶主表,密碼存 BCrypt 雜湊 |
| sys_role | id, code, name, description, enabled | 角色表,code 如 ROLE_ADMIN |
| sys_permission | id, code, name, type, pattern, http_method, description | 權限/資源表,type: 1=URL, 2=方法 |
| sys_user_role | user_id, role_id | 用戶-角色多對多 |
| sys_role_permission | role_id, perm_id | 角色-權限多對多 |
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired private UserMapper userMapper;
@Autowired private PermissionMapper permMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userMapper.selectByUsername(username);
if (user == null) throw new UsernameNotFoundException("用戶不存在");
// 查詢用戶權限碼集合
List<String> permCodes = permMapper.selectPermCodesByUserId(user.getId());
List<GrantedAuthority> authorities = permCodes.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getPassword(), user.isEnabled(),
true, true, true, authorities);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10); // 強度 10
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public DaoAuthenticationProvider daoAuthProvider(CustomUserDetailsService uds,
PasswordEncoder encoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(uds);
provider.setPasswordEncoder(encoder);
// 可選:緩存
// provider.setUserCache(new SpringCacheBasedUserCache(cacheManager));
return provider;
}
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/users/**").hasAuthority("USER_QUERY")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login").permitAll()
.defaultSuccessUrl("/index")
)
.logout(logout -> logout.logoutSuccessUrl("/login"))
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")) // API 通常無狀態
.sessionManagement(sess -> sess.maximumSessions(1)); // 同步登錄控制
return http.build();
}
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {}
@Service
public class OrderService {
@PreAuthorize("hasAuthority('ORDER_CREATE')")
public Order create(OrderDTO dto) { ... }
@PreAuthorize("hasAuthority('ORDER_DELETE') and #order.owner == authentication.name")
public void delete(Long orderId) { ... }
@Secured("ROLE_ADMIN")
public void adminOnly() { ... }
}
@Component
public class DynamicAccessDecisionManager implements AccessDecisionManager {
@Autowired private PermissionMapper permMapper;
@Override
public void decide(Authentication auth, Object object,
Collection<ConfigAttribute> configAttrs) {
// 從數據庫實時加載當前請求 URL 所需權限碼
// 與用戶 authorities 比對
// 不滿足拋出 AccessDeniedException
}
@Override public boolean supports(ConfigAttribute attr) { return true; }
@Override public boolean supports(Class<?> clazz) { return true; }
}
| Acegi (舊) | Spring Security (新) | 備註 |
|---|---|---|
| net.sf.acegisecurity.* | org.springframework.security.* | 包名全變 |
| AuthenticationDao | UserDetailsService | 接口更名 |
| AutoIntegrationFilter | SecurityContextPersistenceFilter | Filter 鏈重構 |
| FilterSecurityInterceptor (XML bean) | SecurityFilterChain (DSL) | 配置方式變革 |
| MethodDefinitionMap / DBFilterInvocationDefinitionSource | @PreAuthorize / MethodSecurityInterceptor | 方法級安全註解化 |
| jdbcDaoImpl + XML SQL | JdbcUserDetailsManager / 自定義 UserDetailsService | 更靈活 |
AuthenticationSuccessEventListener、AuthorizationAuditListener 記錄關鍵操作。@WithMockUser、@WithAnonymousUser 單元測試覆蓋關鍵授權點。從 Acegi 到 Spring Security,核心思想未變:認證確認「你是誰」,授權決定「你能幹什麼」。現代 Spring Security 以 DSL 配置、註解驅動、響應式支持、OAuth2/JWT 整合,將安全從「XML 地獄」解放為「代碼即配置」的工程實踐。掌握其 Filter 鏈機制、Authentication/Authorization 雙引擎、以及與業務數據模型的映射關係,即可構建既符合合規要求、又具備極高擴展性的企業級權限控制系統。