知識庫 / Spring / Spring Security RSS 訂閱

移除 Spring Security 中的 ROLE_ 前綴

Spring Security
HongKong
4
11:04 AM · Dec 06 ,2025

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();
    }
}
</div>
</div>
<p>我們創建了 <em >UserDetailsConfig</em> 配置類,該類生成一個 <a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/in-memory.html"><em >InMemoryUserDetailsManager</em></a > Bean。在工廠方法中,我們使用了 <em >PasswordEncoder</em>,用於生成用户詳細信息的密碼。</p>
<p>接下來,我們將添加我們想要調用的端點:</p>
@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 標頭中。 但我們收到的是 403 響應代碼,而不是 200 響應代碼。

如果深入瞭解 AuthorityAuthorizationManagerhasRole() 方法的工作原理,我們將看到以下代碼:

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 的配置

讓我們修改 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 的權限,用於我們的管理用户。現在我們將根據權限訪問創建安全配置:

@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();
    }
}

在本次配置中,我們實現了相同的訪問限制概念,但使用了 AuthorityAuthorizationManager.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 註解,並指定了 hasAuthority 屬性,以明確我們的預期權限。準備就緒後,我們可以調用我們的安全端點:

@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

如果需要使用不帶 ROLE_ 前綴的角色,則必須將一個自定義的 AuthorizationManager 附加到 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

@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));
    }
}

與其使用 AuthorityAuthorizationManager.hasRole() 方法,我們現在使用 AuthorizeHttpRequestsConfigurer.access(),這允許我們使用自定義的 AuthorizationManager 實現。

現在,讓我們配置測試上下文並調用受保護的端點:

@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 響應代碼。

5. 覆蓋方法安全中的 GrantedAuthorityDefaults

在基於註解的方法中,我們可以覆蓋用於我們角色的前綴。

讓我們修改我們的 MethodSecurityJavaConfig

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecurityJavaConfig {
    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("");
    }
}

我們已添加了 GrantedAuthorityDefaults Bean,並將空字符串作為構造函數參數傳遞。 此空字符串將用作默認角色前綴。

對於此測試用例,我們將創建一個新的安全端點:

@RestController
public class TestSecuredController {

    @RolesAllowed({"ADMIN"})
    @GetMapping("/test-resource-method-security-resource")
    public ResponseEntity<String> testAdminRole() {
        return ResponseEntity.ok("GET request successful");
    }
}

我們已將 ({"ADMIN"}) 添加到此端點,確保只有擁有 <em data-role="ADMIN" 角色的用户才能訪問它。

讓我們調用一下,看看響應結果如何:

@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());
    }
}

我們已成功檢索到 200 響應代碼,調用了 test-resource-method-security-resource,且未添加任何前綴,針對擁有 ADMIN 角色的用户。

6. 結論

在本文中,我們探討了避免 Spring Security 中 ROLE_ 前綴問題的一些方法。 某些方法需要自定義,而另一些方法則利用默認功能。 本文所述的方法可以幫助我們避免在用户詳情中添加前綴,這在某些情況下可能無法實現。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.