1、概览 {#1概览}
Web 应用中,有些资源只能被已登录(认证)的的用户访问。有些资源,可以被匿名用户访问。而有些资源,甚至只能被匿名用户访问,已登录的用户不能访问。
本文将带你了解 Spring Security 中 HttpSecurity
的 permitAll()
和 anonymous()
方法之间的区别以及如何通过这两个方法实现上述的权限设计。
2、权限设计 {#2权限设计}
假如,我们有一个电商网站,权限设计如下:
- 匿名用户和已登录的用户均可查看网站上的商品。
- 需要审计匿名用户和已登录用户请求。
- 匿名用户可以访问用户注册页面,已经登录的用户则不能访问。
- 只有已登录的用户才能查看其购物车。
3、Controller 和 WebSecurity 配置 {#3controller-和-websecurity-配置}
定义 Controller
:
@RestController
public class EcommerceController {
@GetMapping("/private/showCart")
public @ResponseBody String showCart() {
return "Show Cart";
}
@GetMapping("/public/showProducts")
public @ResponseBody String listProducts() {
return "List Products";
}
@GetMapping("/public/registerUser")
public @ResponseBody String registerUser() {
return "Register User";
}
}
接下来在 EcommerceWebSecruityConfig
类中实现上述权限设计:
@Configuration
@EnableWebSecurity
public class EcommerceWebSecurityConfig {
@Bean
public InMemoryUserDetailsManager userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.withUsername("spring")
.password(passwordEncoder.encode("secret"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterAfter(new AuditInterceptor(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/private/**").authenticated().and().httpBasic()
.and().authorizeRequests()
.antMatchers("/public/showProducts").permitAll()
.antMatchers("/public/registerUser").anonymous();
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
如上:
- 在 AnonymousAuthenticationFilter 之后添加一个
AuditInterceptor
Filter,用于记录匿名用户和已登录用户发起的请求。 - 已登录用户才能访问
/private
路径。 - 所有用户都可以访问
/public/showProducts
路径 - 只有匿名用户才能访问
/public/registerUser
路径
还配置了一个 username 为 spring
的用户,后文中,将会使用它来调用 EcommerceController
中的服务。
4、HttpSecurity
中的 permitAll
方法 {#4httpsecurity-中的-permitall-方法}
在 EcommerceWebSecurityConfig
中,我们通过 permitAll() 开放了 /public/showProducts
端点:
测试如下:
@WithMockUser(username = "spring", password = "secret")
@Test
public void givenAuthenticatedUser_whenAccessToProductLinePage_thenAllowAccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/public/showProducts"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("List Products"));
}
@WithAnonymousUser
@Test
public void givenAnonymousUser_whenAccessToProductLinePage_thenAllowAccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/public/showProducts"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("List Products"));
}
匿名用户和已登录用户都可以访问该页面。
在 Spring Security 6 中,permitAll()
可以有效地保护 JS 和 CSS 文件等静态资源的安全。在 Spring Security Filter Chain 中,应始终 优先选择 permitAll()
而不是忽略静态资源,因为 Filter Chain 无法在被忽略的静态资源上设置 Security Header。
5、HttpSecurity
中的 anonymous()
{#5httpsecurity-中的-anonymous}
在实现电商网站的安全需求前,先了解一下 anonymous()
表达式背后的理念。
根据 Spring 安全原则,需要为所有用户定义权限和限制。这也适用于匿名用户,他们与 ROLE_ANONYMOUS
关联。
5.1、实现 AuditInterceptor
{#51实现-auditinterceptor}
Spring Security 在 AnonymousAuthenticationFilter
中填充了匿名用户的 Authentication
对象,这样就可以通过拦截器就对匿名用户的操作进行审计。
下面是上述 EcommerceWebSecurityConfig
类中配置的 AuditInterceptor
的概要:
public class AuditInterceptor extends OncePerRequestFilter {
private final Logger logger = LoggerFactory.getLogger(AuditInterceptor.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof AnonymousAuthenticationToken) {
logger.info("Audit anonymous user");
}
if (authentication instanceof UsernamePasswordAuthenticationToken) {
logger.info("Audit registered user");
}
filterChain.doFilter(request, response);
}
}
即使是匿名用户,Authentication
也不会为 null
。这使得 AuditInterceptor
的实现非常强大。它为匿名用户和已登录用户提供了独立的审计流程。
5.2、拒绝已认证用户访问注册界面 {#52拒绝已认证用户访问注册界面}
在 EcommerceWebSecurityConfig
类中,通过 anonymous()
方法确保只有匿名用户才能访问 public/registerUser
,而已登录的用户则无法访问。
测试匿名用户访问,是否生效:
@WithAnonymousUser
@Test
public void givenAnonymousUser_whenAccessToUserRegisterPage_thenAllowAccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/public/registerUser"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("Register User"));
}
上述测试结果,匿名用户可以访问注册页面。
接着,测试已登录用户是否可以访问注册页面。
@WithMockUser(username = "spring", password = "secret")
@Test
public void givenAuthenticatedUser_whenAccessToUserRegisterPage_thenDenyAccess() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/public/registerUser"))
.andExpect(MockMvcResultMatchers.status().isForbidden());
}
测试结果,已登录用户访问注册页面被拒绝了。
anonymous()
方法也可以用于不需要认证的就能访问的静态资源服务。
6、总结 {#6总结}
在本教程中,我们通过示例演示了 permitAll()
和 anonymous(
) 方法的区别。
anonymous()
用于只允许匿名用户访问的公开内容。permitAll()
用于允许所有用户访问特定 URL。
参考:https://www.baeldung.com/spring-security-permitall-vs-anonymous