使用自定义身份验证修复 React-Spring 应用程序中的 401 未经授权的 Spring 安全错误

使用自定义身份验证修复 React-Spring 应用程序中的 401 未经授权的 Spring 安全错误
使用自定义身份验证修复 React-Spring 应用程序中的 401 未经授权的 Spring 安全错误

调试自定义登录实现中 Spring Security 的身份验证问题

在 Spring Security 项目中遇到 401 Unauthorized 错误可能会令人沮丧,尤其是当登录配置似乎设置正确时。 😣 许多开发人员在 Spring Security 默认值之外实现自定义登录页面时,在尝试保护应用程序的后端资源时会遇到此问题。

当像 React 这样的前端框架管理登录页面并与后端通信,绕过 Spring Security 基于表单的登录设置时,可能会出现此问题。在此类设置中,Spring Security 可能无法识别经过身份验证的会话,从而导致在您尝试使用受保护资源时拒绝访问。

在本文中,我们将深入探讨看似成功登录后出现未经授权的访问错误背后的常见原因。通过了解 Spring 的 SecurityContext 和会话管理的作用,您将清楚地了解如何在自定义设置中解决此问题。

让我们探索实用的策略,以确保您的身份验证逻辑始终设置正确的会话状态,从而实现跨应用程序的平滑、授权访问。 🚀

命令 使用示例
sessionManagement 此命令配置 Spring Security 如何处理 HTTP 会话。使用 session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) 可确保每个请求都经过单独身份验证,这对于基于 REST 的令牌身份验证设置中经常使用的无状态 API 至关重要。
OncePerRequestFilter OncePerRequestFilter 是一个 Spring Security 过滤器,保证每个请求单次执行。它用于自定义身份验证过滤器,以确保身份验证逻辑对每个请求一致应用,没有冗余。
SecurityContextHolder 通过调用 SecurityContextHolder.getContext().setAuthentication(authentication),此命令在安全上下文中设置经过身份验证的用户的详细信息,确保 Spring Security 将用户识别为当前会话的经过身份验证的用户。
DaoAuthenticationProvider 此命令 new DaoAuthenticationProvider() 使用特定的用户详细信息服务和密码编码器设置身份验证,允许基于用户数据库进行自定义验证并确保安全的密码处理。
MockMvcRequestBuilders.post 单元测试中的此命令模拟 HTTP POST 请求,如mockMvc.perform(MockMvcRequestBuilders.post("/login")) 中所示。它可以通过将 HTTP 请求直接发送到控制器的端点来测试 Spring MVC 控制器。
authorizeHttpRequests 此命令指定哪些请求需要身份验证以及哪些请求可以公开访问。 authorize.requestMatchers("/user/login").permitAll() 允许无需凭据即可访问登录和注册端点。
TokenProvider 像 TokenProvider 这样的自定义类用于生成和管理 JWT 令牌。此类封装了令牌创建逻辑,以确保模块化、可重用且安全的令牌处理,这对于基于令牌的身份验证至关重要。
csrf().disable() Disabling CSRF is critical in stateless API configurations, particularly for REST APIs without session-based login. csrf(csrf ->禁用 CSRF 在无状态 API 配置中至关重要,特别是对于没有基于会话登录的 REST API。 csrf(csrf -> csrf.disable()) 对于使用基于令牌的身份验证的应用程序通常是必需的,因为在这种情况下不需要 CSRF 保护。
requestMatchers 此命令过滤哪些端点与特定安全规则匹配,例如authorize.requestMatchers(“/user/register”)。它在这里用于从身份验证要求中排除注册和登录端点。
usernamePasswordAuthenticationToken 该命令在自定义身份验证过程中至关重要。 new UsernamePasswordAuthenticationToken() 使用提供的凭据创建身份验证令牌,允许身份验证管理器根据存储的用户详细信息验证这些凭据。

了解自定义登录身份验证的 Spring Security 配置

在提供的脚本中,我们看到一个用于处理的自定义配置 验证 在 Spring Security 中,不使用其默认登录表单。通过创建一个单独的 安全过滤链 配置中,我们可以控制哪些端点受到保护以及 Spring 如何管理会话。该配置禁用 CSRF(跨站点请求伪造)保护,这在 REST API 中很常见,因为前端框架(如 React)使用安全的、基于令牌的请求进行通信。这里,命令authorizeHttpRequests是关键;它确保特定的 URL(例如“/user/login”和“/user/register”)对所有用户开放,同时将其他请求(例如访问受保护的资源)限制为仅对经过身份验证的用户开放。

我们还使用 SessionCreationPolicy.IF_REQUIRED 设置会话创建策略,仅在必要时才允许创建会话。此方法适合某些请求可能依赖于基于会话的身份验证,但其他请求(如具有令牌的请求)则不依赖的应用程序。例如,如果用户通过 React 前端登录并期望持久访问资源,则此会话策略可确保用户在应用程序中切换路由时不会面临重复注销。它对于处理会话和无状态需求特别有帮助,具体取决于客户端(React 应用程序)与后端 API 的交互方式。

该服务类包括一个名为authenticateUser 的方法,其中AuthenticationManager bean 发挥作用。该bean 配置有DaoAuthenticationProvider 和PasswordEncoder,这对于根据数据库验证用户凭据至关重要。该方法使用 UsernamePasswordAuthenticationToken 调用authenticationManager.authenticate,尝试根据提供的用户名和密码进行身份验证。如果成功,Spring Security 的 SecurityContextHolder 将保存此经过身份验证的用户的会话。这样,当前端发出另一个请求时,Spring 可以检索用户的身份验证状态,而无需重新验证。

然而,尽管如此设置,如果会话或令牌未正确维护,可能会出现接收 401 未经授权错误等问题。例如,当使用具有无状态会话的 REST API 时,如果服务器不保留请求之间的身份验证,则此设置可能会失败。为了解决这个问题,我们可以实现基于令牌的身份验证,其中生成的令牌在登录后附加到每个请求标头,使会话独立于服务器。在测试环境中,MockMvcRequestBuilders允许开发人员模拟请求并确认登录端点正确返回授权令牌。然后可以在进一步的请求中使用该令牌,允许 React 前端访问安全端点而无需重新进行身份验证,从而提供更流畅的用户体验。 🔐

解决方案 1:更新无状态会话管理的 Spring Security 配置

此方法使用 Spring Security 的无状态会话策略来解决 REST API 上下文中的会话管理,该上下文针对 React 等单页应用程序 (SPA) 进行了优化。在这里,我们调整 SecurityFilterChain 配置以匹配 REST API 无状态模型。

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
        .csrf(csrf -> csrf.disable())  // Disable CSRF for REST APIs
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/user/register", "/user/login").permitAll()
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults())
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .build();
}

解决方案 2:基于令牌的身份验证的自定义身份验证过滤器

在此解决方案中,自定义过滤器对用户进行身份验证并在响应标头中附加令牌。该过滤器使用基于令牌的身份验证,非常适合 RESTful 应用程序,并且可以与 React 无缝协作。

@Component
public class CustomAuthFilter extends OncePerRequestFilter {
    private final AuthenticationManager authenticationManager;
    public CustomAuthFilter(AuthenticationManager authManager) {
        this.authenticationManager = authManager;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
                                    throws ServletException, IOException {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            Authentication authentication = new UsernamePasswordAuthenticationToken(token, null);
            Authentication authResult = authenticationManager.authenticate(authentication);
            SecurityContextHolder.getContext().setAuthentication(authResult);
        }
        filterChain.doFilter(request, response);
    }
}

解决方案3:服务类调整和令牌响应

该服务实现在成功登录时发送 JWT 令牌,使用模块化设计来确保每个功能在整个应用程序中都是可测试和可重用的。

@Service
public class AuthenticationService {
    private final AuthenticationManager authenticationManager;
    private final TokenProvider tokenProvider; // Custom class for generating JWTs
    public AuthenticationService(AuthenticationManager authenticationManager,
                                 TokenProvider tokenProvider) {
        this.authenticationManager = authenticationManager;
        this.tokenProvider = tokenProvider;
    }
    public String authenticateAndGenerateToken(LoginDTO loginDTO) {
        Authentication authentication = authenticationManager
            .authenticate(new UsernamePasswordAuthenticationToken(loginDTO.getUserName(),
                                                                loginDTO.getPassword()));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        return tokenProvider.createToken(authentication);
    }
}

令牌生成和身份验证的单元测试

此 JUnit 测试可确保身份验证和令牌生成正常工作,并验证访问安全资源的身份验证。

@SpringBootTest
@AutoConfigureMockMvc
public class AuthenticationServiceTest {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private AuthenticationService authenticationService;
    @Test
    public void testAuthenticateAndGenerateToken() throws Exception {
        LoginDTO loginDTO = new LoginDTO("user", "password");
        String token = authenticationService.authenticateAndGenerateToken(loginDTO);
        mockMvc.perform(MockMvcRequestBuilders.post("/login")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\\"userName\\":\\"user\\", \\"password\\":\\"password\\"}"))
                .andExpect(status().isOk())
                .andExpect(header().exists("Authorization"))
                .andExpect(content().string("Successfully Authenticated"));
    }
}

克服无状态 Spring Security 应用程序中的会话挑战

在以下情况下 春季安全 配置为无状态 API 通信时,会话管理可能会很棘手,尤其是在使用自定义登录流程时。无状态配置意味着每个请求理想情况下都应该携带自己的身份验证令牌,服务器独立于之前的请求进行验证。这与传统的基于会话的设置不同,在传统的设置中,用户登录一次,其会话就保留在服务器上。由于 React 前端通常用于处理身份验证并通过 REST API 发送登录请求,因此集成需要确保每个 API 请求都经过身份验证,通常使用 JWT 等令牌。

当 Spring Security 的默认会话管理被自定义配置取代时,了解如何在 Spring Security 中设置和维护用户身份验证至关重要 安全上下文持有者。解决此问题的一种方法是使用自定义身份验证过滤器来验证请求标头中包含的令牌,而不是依赖于会话。如果您的应用程序需要重复的用户识别而不需要会话持久性,您可能需要将令牌本地存储在前端并将其包含在每个请求的标头中。这消除了服务器跟踪会话状态的需要,与安全高效的 RESTful API 的无状态设计模型保持一致。

此外,实现注销功能是无状态应用程序中需要考虑的另一个方面。由于服务器上不存在会话,注销通常涉及从客户端删除令牌。在这种情况下,只需丢弃客户端本地存储上的令牌并拒绝使用服务器上的令牌的请求即可成功注销。此方法通过防止未经授权的访问而无需服务器端会话处理,从而支持更高的安全级别。最终,这种配置非常适合优先考虑可扩展性和安全性的应用程序,特别是与像 React 这样可以有效管理令牌存储的前端框架配合使用时。 🚀

有关 Spring Security 自定义身份验证问题的常见问题

  1. 为什么我设置了之后仍然收到 401 Unauthorized 错误 SecurityContextHolder
  2. 如果身份验证上下文不持久,通常会出现 401 错误。如果您的应用程序是无状态的,请确保您使用基于令牌的身份验证。
  3. 如何在 Spring Security 中启用无状态会话管理?
  4. SessionCreationPolicy.STATELESS 在你的 SecurityFilterChain 确保每个请求都经过独立验证。
  5. 的作用是什么 DaoAuthenticationProvider 在自定义身份验证中?
  6. DaoAuthenticationProvider 根据数据库验证用户凭据并对密码进行编码以进行安全身份验证。
  7. 我可以在 Spring Security 中使用 JWT 令牌进行会话管理吗?
  8. 是的,JWT 令牌非常适合无状态应用程序。身份验证后生成令牌并将其包含在后续请求的标头中。
  9. CSRF 保护如何影响无状态 API?
  10. CSRF 保护通常在无状态 API 中禁用,使用 csrf().disable() 因为对于没有会话的 API 来说这是不必要的。
  11. 如果我想允许公共访问某些端点(例如登录或注册)怎么办?
  12. 使用 authorizeHttpRequests 并指定无需身份验证即可访问的端点 permitAll()
  13. 如何使用 React 在客户端存储令牌?
  14. 将令牌存储在 localStorage 或者 sessionStorage,然后将它们包含在每个请求的标头中,以确保后端可以验证每个请求。
  15. 禁用 API 的 CSRF 安全吗?
  16. 如果您的应用程序依赖于令牌或不使用 cookie,则禁用 API 的 CSRF 是安全的,因为 CSRF 主要防止基于 cookie 的攻击。
  17. 的作用是什么 OncePerRequestFilter 在自定义身份验证中?
  18. 该过滤器每个请求仅执行一次,确保身份验证逻辑一致应用,而无需在请求周期中进行冗余检查。
  19. 为什么不同端点可能无法识别我的身份验证令牌?
  20. 确保在每个请求的标头中设置令牌,并使用一致的令牌验证过程确认它在服务器上正确验证。
  21. 如何测试我的 Spring Security 配置?
  22. 使用 MockMvc 在测试中模拟请求,检查身份验证响应,并验证受保护的端点只能在登录后访问。

关于解决自定义 Spring Security 身份验证中的 401 错误的最终想法

使用自定义登录页面成功保护基于 Spring 的应用程序需要仔细配置,特别是在使用无状态会话或基于令牌的方法时。与 React 前端集成时,确保您的安全配置符合 RESTful、无状态原则可以帮助避免会话问题。

从修改 安全过滤链 设置来实现基于令牌的流程,每种方法都在创建可靠的身份验证设置中发挥着作用。通过了解会话管理、令牌处理和 SecurityContext,您将能够很好地解决 Spring Security 应用程序中的 401 未经授权错误。 🔒

在 Spring Security 中实现自定义身份验证的资源和参考
  1. 有关 Spring Security 配置和会话管理的全面详细信息,请参阅 Spring Security 官方文档
  2. 要了解并使用 React 前端实现自定义身份验证流程,请参阅以下指南: Spring Security 和 React 登录教程
  3. 本文的配置示例和 Spring Boot 设置基于以下见解 Baeldung 的 Spring 安全会议指南