springboot-security安全插件使用故障解析
springboot-security安全插件使用故障解析!最近开发网站,使用了这个安全插件,但是我发现,用户虽然可以正常登录,但是无论如何也无法设置HttpSession对象绑定loginUser;后来偶然间想到了一件事,会不会是,登录时,插件本身默认调用了
public class CustomUserDetailsService implements UserDetailsService;
因为UserDetailsService的职责就是负责用户登录验证的。我果断打了一个断点,启动debug模式,顺利捕捉到了断点路径。
这个就解决了之前困惑的问题,为什么我辛苦写的登录方法,还有配置的业务逻辑方法(追加登录日志,修改用户登录信息等)丝毫不起作用呢。这个问题原因根源在此啊。
给大家看我自己写的登录方法。
/*** 处理用户登录信息-email* @param username 用户名(手机号或邮箱)* @param password 密码* @param ipAddress 登录IP地址* @param userAgent 用户代理信息* @return* @throws BusinessException*/ @Override @Transactional public User login(String username, String password, String ipAddress, String userAgent) throws BusinessException {return handleLoginSuccess(username, ipAddress, userAgent);}它位于我自定义的一个AuthService业务接口实现类里面。
package com.example.feng.service.impl;import com.example.feng.dto.UserRegistrationDTO;
import com.example.feng.entity.LoginLog;
import com.example.feng.entity.User;
import com.example.feng.exception.BusinessException;
import com.example.feng.service.AuthService;
import com.example.feng.service.LoginLogService;
import com.example.feng.service.UserService;
import com.example.feng.service.VerificationCodeService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;import java.time.LocalDateTime;/*** 认证服务实现类* 实现用户注册、登录功能,集成角色管理和登录日志记录*/
@Service
@Transactional
@Slf4j
public class AuthServiceImpl implements AuthService {@Resourceprivate UserService userService;@Resourceprivate LoginLogService loginLogService;@Resourceprivate PasswordEncoder passwordEncoder;@Resourceprivate VerificationCodeService verificationCodeService;/*** 最大允许连续登录失败次数*/private static final int MAX_FAILED_ATTEMPTS = 5;/*** 账号锁定时长(分钟)*/private static final int LOCK_MINUTES = 60;@Overridepublic User registerByEmail(UserRegistrationDTO dto) throws BusinessException {// 1. 验证数据validateRegistrationData(dto, "email");// 2. 验证验证码verificationCodeService.verifyCode(dto.getEmail(), dto.getCode(), "email");// 3. 检查邮箱是否已注册if (userService.existsByEmail(dto.getEmail())) {throw new BusinessException("该邮箱已注册");}// 4. 创建用户User user = createUser(dto);user.setEmail(dto.getEmail());// 5. 保存用户User savedUser = userService.addOneUser(user);log.info("用户邮箱注册成功 - 用户ID: {}, 邮箱: {}", savedUser.getId(), savedUser.getEmail());return savedUser;}/*** 处理用户登录信息-email* @param username 用户名(手机号或邮箱)* @param password 密码* @param ipAddress 登录IP地址* @param userAgent 用户代理信息* @return* @throws BusinessException*/@Override@Transactionalpublic User login(String username, String password, String ipAddress, String userAgent) throws BusinessException {return handleLoginSuccess(username, ipAddress, userAgent);}@Overridepublic void verifyCode(String target, String code, String type) throws BusinessException {if (!verificationCodeService.verifyCode(target, code, type)) {throw new BusinessException("验证码不正确或已过期");}}/*** 创建用户对象*/private User createUser(UserRegistrationDTO dto) {User user = new User();user.setPassword(dto.getPassword());user.setRole("STUDENT"); // 默认角色为学生//INSTITUTIONuser.setStatus(1); // 默认为正常状态user.setLoginCount(0);user.setFailLoginCount(0);return user;}/*** 验证注册数据*/private void validateRegistrationData(UserRegistrationDTO dto, String type) throws BusinessException {System.out.println(dto.getPassword()+":"+dto.getConfirmPassword());// 验证密码一致性
// if (!dto.getPassword().equals(dto.getConfirmPassword())) {
// throw new BusinessException("两次输入的密码不一致");
// }if ("email".equals(type) && !isValidEmail(dto.getEmail())) {throw new BusinessException("请输入正确的邮箱地址");}}/*** 手机号格式验证*/private boolean isValidPhone(String phone) {return StringUtils.hasText(phone) && phone.matches("^1[3-9]\\d{9}$");}/*** 邮箱格式验证*/private boolean isValidEmail(String email) {return StringUtils.hasText(email) && email.matches("^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$");}/*** 检查用户状态*/private void checkUserStatus(User user) throws BusinessException {if (user.getStatus() == 0) {throw new BusinessException("账号尚未激活,请先激活");}if (user.getStatus() == 3) {throw new BusinessException("账号已注销");}if (user.isLocked()) {throw new BusinessException("账号已被锁定," + user.getLockReason() + ",请" + LOCK_MINUTES + "分钟后再试");}}/*** 处理登录成功*/private User handleLoginSuccess(String username, String ipAddress, String userAgent) {User user = userService.findUserByUsername(username);try{// 1. 更新用户信息//根据用户名(手机号或邮箱)查找用户user.setLastLoginTime(LocalDateTime.now());user.setLastLoginIp(ipAddress);user.setLastLoginDevice(extractDeviceType(userAgent));user.setLoginCount(user.getLoginCount() + 1);user.resetFailLoginCount(); // 重置失败次数System.out.println("here is 处理登录成功操作方法内部。准备更新用户信息。");User updatedUser = userService.update(user);// 2. 记录成功登录日志recordSuccessLoginLog(updatedUser, ipAddress, userAgent);}catch (Exception e){e.printStackTrace();}return user;}/*** 处理登录失败*/private void handleLoginFailure(User user, String ipAddress, String userAgent, String reason) throws BusinessException {// 1. 增加失败次数user.incrementFailLoginCount();// 2. 检查是否需要锁定账号if (user.getFailLoginCount() >= MAX_FAILED_ATTEMPTS) {user.setStatus(2); // 锁定状态user.setLockReason("连续" + MAX_FAILED_ATTEMPTS + "次登录失败");user.setLockTime(LocalDateTime.now().plusMinutes(LOCK_MINUTES));reason = "连续" + MAX_FAILED_ATTEMPTS + "次登录失败,账号已锁定" + LOCK_MINUTES + "分钟";}userService.update(user);// 3. 记录失败登录日志-记录成功登录日志recordFailLoginLog(user.getLoginAccount(), ipAddress, userAgent, reason);log.warn("用户登录失败 - 用户ID: {}, 账号: {}, 原因: {}", user.getId(), user.getLoginAccount(), reason);}/*** 记录成功登录日志*/private void recordSuccessLoginLog(User user, String ipAddress, String userAgent) {try{LoginLog loginLog = new LoginLog();loginLog.setUserId(user.getId());loginLog.setUserName(user.getUsername());loginLog.setLoginIp(ipAddress);loginLog.setLoginTime(LocalDateTime.now());loginLog.setStatus(1);loginLog.setUserAgent(userAgent);loginLog.setDeviceType(extractDeviceType(userAgent));loginLog.setLocation(resolveLocation(ipAddress)); // 实际项目中可集成IP解析服务System.out.println("这里是记录登录成功日志的方法内部");log.warn("用户登录成功了 - 用户: {}", user.getUsername());loginLogService.addOneLoginlog(loginLog);}catch (Exception e){e.printStackTrace();}}/*** 记录失败登录日志*/private void recordFailLoginLog(String account, String ipAddress, String userAgent, String reason) {try{LoginLog loginLog = new LoginLog();loginLog.setUserName(account);loginLog.setLoginIp(ipAddress);loginLog.setLoginTime(LocalDateTime.now());loginLog.setStatus(0);// 0=失败(必须显式设置,避免默认值错误)loginLog.setFailReason(reason);loginLog.setUserAgent(userAgent);loginLog.setDeviceType(extractDeviceType(userAgent));loginLog.setLocation(resolveLocation(ipAddress));System.out.println("这里是记录登录失败日志的方法内部");log.warn("用户登录失败 - 用户: {}", account);loginLogService.addOneLoginlog(loginLog);}catch (Exception e){e.printStackTrace();}}/*** 提取设备类型*/private String extractDeviceType(String userAgent) {if (userAgent == null) {return "Unknown";}if (userAgent.contains("Android")) {return "Android";} else if (userAgent.contains("iPhone") || userAgent.contains("iOS")) {return "iOS";} else if (userAgent.contains("Windows") || userAgent.contains("Macintosh")) {return "PC";} else {return "Other";}}/*** 解析IP地址对应的地理位置* 实际项目中可集成IP解析服务*/private String resolveLocation(String ipAddress) {// 示例实现,实际项目中应调用IP解析APIif (ipAddress.startsWith("192.168.") || ipAddress.startsWith("127.0.")) {return "本地网络";}return "未知地区";}
}
虽然,我也做了配置路径,设置好了处理登录请求的接口地址。但是,系统依然没有去寻址这里。
package com.example.feng.config;import com.example.feng.security.JwtAuthenticationFilter;
import com.example.feng.security.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 启用方法级权限注解
public class SecurityConfig {private final JwtTokenProvider jwtTokenProvider;private final UserDetailsService userDetailsService; // 注入用户详情服务(表单登录必需)// 构造函数注入依赖public SecurityConfig(JwtTokenProvider jwtTokenProvider, UserDetailsService userDetailsService) {this.jwtTokenProvider = jwtTokenProvider;this.userDetailsService = userDetailsService;}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 1. 修复Session配置冲突:表单登录需保留Session,删除STATELESS配置.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))// 2. 表单登录配置(核心:确保登录接口可被匿名访问).formLogin(form -> form.loginPage("/auth/login") // 登录页面路径.loginProcessingUrl("/auth/login-by-email") // 登录请求处理接口.defaultSuccessUrl("/web/index", true) // 登录成功强制跳转.failureUrl("/auth/login?error=true") // 登录失败跳转(带错误参数).permitAll() // 允许匿名访问登录相关接口)// 3. CSRF配置:排除登录接口,避免表单提交CSRF验证失败.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 前端可获取CSRF令牌.ignoringRequestMatchers("/auth/login-by-email", // 登录接口排除CSRF"/liuyan","/liuyan/response","/about/**","/auth/send-email-code"))// 4. 授权规则配置(修复/liuyan权限,明确公共接口).authorizeHttpRequests(auth -> auth// 公共接口:允许匿名访问.requestMatchers("/auth/login","/auth/login-by-email", // 关键:登录接口允许匿名调用"/auth/register","/auth/send-email-code","/auth/register-by-email","/swagger-ui/**","/v3/api-docs/**","/webjars/**","/favicon.ico","/web/**","/static/**","/about/**").permitAll()// 管理员接口:需ADMIN角色.requestMatchers("/admin/**").hasRole("ADMIN")// 其他所有请求:需登录认证.anyRequest().authenticated())// 5. 添加JWT过滤器:在用户名密码过滤器之前执行,且已在过滤器内部排除登录接口.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),UsernamePasswordAuthenticationFilter.class)// 6. 注销配置(可选,优化用户体验).logout(logout -> logout.logoutUrl("/auth/logout").logoutSuccessUrl("/auth/login?logout=true") // 注销成功跳转登录页.permitAll());return http.build();}// 密码加密器(表单登录密码验证必需)@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 认证管理器(表单登录认证必需)@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}
// @Bean
// public AuthenticationSuccessHandler authenticationSuccessHandler() {
// return (request, response, authentication) -> {
// // 设置安全上下文
// SecurityContextHolder.getContext().setAuthentication(authentication);
//
// // 设置Session
// request.getSession().setAttribute("loginUser", authentication.getPrincipal());
//
// response.sendRedirect("/web/index");
// };
// }
}
如图。目前就是这样。
也就是说,你可以自定义一个实现类,但是必须实现官方的那个接口。
UserDetailsService;
它里面有一个方法叫
loadUserByUsername;
你必须重写这个方法。才能实现登录效果。