我正在学习 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。
我认为您正在使用 Spring Boot 3.X,您可能必须在请求之间显式保存您的 Authentication 对象。 您的重定向将向您的客户端返回 HTTP 30X,并且客户端从您的
/search
视图请求 HTTP GET。