1. 概述
有時,在配置應用程序安全時,我們的用户詳細信息可能不包含 Spring Security 期望的 ROLE_ 前綴。 從而導致“Forbidden”授權錯誤,無法訪問我們的受保護端點。
在本教程中,我們將探索如何重新配置 Spring Security 以允許使用不帶 ROLE_ 前綴的角色。
2. Spring Security 默認行為
我們將從演示 Spring security 角色檢查機制的默認行為開始。讓我們添加一個 InMemoryUserDetailsManager,其中只包含一個用户,具有 ADMIN 角色:
@Configuration
public class UserDetailsConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails admin = User.withUsername("admin")
.password(encoder().encode("password"))
.authorities(singletonList(new SimpleGrantedAuthority("ADMIN")))
.build();
return new InMemoryUserDetailsManager(admin);
}
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}
我們創建了 UserDetailsConfig 配置類,它生成了一個 InMemoryUserDetailsManager Bean。在工廠方法中,我們使用了 PasswordEncoder,它對於用户詳細信息密碼是必需的。
接下來,我們將添加要調用的端點:
@RestController
public class TestSecuredController {
@GetMapping("/test-resource")
public ResponseEntity<String> testAdmin() {
return ResponseEntity.ok("GET request successful");
}
}
我們添加了一個簡單的 GET 端點,它應該返回 200 狀態碼。
讓我們創建一個安全配置:
@Configuration
@EnableWebSecurity
public class DefaultSecurityJavaConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests (authorizeRequests -> authorizeRequests
.requestMatchers("/test-resource").hasRole("ADMIN"))
.httpBasic(withDefaults())
.build();
}
}
在這裏,我們創建了一個 SecurityFilterChain Bean,其中我們指定只有具有 ADMIN 角色才能訪問 test-resource 端點的。
現在,讓我們將這些配置添加到我們的測試上下文並調用受保護的端點:
@WebMvcTest(controllers = TestSecuredController.class)
@ContextConfiguration(classes = { DefaultSecurityJavaConfig.class, UserDetailsConfig.class,
TestSecuredController.class })
public class DefaultSecurityFilterChainIntegrationTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
void givenDefaultSecurityFilterChainConfig_whenCallTheResourceWithAdminRole_thenForbiddenResponseCodeExpected() throws Exception {
MockHttpServletRequestBuilder with = MockMvcRequestBuilders.get("/test-resource")
header("Authorization", basicAuthHeader("admin", "password"));
ResultActions performed = mockMvc.perform(with);
MvcResult mvcResult = performed.andReturn();
assertEquals(403, mvcResult.getResponse().getStatus());
}
}
我們已將用户詳細信息配置、安全配置和控制器 Bean 連接到我們的測試上下文。然後,我們使用管理員用户憑據調用測試資源,並在 Basic Authorization 標頭中發送它們。但是,我們不獲得 200 狀態碼,而是遇到禁止響應碼 403。
如果我們深入瞭解 AuthorityAuthorizationManager 的 hasRole() 方法的工作原理,我們會看到以下代碼:
public static <T> AuthorityAuthorizationManager<T> hasRole(String role) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(!role.startsWith(ROLE_PREFIX), () -> role + " should not start with " + ROLE_PREFIX + " since "
+ ROLE_PREFIX + " is automatically prepended when using hasRole. Consider using hasAuthority instead.");
return hasAuthority(ROLE_PREFIX + role);
}
正如您所看到的,ROLE_PREFIX 在這裏是硬編碼的,所有角色都應包含它才能通過驗證。 當我們使用諸如 @RolesAllowed 這樣的方法安全註解時,我們也會遇到類似的行為。
3. 使用權威代替角色
解決此問題的最簡單方法是使用權威代替角色。權威不需要預期的前綴。如果我們對它們感到滿意,選擇權威可以幫助我們避免與前綴相關的任何問題。
3.1. 基於 SecurityFilterChain 的配置
讓我們修改 user details 在 UserDetailsConfig 類中:
@Configuration
public class UserDetailsConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails admin = User.withUsername("admin")
.password(encoder.encode("password"))
.authorities(Arrays.asList(new SimpleGrantedAuthority("ADMIN"),
new SimpleGrantedAuthority("ADMINISTRATION")))
.build();
return new InMemoryUserDetailsManager(admin);
}
}
我們添加了一個名為 ADMINISTRATION 的權威用於我們的 admin 用户。現在我們將根據權威訪問創建安全配置:
@Configuration
@EnableWebSecurity
public class AuthorityBasedSecurityJavaConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests (authorizeRequests -> authorizeRequests
.requestMatchers("/test-resource").hasAuthority("ADMINISTRATION"))
.httpBasic(withDefaults())
.build();
}
}
在這一配置中,我們實現了相同訪問限制的概念,但使用了 hasAuthority() 方法。現在我們將新的安全配置設置到上下文中,並調用我們的受保護端點:
@WebMvcTest(controllers = TestSecuredController.class)
@ContextConfiguration(classes = { AuthorityBasedSecurityJavaConfig.class, UserDetailsConfig.class,
TestSecuredController.class })
public class AuthorityBasedSecurityFilterChainIntegrationTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
void givenAuthorityBasedSecurityJavaConfig_whenCallTheResourceWithAdminAuthority_thenOkResponseCodeExpected() throws Exception {
MockHttpServletRequestBuilder with = MockMvcRequestBuilders.get("/test-resource")
.header("Authorization", basicAuthHeader("admin", "password"));
ResultActions performed = mockMvc.perform(with);
MvcResult mvcResult = performed.andReturn();
assertEquals(200, mvcResult.getResponse().getStatus());
}
}
正如我們所見,我們可以使用基於權威的安全性配置相同的用户訪問測試資源。
3.2. 基於註解的配置
為了使用基於註解的方法安全性,首先需要啓用方法安全性。 讓我們創建一個具有 @EnableMethodSecurity 註解的安全性配置:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecurityJavaConfig {
}
現在,讓我們在受保護控制器中添加一個端點:
@RestController
public class TestSecuredController {
@PreAuthorize("hasAuthority('ADMINISTRATION')")
@GetMapping("/test-resource-method-security-with-authorities-resource")
public ResponseEntity<String> testAdminAuthority() {
return ResponseEntity.ok("GET request successful");
}
}
在這裏,我們使用了 @PreAuthorize 註解,並指定了期望的權威。準備就緒後,我們可以調用我們的受保護端點:
@WebMvcTest(controllers = TestSecuredController.class)
@ContextConfiguration(classes = { MethodSecurityJavaConfig.class, UserDetailsConfig.class,
TestSecuredController.class })
public class AuthorityBasedMethodSecurityIntegrationTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
void givenMethodSecurityJavaConfig_whenCallTheResourceWithAdminAuthority_thenOkResponseCodeExpected() throws Exception {
MockHttpServletRequestBuilder with = MockMvcRequestBuilders.get("/test-resource-method-security-with-authorities-resource")
.header("Authorization", basicAuthHeader("admin", "password"));
ResultActions performed = mockMvc.perform(with);
MvcResult mvcResult = performed.andReturn();
assertEquals(200, mvcResult.getResponse().getStatus());
}
}
我們附加了 MethodSecurityJavaConfig 和相同的 UserDetailsConfig 到測試上下文中。然後,我們調用了 test-resource-method-security-with-authorities-resource 端點,併成功地訪問了它。
4. 自定義授權管理器用於 SecurityFilterChain
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final Set<String> roles = new HashSet<>();
public CustomAuthorizationManager withRole(String role) {
roles.add(role);
return this;
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
RequestAuthorizationContext object) {
for (GrantedAuthority grantedRole : authentication.get().getAuthorities()) {
if (roles.contains(grantedRole.getAuthority())) {
return new AuthorizationDecision(true);
}
}
return new AuthorizationDecision(false);
}
}
我們實現了 AuthorizationManager 接口。 在我們的實現中,我們可以指定多個角色,以便調用可以傳遞權限驗證。 在 check() 方法中,我們正在驗證身份驗證中獲得的權限是否包含在我們的預期角色集中。 現在,讓我們將我們的自定義授權管理器附加到 SecurityFilterChain: @WebMvcTest(controllers = TestSecuredController.class)
@ContextConfiguration(classes = { CustomAuthorizationManagerSecurityJavaConfig.class,
TestSecuredController.class, UserDetailsConfig.class })
public class RemovingRolePrefixIntegrationTest {
@Autowired
WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
public void givenCustomAuthorizationManagerSecurityJavaConfig_whenCallTheResourceWithAdminRole_thenOkResponseCodeExpected() throws Exception {
MockHttpServletRequestBuilder with = MockMvcRequestBuilders.get("/test-resource")
.header("Authorization", basicAuthHeader("admin", "password"));
ResultActions performed = mockMvc.perform(with);
MvcResult mvcResult = performed.andReturn();
assertEquals(200, mvcResult.getResponse().getStatus());
}
}
我們已附加我們的 CustomAuthorizationManagerSecurityJavaConfig 並調用 test-resource 端點。 就像預期的那樣,我們收到了 200 響應代碼。 在基於註解的方法中,我們可以覆蓋我們使用角色的前綴。 讓我們修改我們的 MethodSecurityJavaConfig: 我們添加了 GrantedAuthorityDefaults 豆子,並將一個空字符串作為構造函數參數傳遞。這個空字符串將被用作默認角色前綴。 對於此測試用例,我們將創建一個新的受保護端點: 我們添加了 @RolesAllowed({“ADMIN”}) 到此端點,因此只有具有 ADMIN 角色的用户才能訪問它。 讓我們調用它並查看響應是什麼: 我們成功地檢索了 200 響應代碼,調用 test-resource-method-security-resource 對於具有 ADMIN 角色的用户,沒有任何前綴。 在本文中,我們探討了避免在 Spring Security 中出現 ROLE_ 前綴問題的各種方法。 某些方法需要自定義,而另一些方法則使用默認功能。 本文所述的方法可以幫助我們避免在用户詳細信息中添加前綴,這在某些情況下可能無法實現。@Configuration
@EnableWebSecurity
public class CustomAuthorizationManagerSecurityJavaConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests (authorizeRequests -> {
hasRole(authorizeRequests.requestMatchers("/test-resource"), "ADMIN");
})
.httpBasic(withDefaults());
return http.build();
}
private void hasRole(AuthorizeHttpRequestsConfigurer.AuthorizedUrl authorizedUrl, String role) {
authorizedUrl.access(new CustomAuthorizationManager().withRole(role));
}
}5. 覆蓋 GrantedAuthorityDefaults 用於方法安全
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecurityJavaConfig {
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("");
}
}@RestController
public class TestSecuredController {
@RolesAllowed({"ADMIN"})
@GetMapping("/test-resource-method-security-resource")
public ResponseEntity<String> testAdminRole() {
return ResponseEntity.ok("GET request successful");
}
}
@WebMvcTest(controllers = TestSecuredController.class)
@ContextConfiguration(classes = { MethodSecurityJavaConfig.class, UserDetailsConfig.class,
TestSecuredController.class })
public class RemovingRolePrefixMethodSecurityIntegrationTest {
@Autowired
WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
@Test
public void givenMethodSecurityJavaConfig_whenCallTheResourceWithAdminRole_thenOkResponseCodeExpected() throws Exception {
MockHttpServletRequestBuilder with = MockMvcRequestBuilders.get("/test-resource-method-security-resource")
.header("Authorization", basicAuthHeader("admin", "password"));
ResultActions performed = mockMvc.perform(with);
MvcResult mvcResult = performed.andReturn();
assertEquals(200, mvcResult.getResponse().getStatus());
}
}6. 結論