1. 概述

在这个教程中,我们将学习Spring Security框架中的HttpSecurity类的permitAll()anonymous()方法。Spring Security框架有助于防止漏洞攻击,并为Web应用提供身份验证和授权功能。通过它,Web应用可以控制对服务器资源(如HTML表单、CSS文件、JS文件、Web服务端点等)的访问。它还支持基于角色的访问控制(RBAC),以便用户访问服务器资源。

在Web应用中,总是有些部分只有经过身份验证的用户才能访问,但也有一些部分不需要用户身份验证。有趣的是,有时甚至有已认证用户无法访问某些服务器资源的情况。我们将简要讨论这些情况,并了解如何使用permitAll()anonymous()方法配合Spring Security表达式定义这类安全访问。

2. 安全需求

在继续之前,让我们设想一个电子商务网站,其需求如下:

  • 匿名用户和已认证用户都可以查看网站上的产品
  • 记录匿名用户和已认证用户的请求日志
  • 匿名用户可以访问用户注册表单,而已认证用户不能
  • 只有已认证用户才能查看他们的购物车

3. 控制器和WebSecurity配置

首先,我们定义一个控制器类,包含电子商务网站的端点:

@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 {
        return http.addFilterAfter(new AuditInterceptor(), AnonymousAuthenticationFilter.class)
            .authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/private/**"))
                .authenticated())
            .httpBasic(Customizer.withDefaults())
            .authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/public/showProducts"))
                .permitAll())
            .authorizeHttpRequests(request -> request.requestMatchers(new AntPathRequestMatcher("/public/registerUser"))
                .anonymous())
            .build();
    }
    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

基本上,我们定义了以下内容:

  • 匿名身份验证过滤器之后,添加了一个审计拦截器(AuditInterceptor),用于记录匿名和已认证用户发出的请求
  • 用户必须强制进行身份验证才能访问路径为*/private/*的URL
  • 所有用户都可以访问路径/public/showProducts**
  • 只有匿名用户可以访问路径/public/registerUser**

我们还配置了一个名为spring的用户,将在整篇文章中调用EcommerceController中定义的Web服务端点。

4. HttpSecurity中的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文件)方面非常高效。而且,我们应始终优先选择permitAll()而不是忽略静态资源在Spring Security过滤链中,因为过滤链将无法为被忽略的静态资源设置安全头。

5. HttpSecurity中的anonymous()方法

在开始实现电子商务网站的需求之前,理解anonymous()表达式的概念至关重要。

遵循Spring Security的原则,我们需要为所有用户定义权限和限制,包括匿名用户。因此,他们关联着ROLE_ANONYMOUS角色。

5.1. 实现AuditInterceptor

Spring Security在匿名身份验证过滤器中填充了匿名用户的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. 否定已认证用户访问注册用户界面

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

上述方法成功地拒绝了已认证用户访问用户注册页面。

permitAll()方法不同,anonymous()也可以在不需要身份验证的情况下为静态资源提供服务。

6. 总结

通过示例,本教程演示了permitAll()anonymous()方法之间的区别。

anonymous()用于公开内容,只允许匿名用户访问。而permitAll()则用于允许所有用户无差别的访问特定URL,无需区分他们的身份验证状态。

最后,示例代码可以在GitHub上找到。