Spring Security 中 permitAll() 和 anonymous() 的区别

1、概览

Web 应用中,有些资源只能被已登录(认证)的的用户访问。有些资源,可以被匿名用户访问。而有些资源,甚至只能被匿名用户访问,已登录的用户不能访问。

本文将带你了解 Spring Security 中 HttpSecuritypermitAll()anonymous() 方法之间的区别以及如何通过这两个方法实现上述的权限设计。

2、权限设计

假如,我们有一个电商网站,权限设计如下:

  • 匿名用户和已登录的用户均可查看网站上的商品。
  • 需要审计匿名用户和已登录用户请求。
  • 匿名用户可以访问用户注册页面,已经登录的用户则不能访问。
  • 只有已登录的用户才能查看其购物车。

3、Controller 和 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 方法

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

在实现电商网站的安全需求前,先了解一下 anonymous() 表达式背后的理念。

根据 Spring 安全原则,需要为所有用户定义权限和限制。这也适用于匿名用户,他们与 ROLE_ANONYMOUS 关联。

5.1、实现 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、拒绝已认证用户访问注册界面

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、总结

在本教程中,我们通过示例演示了 permitAll()anonymous() 方法的区别。

anonymous() 用于只允许匿名用户访问的公开内容。permitAll() 用于允许所有用户访问特定 URL。


参考:https://www.baeldung.com/spring-security-permitall-vs-anonymous