1. 概述
使用 Spring Security,我們可以為應用程序的方法(例如我們的端點)配置身份驗證和授權。例如,如果用户在我們的域中具有身份驗證,我們可以通過在現有方法上應用限制來分析其應用程序的使用情況。
使用 <a href="https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#jc-enable-global-method-security">@EnableGlobalMethodSecurity</a> 註解在 5.6 版本之前一直是標準做法,而 <a href="https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html#_enablemethodsecurity">@EnableMethodSecurity</a> 註解則提供了一種更靈活的方法來配置方法安全性。
在本教程中,我們將看到 @EnableMethodSecurity 註解如何取代 @EnableGlobalMethodSecurity 註解。我們還將看到它們之間的差異以及一些代碼示例。
2. @EnableMethodSecurity vs. @EnableGlobalMethodSecurity
讓我們來了解一下方法授權如何與 @EnableMethodSecurity 和 @EnableGlobalMethodSecurity> 一起工作。
2.1. <em @EnableMethodSecurity</em>>
使用 <em @EnableMethodSecurity</em>>,我們可以看到 Spring Security 轉向基於 Bean 的配置,用於授權類型。
與其採用全局配置,現在我們為每種類型都提供了一個配置。例如,讓我們看看 <a href="https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/access/annotation/Jsr250MethodSecurityConfig.html"><em>Jsr250MethodSecurityConfiguration</em></a>。
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
class Jsr250MethodSecurityConfiguration {
// ...
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor jsr250AuthorizationMethodInterceptor() {
return AuthorizationManagerBeforeMethodInterceptor.jsr250(this.jsr250AuthorizationManager);
}
@Autowired(required = false)
void setGrantedAuthorityDefaults(GrantedAuthorityDefaults grantedAuthorityDefaults) {
this.jsr250AuthorizationManager.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
}MethodInterceptor 基本上包含一個 AuthorizationManager,它現在將檢查和返回 AuthorizationDecision 對象以及最終決策的責任委託給適當的實現,例如 AuthenticatedAuthorizationManager。
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
boolean granted = isGranted(authentication.get());
return new AuthorityAuthorizationDecision(granted, this.authorities);
}
private boolean isGranted(Authentication authentication) {
return authentication != null && authentication.isAuthenticated() && isAuthorized(authentication);
}
private boolean isAuthorized(Authentication authentication) {
Set<String> authorities = AuthorityUtils.authorityListToSet(this.authorities);
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (authorities.contains(grantedAuthority.getAuthority())) {
return true;
}
}
return false;
}
MethodInterceptor 在我們無法訪問資源時,會拋出 AccessDeniedException。
AuthorizationDecision decision = this.authorizationManager.check(AUTHENTICATION_SUPPLIER, mi);
if (decision != null && !decision.isGranted()) {
// ...
throw new AccessDeniedException("Access Denied");
}2.2. <em @EnableGlobalMethodSecurity</em>>
<em @EnableGlobalMethodSecurity</em>> 是我們需要與 <em @EnableWebSecurity</em>> 一起使用的函數接口,用於創建我們的安全層並實現方法授權。
讓我們創建一個示例配置類:
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Configuration
public class SecurityConfig {
// security beans
}所有方法安全實現都使用一個 MethodInterceptor,當需要進行授權時觸發。
在這種情況下,GlobalMethodSecurityConfiguration 類是啓用全局方法安全的基礎配置。
MethodInterceptor 方法使用元數據創建 MethodInterceptor Bean,用於不同的授權類型。
Spring Security 支持三種內置的方法安全註解:
- prePostEnabled,用於 Spring pre/post 註解
- securedEnabled,用於 Spring @Secured 註解
- jsr250Enabled,用於標準 Java @RoleAllowed 註解
此外,在 methodSecurityInterceptor() 中,還設置了:
- AccessDecisionManager,它使用基於投票的機制來決定是否授予訪問權限
- AuthenticationManager,它從安全上下文中獲取,負責身份驗證
- AfterInvocationManager,負責提供 pre/post 表達式的處理程序
框架具有一個投票機制,用於拒絕或授予特定方法的訪問權限。我們可以查看 Jsr250Voter: 作為一個例子。
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> definition) {
boolean jsr250AttributeFound = false;
for (ConfigAttribute attribute : definition) {
if (Jsr250SecurityConfig.PERMIT_ALL_ATTRIBUTE.equals(attribute)) {
return ACCESS_GRANTED;
}
if (Jsr250SecurityConfig.DENY_ALL_ATTRIBUTE.equals(attribute)) {
return ACCESS_DENIED;
}
if (supports(attribute)) {
jsr250AttributeFound = true;
// Attempt to find a matching granted authority
for (GrantedAuthority authority : authentication.getAuthorities()) {
if (attribute.getAttribute().equals(authority.getAuthority())) {
return ACCESS_GRANTED;
}
}
}
}
return jsr250AttributeFound ? ACCESS_DENIED : ACCESS_ABSTAIN;
}在投票時,Spring Security 會從當前方法中拉取元數據屬性,例如我們的 REST 端點。最後,它會根據用户授予的權限進行驗證。
我們也應該注意到,投票者可能不支持投票系統並退避。
我們的 AccessDecisionManager 會評估可用的投票者返回的所有響應:
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}如果我們想要自定義我們的 Bean,我們可以擴展 GlobalMethodSecurityConfiguration 類。例如,我們可能想要自定義的安全表達式,而不是 Spring Security 內置的 Spring EL。或者,我們可能想要創建一個自定義的安全投票器。
3. @EnableMethodSecurity</em/> 功能
@EnableMethodSecurity 與之前的遺留實現相比,帶來了顯著的改進,包括次要和主要方面。
3.1. 細微改進
所有授權類型仍然得到支持。例如,它仍然符合 JSR-250 的規範。但是,我們無需在註解中添加 prePostEnabled ,因為它現在默認設置為 true:。
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
我們需要將 prePostEnabled 設置為 false 才能禁用它。
3.2. 主要改進
GlobalMethodSecurityConfiguration 類已不再使用。Spring Security 使用分段配置和 <em><a href="https://docs.spring.io/spring-security/reference/servlet/authorization/architecture.html#_the_authorizationmanager">AuthorizationManager</a></em>,這意味着我們可以定義授權 Bean,而無需擴展任何基礎配置類。
值得注意的是,<em>AuthorizationManager</em> 接口是通用的,可以適應任何對象,儘管標準安全適用於 <em>MethodInvocation</em>。
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
總體而言,這使我們能夠使用精細化的授權,通過 委派實現。 因此,在實踐中,我們為每種類型都有一個 AuthorizationManager。 當然,我們也可以自行構建。
此外,這也意味着 @EnableMethodSecurity 不允許使用像遺留實現中那樣,帶有 @AspectJ 註解的 @AspectJ 方法攔截器。
public final class AspectJMethodSecurityInterceptor extends MethodSecurityInterceptor {
public Object invoke(JoinPoint jp) throws Throwable {
return super.invoke(new MethodInvocationAdapter(jp));
}
// ...
}然而,我們仍然完全支持 AOP。例如,讓我們來看一下 Jsr250MethodSecurityConfiguration 中討論過的攔截器:
public final class AuthorizationManagerBeforeMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
// ...
public AuthorizationManagerBeforeMethodInterceptor(
Pointcut pointcut, AuthorizationManager<MethodInvocation> authorizationManager) {
Assert.notNull(pointcut, "pointcut cannot be null");
Assert.notNull(authorizationManager, "authorizationManager cannot be null");
this.pointcut = pointcut;
this.authorizationManager = authorizationManager;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
attemptAuthorization(mi);
return mi.proceed();
}
}4. 自定義 AuthorizationManager 應用
讓我們看看如何創建自定義的授權管理器。
假設我們有需要應用策略的端點。我們只想授權用户如果他擁有該策略,否則會阻止該用户。
首先,我們通過向用户對象添加一個字段來訪問受限策略:
public class SecurityUser implements UserDetails {
private String userName;
private String password;
private List<GrantedAuthority> grantedAuthorityList;
private boolean accessToRestrictedPolicy;
// getters and setters
}現在,讓我們來查看我們的身份驗證層,以定義系統中的用户。為此,我們將創建一個自定義的 UserDetailsService。我們將使用內存映射來存儲用户:
public class CustomUserDetailService implements UserDetailsService {
private final Map<String, SecurityUser> userMap = new HashMap<>();
public CustomUserDetailService(BCryptPasswordEncoder bCryptPasswordEncoder) {
userMap.put("user", createUser("user", bCryptPasswordEncoder.encode("userPass"), false, "USER"));
userMap.put("admin", createUser("admin", bCryptPasswordEncoder.encode("adminPass"), true, "ADMIN", "USER"));
}
@Override
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
return Optional.ofNullable(map.get(username))
.orElseThrow(() -> new UsernameNotFoundException("User " + username + " does not exists"));
}
private SecurityUser createUser(String userName, String password, boolean withRestrictedPolicy, String... role) {
return SecurityUser.builder().withUserName(userName)
.withPassword(password)
.withGrantedAuthorityList(Arrays.stream(role)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()))
.withAccessToRestrictedPolicy(withRestrictedPolicy);
}
}一旦用户進入我們的系統,我們希望通過檢查用户是否擁有特定權限策略來限制其可訪問的信息。
為了演示,我們創建了一個 Java 註解 @Policy,用於在方法上應用,以及一個策略枚舉:
@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Policy {
PolicyEnum value();
}public enum PolicyEnum {
RESTRICTED, OPEN
}讓我們創建一個應用該策略的服務:
@Service
public class PolicyService {
@Policy(PolicyEnum.OPEN)
public String openPolicy() {
return "Open Policy Service";
}
@Policy(PolicyEnum.RESTRICTED)
public String restrictedPolicy() {
return "Restricted Policy Service";
}
}我們不能使用內置的授權管理器,例如 Jsr250AuthorizationManager。它不會知道何時以及如何攔截服務策略檢查。因此,我們定義一個自定義管理器:
public class CustomAuthorizationManager<T> implements AuthorizationManager<MethodInvocation> {
...
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
if (hasAuthentication(authentication.get())) {
Policy policyAnnotation = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), Policy.class);
SecurityUser user = (SecurityUser) authentication.get().getPrincipal();
return new AuthorizationDecision(Optional.ofNullable(policyAnnotation)
.map(Policy::value).filter(policy -> policy == PolicyEnum.OPEN
|| (policy == PolicyEnum.RESTRICTED && user.hasAccessToRestrictedPolicy())).isPresent());
}
return new AuthorizationDecision(false);
}
private boolean hasAuthentication(Authentication authentication) {
return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated();
}
private boolean isNotAnonymous(Authentication authentication) {
return !this.trustResolver.isAnonymous(authentication);
}
}當服務方法被觸發時,我們首先驗證用户是否已進行身份驗證。如果策略已打開,則授予訪問權限。如果存在限制,則檢查用户是否可以訪問受限制的策略。
為此,我們需要定義一個 MethodInterceptor,它將在執行之前或之後生效。讓我們將其與我們的安全配置類結合起來:
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity httpSecurity, UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
return authenticationManagerBuilder.build();
}
@Bean
public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) {
return new CustomUserDetailService(bCryptPasswordEncoder);
}
@Bean
public AuthorizationManager<MethodInvocation> authorizationManager() {
return new CustomAuthorizationManager<>();
}
@Bean
@Role(ROLE_INFRASTRUCTURE)
public Advisor authorizationManagerBeforeMethodInterception(AuthorizationManager<MethodInvocation> authorizationManager) {
JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
pattern.setPattern("com.baeldung.enablemethodsecurity.services.*");
return new AuthorizationManagerBeforeMethodInterceptor(pattern, authorizationManager);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry.anyRequest().authenticated())
.sessionManagement(httpSecuritySessionManagementConfigurer ->
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}我們正在使用 AuthorizationManagerBeforeMethodInterceptor。它與我們的策略服務模式相匹配,並使用自定義的授權管理器。
此外,我們還需要使 AuthenticationManager 能夠感知自定義的 UserDetailsService。然後,當 Spring Security 攔截服務方法時,我們可以訪問我們的自定義用户並檢查用户策略訪問權限。
5. 測試
以下定義一個 REST 控制器:
@RestController
public class ResourceController {
// ...
@GetMapping("/openPolicy")
public String openPolicy() {
return policyService.openPolicy();
}
@GetMapping("/restrictedPolicy")
public String restrictedPolicy() {
return policyService.restrictedPolicy();
}
}我們將會使用 Spring Boot Test 與我們的應用程序來模擬方法安全驗證:
@SpringBootTest(classes = EnableMethodSecurityApplication.class)
public class EnableMethodSecurityTest {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@BeforeEach
public void setup() {
mvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessOpenEndpoint_thenOk() throws Exception {
mvc.perform(get("/openPolicy"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails(value = "admin")
public void whenAdminAccessRestrictedEndpoint_thenOk() throws Exception {
mvc.perform(get("/restrictedPolicy"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails()
public void whenUserAccessOpenEndpoint_thenOk() throws Exception {
mvc.perform(get("/openPolicy"))
.andExpect(status().isOk());
}
@Test
@WithUserDetails()
public void whenUserAccessRestrictedEndpoint_thenIsForbidden() throws Exception {
mvc.perform(get("/restrictedPolicy"))
.andExpect(status().isForbidden());
}
}所有響應都應獲得授權,除非用户調用某個服務,且他沒有訪問受限策略的權限。
6. 結論
在本文中,我們瞭解了 @EnableMethodSecurity 的主要功能,以及它如何取代 @EnableGlobalMethodSecurity。
我們還通過分析實現流程 ,學習了這兩種註解之間的差異。隨後,我們探討了 @EnableMethodSecurity 提供更多基於 Bean 的配置靈活性。最後,我們明白瞭如何創建自定義授權管理器和 MVC 測試。