1. 概述

我们创建了一个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头发送认证信息。但实际返回的是403 Forbidden状态码,而非预期的200。

深入分析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等方法安全注解时也会遇到类似问题。

2. 使用权限(Authorities)替代角色(Roles)

最简单的解决方案是使用权限(authorities)替代角色(roles)。权限不需要强制前缀,如果习惯使用权限,可以完全避开前缀问题。

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

使用相同的用户和基于权限的安全配置,成功访问了测试接口。

2.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加入测试上下文,成功访问了接口。

3. 为SecurityFilterChain自定义授权管理器

**如果必须使用不带ROLE_前缀的角色,需要为SecurityFilterChain配置自定义的AuthorizationManager**。这个自定义管理器不会硬编码前缀。

创建实现类:

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状态码。

4. 覆盖方法安全中的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");
    }
}

添加@RolesAllowed({"ADMIN"})注解,只有拥有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());
    }
}

成功访问test-resource-method-security-resource接口,用户使用无前缀的ADMIN角色获得200响应。

5. 总结

本文探讨了在Spring Security中避免ROLE_前缀问题的多种方案:

权限替代角色:直接使用authorities避开前缀要求
自定义授权管理器:通过实现AuthorizationManager完全控制验证逻辑
覆盖默认前缀:在方法安全中通过GrantedAuthorityDefaults修改前缀

这些方案各有适用场景,能帮助我们在无法修改角色定义时(如集成外部系统)优雅处理前缀问题。根据实际需求选择最合适的方案,可以避免很多不必要的踩坑。

完整源码可在GitHub仓库获取。


原始标题:Removing ROLE_ Prefix in Spring Security | Baeldung