Spring Security登录后仍然收到403错误

问题描述 投票:0回答:1

我正在学习 Spring,并且我正在学习使用 Spring Security 进行传统的用户/密码身份验证。

目前,我正在使用自己的登录页面。在我的控制器中,我可以使用我的 userService 验证用户凭据。 LoginController 片段:

/**
     * Displays the login page.
     * <p>
     * This method is invoked when a user requests the login page. It initializes
     * the login form and adds it to the model.
     * </p>
     *
     * @param model the model to be used in the view.
     * @return the name of the login view (Thymeleaf template).
     */
    @GetMapping("/login")
    public String loginGet(Model model) {
        log.info("loginGet: Get login page");
        model.addAttribute("loginForm", new LoginForm());
        return "login";
    }

    /**
     * Processes the login form submission.
     * <p>
     * This method handles POST requests when a user submits the login form. It checks
     * the validity of the submitted form and validates the user's credentials.
     * On success, it redirects to the search page; on failure, it reloads the login page with an error.
     * </p>
     *
     * @param loginForm the login form submitted by the user.
     * @param result    the result of the form validation.
     * @param attrs     attributes to be passed to the redirect.
     * @param httpSession the HTTP session for storing the authenticated user.
     * @param model     the model to add error messages, if necessary.
     * @return the name of the view to render.
     */
    @PostMapping("/login")
    public String loginPost(@Valid @ModelAttribute LoginForm loginForm, BindingResult result,
                            RedirectAttributes attrs, HttpSession httpSession, Model model) {
        log.info("loginPost: User '{}' attempted login", loginForm.getUsername());

        // Check for validation errors in the form submission
        if (result.hasErrors()) {
            log.info("loginPost: Validation errors: {}", result.getAllErrors());
            return "login";
        }

        // Validate the username and password
        if (!loginService.validateUser(loginForm.getUsername(), loginForm.getPassword())) {
            log.info("loginPost: Username and password don't match for user '{}'", loginForm.getUsername());
            model.addAttribute("errorMessage", "That username and password don't match.");
            return "login";  // Reload the form with an error message
        }

        // If validation is successful, retrieve the user and set the session
        User foundUser = userService.getUser(loginForm.getUsername());
        attrs.addAttribute("username", foundUser.getUsername());
        httpSession.setAttribute("currentUser", foundUser);

        log.info("loginPost: User '{}' logged in", foundUser.getUsername());

        return "redirect:/search";  // Redirect to the search page after successful login
    }

但是,当用户重定向到搜索页面时,会产生 403 错误,因为用户无权访问搜索页面。我以为我的 securityConfig 类设置正确,未登录的用户可以访问登录和注册页面,但所有其他页面只能由登录的用户查看。

安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(final CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * Bean for password encoding using BCrypt.
     *
     * @return a BCryptPasswordEncoder instance for encoding passwords.
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * Configures the security filter chain for the application.
     *
     * @param http the HttpSecurity object to configure.
     * @return the configured SecurityFilterChain.
     * @throws Exception if an error occurs while configuring the security settings.
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers(
                        "/",
                        "/login",
                        "/register",
                        "/js/**",
                        "/css/**",
                        "/images/**").permitAll()
                .anyRequest().authenticated());
        http.logout(lOut -> {
            lOut.invalidateHttpSession(true)
                    .clearAuthentication(true)
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/login?logout")
                    .permitAll();
        });
        http.csrf().disable();

        return http.build();
    }

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationManagerBuilder.authenticationProvider(authenticationProvider);
        return authenticationManagerBuilder.build();
    }
}

自定义用户详细信息服务:

@Service
public class CustomUserDetailsService implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(CustomUserDetailsService.class);


    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("loadUserByUsername: username={}", username);
        final List<User> user = userRepository.findByUsernameIgnoreCase(username);
        if(user.size() != 1) {
            throw new UsernameNotFoundException("User not found");
        }
        return new CustomUserDetails(user.getFirst());
    }
}

自定义用户详细信息:

public record CustomUserDetails(User user) implements UserDetails {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

    @Override
    public String getPassword() {
        return user.getHashedPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

我尝试了几种不同的方法,但都导致了 403 错误。我需要令牌吗?如何允许用户在登录后访问我的所有其他页面?

用户输入有效凭据并尝试登录后记录消息:

[DEBUG] - from org.springframework.security.web.FilterChainProxy in http-nio-8080-exec-4
Securing POST /login

[DEBUG] - from org.springframework.security.web.FilterChainProxy in http-nio-8080-exec-4
Secured POST /login

[INFO] - from edu.school.appName.web.controller.LoginController in http-nio-8080-exec-4
loginPost: User 'testUser' attempted login

[INFO] - from edu.school.appName.service.LoginServiceImpl in http-nio-8080-exec-4
validateUser: Attempting to validate user 'testUser' for login

[INFO] -
from edu.school.appName.service.LoginServiceImpl in http-nio-8080-exec-4
validateUser: User 'testUser' found

[INFO] - from edu.school.appName.service.LoginServiceImpl in http-nio-8080-exec-4
validateUser: Successful login for 'testUser'

[INFO] - from edu.school.appName.web.controller.LoginController in http-nio-8080-exec-4
loginPost: User 'testUser' logged in

[DEBUG] - from org.springframework.security.web.authentication.AnonymousAuthenticationFilter in http-nio-8080-exec-4
Set SecurityContextHolder to anonymous SecurityContext

[DEBUG ]- from org.springframework.security.web.FilterChainProxy in http-nio-8080-exec-5
Securing GET /search?username=testUser

[DEBUG] - from org.springframework.security.web.authentication.AnonymousAuthenticationFilter in http-nio-8080-ехес-5
Set SecurityContextHolder to anonymous SecurityContext

[DEBUG] - from org.springframework.security.web.savedrequest.HttpSessionRequestCache in http-nio-8080-exec-5
Saved request http://localhost:8080/search?username=testUser& continue to session

[DEBUG] - from org.springframework.security.web.authentication.Http403ForbiddenEntryPoint in http-nio-8080-eхес-5
Pre-authenticated entry point called. Rejecting access

用于日志中上下文的 validateUser 函数:

/**
     * Validates the provided username and password by checking the user data stored in the repository.
     *
     * @param username the username provided by the user during login
     * @param password the password provided by the user during login
     * @return true if the user exists and the password matches; false otherwise
     */
    @Override
    public boolean validateUser(String username, String password) {
        log.info("validateUser: Attempting to validate user '{}' for login", username);

        // Perform a case-insensitive search for the user in the repository.
        List<User> users = userRepo.findByUsernameIgnoreCase(username);

        // Check if exactly one user is found. If zero or more than one are found, return false.
        if (users.size() != 1) {
            log.info("validateUser: Found {} users with username '{}'", users.size(), username);
            return false;
        }

        User u = users.get(0);
        log.info("validateUser: User '{}' found", u.getUsername());

        // Check if the provided password matches the hashed password stored in the database.
        if (!passwordEncoder.matches(password, u.getHashedPassword())) {
            log.info("validateUser: Password does not match for user '{}'", username);
            return false;
        }

        // User exists, and the password matches the stored hash.
        log.info("validateUser: Successful login for user '{}'", username);
        return true;
    }

我还尝试使用

AuthenticationManager
来处理登录逻辑:

@Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        authenticationManagerBuilder.authenticationProvider(authenticationProvider);
        
        return authenticationManagerBuilder.build();
    }

更新了loginController postMethod:

@PostMapping("/login")
    public String loginPost(@Valid @ModelAttribute LoginForm loginForm, BindingResult result,
                            RedirectAttributes attrs, HttpSession httpSession, Model model) {
        return "redirect:/search";  // Redirect to the search page after successful login
    }

用户尝试登录后更新的日志:

[DEBUG] - from org.springframework.security.web.FilterChainProxy in http-nio-8080-exec-5
Securing POST /login

[DEBUG] - from org.springframework.security.web.FilterChainProxy in http-nio-8080-exec-5
Secured POST /login

[DEBUG] - from org.springframework.security.web.authentication.AnonymousAuthenticationFil
ter in http-nio-8080-exec-5
Set SecurityContextHolder to anonymous SecurityContext

[DEBUG] - from org. springframework. security web.FilterChainProxy in http-nio-8080-exec-6
Securing GET / search

[DEBUG] - from org.springframework.security.web.authentication.AnonymousAuthenticationFil
ter in http-nio-8080-exec-6
Set SecurityContextHolder to anonymous SecurityContext

[DEBUG] - from org.springframework.security.web.savedrequest.HttpSessionRequestCache inh
ttp-nio-8080-exec-6
Saved request http://localhost:8080/search?continue to session

[DEBUG] - from org.springframework.security.web.authentication.Http403ForbiddenEntryPoint
in http-nio-8080-exec-6
Pre-authenticated entry point called. Rejecting access

登录凭据有效并且在我的数据库中,因此用户应该登录。我不需要自定义的authenticationProvider或登录逻辑,但想使用我自己的userDetailsService和passwordEncoder,它们现在传递给新的DaoAuthenticationProvider对象然后在上面更新的代码中将其设置为authenticationProvider。我的理解是,这现在将处理登录逻辑,并应该告诉 spring boot 用户已登录,但事实并非如此,我仍然收到与上述消息相同的消息,减去 validateUser 消息,因为不再使用该消息。我的 CustomUserDetailsService 中的 loadUserByUsername 中的日志消息从未被触发,我不确定为什么,因为我将该 userDetailsService 传递给了我的authenticationProvider。

java spring spring-boot spring-security
1个回答
0
投票

我认为您正在使用 Spring Boot 3.X,您可能必须在请求之间显式保存您的 Authentication 对象。 您的重定向将向您的客户端返回 HTTP 30X,并且客户端从您的

/search
视图请求 HTTP GET。

您是否看过需要显式调用 SessionAuthenticationStrategy

© www.soinside.com 2019 - 2024. All rights reserved.