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仓库获取。