package com.backendsys.modules.system.service.impl; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import com.backendsys.exception.CustException; import com.backendsys.modules.common.config.redis.utils.RedisUtil; import com.backendsys.modules.common.config.security.entity.SecurityUserInfo; import com.backendsys.modules.common.config.security.utils.*; import com.backendsys.modules.system.dao.SysMobileAreaDao; import com.backendsys.modules.system.dao.SysUserDao; import com.backendsys.modules.system.dao.SysUserInfoDao; import com.backendsys.modules.system.dao.SysUserRoleDao; import com.backendsys.modules.system.entity.*; import com.backendsys.modules.system.service.SysAuthService; import com.backendsys.modules.system.service.SysCommonService; import com.backendsys.modules.system.service.SysUserIntegralService; import com.backendsys.modules.system.service.SysUserService; import com.backendsys.modules.system.utils.UserUtil; import com.backendsys.utils.response.ResultEnum; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.google.code.kaptcha.Producer; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import org.springframework.core.env.Environment; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.*; import java.util.concurrent.TimeUnit; @Service public class SysAuthServiceImpl implements SysAuthService { @Autowired private Environment env; @Autowired private UserUtil userUtil; @Autowired private JwtUtil jwtUtil; @Autowired private RedisUtil redisUtil; @Autowired private TokenUtil tokenUtil; @Autowired private HttpRequestUtil httpRequestUtil; @Autowired private LockStatusUtil lockStatusUtil; @Autowired private CaptchaUtil captchaUtil; @Autowired private Producer captchaProducer; @Autowired private SysUserDao sysUserDao; @Autowired private SysUserRoleDao sysUserRoleDao; @Autowired private SysUserInfoDao sysUserInfoDao; @Autowired private SysUserService sysUserService; @Autowired private SysMobileAreaDao sysMobileAreaDao; @Autowired private SysUserIntegralService sysUserIntegralService; @Autowired private SysCommonService sysCommonService; @Value("${tencent.sms.debug}") private String SMS_DEBUG; @Value("${CAPTCHA_DURATION}") private Integer CAPTCHA_DURATION; @Value("${REDIS_LOGIN_TOKEN_PREFIX}") private String REDIS_LOGIN_TOKEN_PREFIX; @Value("${spring.application.name}") private String APPLICATION_NAME; @Override public void renderCaptcha(HttpServletResponse response) throws IOException { byte[] captchaChallengeAsJpeg; ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream(); try { String createText = captchaProducer.createText(); // 获得当前 (UA + IP) 生成的 Key String captchaRedisKey = httpRequestUtil.getKaptchaKey(); // 保存 验证码字符串 到 redis 中 redisUtil.setCacheObject(captchaRedisKey, createText, this.CAPTCHA_DURATION, TimeUnit.MILLISECONDS); // 返回 BufferedImage 对象并转为 byte 写入到 byte 数组中 BufferedImage challenge = captchaProducer.createImage(createText); ImageIO.write(challenge, "jpg", jpegOutputStream); } catch (Exception e) { response.sendError(HttpServletResponse.SC_NOT_FOUND); } // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组 captchaChallengeAsJpeg = jpegOutputStream.toByteArray(); response.setHeader("Cache-Control", "no-store"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); response.setContentType("image/jpeg"); ServletOutputStream responseOutputStream = response.getOutputStream(); responseOutputStream.write(captchaChallengeAsJpeg); responseOutputStream.flush(); responseOutputStream.close(); } // 判断是否需验证码登录状态 @Override public Map checkCaptchaRequired(String username) { if (StrUtil.isEmpty(username)) throw new CustException("username 不能为空"); Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3); return Map.of("is_captcha_required", currentCaptchaRequired); } @Override @Cacheable(value = "catch::mobile-area", key = "'list'", unless = "#result == null") public List getMobileAreaList(SysMobileArea sysMobileArea) { return sysMobileAreaDao.selectMobileAreaList(sysMobileArea); } // [方法] 登录失败 (通用) (errMsg: 错误提示文本, username: 用户名, intercept: 是否拦截) public void loginFail(String errMsg, String username, Boolean isIntercept) { // 验证码是否必填 Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3); System.out.println("(loginFailByUsername) currentCaptchaRequired = " + currentCaptchaRequired); // 删除图形验证码 redisUtil.delete(httpRequestUtil.getKaptchaKey()); // 添加登录错误的冻结标记 if (isIntercept) lockStatusUtil.setLockStatus(APPLICATION_NAME + "-login-error", username); if (currentCaptchaRequired) { throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode(), Map.of("is_captcha_required", true)); } else { throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode()); } } // [方法] 登录成功 public SysUserInfo loginSuccess(Long user_id, Integer is_remember) { // [查询] 登录的用户信息 SysUserInfo entity = new SysUserInfo(); entity.setUser_id(user_id); SysUserInfo sysUserInfo = sysUserService.selectUserInfo(entity); // 删除图形验证码缓存 redisUtil.delete(httpRequestUtil.getKaptchaKey()); // 删除旧的登录缓存 tokenUtil.deleteRedisLoginToken(sysUserInfo.getLast_login_uuid()); // 判断用户是否审核 Integer audit_status = sysUserInfo.getAudit_status(); if (audit_status != null && audit_status.equals(1)) throw new CustException("用户正在审核中"); if (audit_status != null && audit_status.equals(-1)) throw new CustException("用户审核未通过,请与客服联系"); // 判断用户是否启用 Integer status = sysUserInfo.getStatus(); if (status != null && status.equals(-1)) throw new CustException("该用户已被禁用,请与客服联系"); // 判断用户是否已删除 Integer del_flag = sysUserInfo.getDel_flag(); if (del_flag != null && del_flag.equals(1)) throw new CustException("当前用户不可用,请与客服联系"); // 设置 最后一次的登录信息 (uuid, ip, 登录时间) String uuid = Convert.toStr(UUID.randomUUID()); sysUserInfo.setLast_login_uuid(uuid); sysUserInfo.setLast_login_ip(httpRequestUtil.getIpAddr()); // sysUserInfo.setLast_login_time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); sysUserInfo.setLast_login_time(LocalDateTime.now(ZoneOffset.UTC)); sysUserInfoDao.updateById(sysUserInfo); // [系统配置] 系统用户默认登录过期时间(小时) Integer SYSTEM_USER_LOGIN_DURATION_DEFAULT = Convert.toInt(sysCommonService.getCommonByTag("SYSTEM_USER_LOGIN_DURATION_DEFAULT")); // 将小时转换为毫秒 Long DEFAULT_MILLISECONDS = SYSTEM_USER_LOGIN_DURATION_DEFAULT * DateUnit.HOUR.getMillis(); // 7天 (转毫秒) Long SEVEN_DAY_MILLISECONDS = 7L * 24 * 60 * 60 * 1000; // 是否钩选 "7天免登录",否则按系统配置-默认过期时间 Long token_duration_milliseconds = (is_remember != null && is_remember.equals(1)) ? SEVEN_DAY_MILLISECONDS : DEFAULT_MILLISECONDS; Integer tokenDurationHours = Convert.toInt(token_duration_milliseconds / 3600000L); Date token_expiration = new Date((new Date()).getTime() + token_duration_milliseconds); sysUserInfo.setToken_expiration(DateUtil.format(token_expiration, "yyyy-MM-dd HH:mm:ss")); // 实时更新缓存: 用户信息 sysUserInfo = userUtil.syncUpdateUserCatch(sysUserInfo, tokenDurationHours); //// 生成 Token //SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserInfo), SecurityUserInfo.class); //String token = jwtUtil.createSystemJwtToken(securityUserInfo); //sysUserInfo.setToken(token); // //// 生成 PerMissionIds //List permission_ids_list = sysUserInfo.getPermission_ids(); // //// [Redis] 将 Token 与 Permission 存入缓存 //String token_redis_key = REDIS_LOGIN_TOKEN_PREFIX + uuid; //TokenCatch tokenCatch = new TokenCatch(token, permission_ids_list); //redisUtil.setCacheObject(token_redis_key, JSONUtil.toJsonStr(tokenCatch), token_duration_hours, TimeUnit.HOURS); return sysUserInfo; } private void setLoginRequired(String key) { Object captchaValue = redisUtil.getCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key); Integer currentErrCount = (captchaValue == null) ? 1 : (Convert.toInt(captchaValue) + 1); redisUtil.setCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key, currentErrCount, 1, TimeUnit.MINUTES); System.out.println("currentErrCount: " + currentErrCount); } public void cleanLoginRequired(String key) { redisUtil.delete(APPLICATION_NAME + "-login-required-captcha-" + key); } /** * 登录 (用户名) */ @Override @Transactional(rollbackFor = Exception.class) public SysUserInfo login(SysAuth sysAuth) { String username = sysAuth.getUsername(); String password = sysAuth.getPassword(); String captcha = sysAuth.getCaptcha(); // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示) lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", username); // -- 判断是否需要输入验证码 ---------------------------------------------------- // - 当输错 3 次密码时,需要输入验证码 // - 当输错后 1 分钟后重置 Boolean isCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3); if (isCaptchaRequired) { Boolean isCaptchaEmpty = StrUtil.isEmpty(captcha); Boolean isCpatchaValid = (captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey())); if (isCaptchaEmpty) { loginFail("验证码不能为空", username, false); return null; } if (!isCpatchaValid) { loginFail("验证码错误", username, false); return null; } } // -------------------------------------------------------------------------- // [Method] 判断 用户 是否存在 && 密码是否正确 SysUser sysUser = sysUserDao.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); if (sysUser == null) { // 输入错误时,计数器叠加,并且设置重置时间 (会一直叠加,直到重置 或 登录成功) setLoginRequired(username); // [登录失败] 用户不存在 loginFail("用户名或密码错误", username, true); return null; } else { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); if (!encoder.matches(password, sysUser.getPassword())) { // 输入错误时,计数器叠加,并且设置重置时间 (会一直叠加,直到重置 或 登录成功) setLoginRequired(username); // [登录失败] 密码不正确 loginFail("用户名或密码错误", username, true); } // [登录成功] cleanLoginRequired(username); return loginSuccess(sysUser.getId(), sysAuth.getIs_remember()); } } /** * 登录 (手机号码) */ @Override @Transactional(rollbackFor = Exception.class) public SysUserInfo loginWithPhone(SysAuthPhone sysAuthPhone) { String phone = sysAuthPhone.getPhone(); Integer phoneAreaCode = sysAuthPhone.getPhone_area_code(); Integer phoneValidCode = sysAuthPhone.getPhone_valid_code(); // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示) lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", phone); // 判断短信验证码是否正确 String redisKey = APPLICATION_NAME + "-sms-login" + "-" + phone; Integer smsCode = redisUtil.getCacheObject(redisKey); // 判断是否发送验证码 if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码"); // 判断短信验证码是否错误 if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true); // 判断手机号是否存在 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode); SysUser sysUser = sysUserDao.selectOne(queryWrapper); if (sysUser == null) { // [登录失败] 用户不存在 (并不会销毁短信验证码) loginFail("手机号码未注册,请先注册", phone, true); return null; } else { // 登录成功,销毁短信验证码 redisUtil.delete(redisKey); // [登录成功] return loginSuccess(sysUser.getId(), sysAuthPhone.getIs_remember()); } } @Override @Transactional(rollbackFor = Exception.class) public Map register(SysUserDTO sysUserDTO) { // 判断是否允许注册 // [系统配置] 是否允许系统用户注册 Boolean SYSTEM_USER_ALLOW_REGISTER = Convert.toBool(sysCommonService.getCommonByTag("SYSTEM_USER_ALLOW_REGISTER")); if (!SYSTEM_USER_ALLOW_REGISTER) throw new CustException("系统已禁止注册"); // -- 参数校验 -------------------------------------------------------------- String username = sysUserDTO.getUsername(); String password = sysUserDTO.getPassword(); String captcha = sysUserDTO.getCaptcha(); String phone = sysUserDTO.getPhone(); Integer phoneAreaCode = sysUserDTO.getPhone_area_code(); Integer phoneValidCode = sysUserDTO.getPhone_valid_code(); String activeProfile = env.getActiveProfiles()[0]; if (!"local".equals(activeProfile)) { // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示) lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", username); lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", phone); // 判断图形验证码是否正确 if (!captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey())) { loginFail("验证码错误", username, false); return null; } } // [查询] 判断用户名是否存在 SysUser sysUser1 = sysUserDao.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); if (sysUser1 != null) throw new CustException("用户名 (" + username + ") 已被注册"); // 判断短信验证码是否正确 if (!"local".equals(activeProfile)) { String redisKey = APPLICATION_NAME + "-sms-register" + "-" + phone; Integer smsCode = redisUtil.getCacheObject(redisKey); // 判断是否发送验证码 if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码"); // 判断短信验证码是否错误 if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true); } // [查询] 判断手机号是否存在 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode); SysUser sysUser2 = sysUserDao.selectOne(queryWrapper); if (sysUser2 != null) throw new CustException("手机号码 (+" + phoneAreaCode + " " + phone + ") 已被注册"); // -- 通过校验 -------------------------------------------------------------- // 密码二次加密 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(password); sysUserDTO.setPassword(encodedPassword); // 注册 SysUserDTO registerEntity = new SysUserDTO(); registerEntity.setUsername(sysUserDTO.getUsername()); registerEntity.setPhone(sysUserDTO.getPhone()); registerEntity.setPhone_area_code(sysUserDTO.getPhone_area_code()); registerEntity.setPassword(sysUserDTO.getPassword()); // 做成后台可控制? // 邀请码 registerEntity.setInvite_code(sysUserDTO.getInvite_code()); // 注册时,默认使用 权限 (DEFAULT) String role_sign = "DEFAULT"; // 如果邀请码是 (Material),则注册成为 [素材游客] if ("Material".equals(sysUserDTO.getInvite_code())) role_sign = "MATERIAL_GUEST"; LambdaQueryWrapper wrapperRole = new LambdaQueryWrapper<>(); wrapperRole.eq(SysUserRole::getRole_sign, role_sign); SysUserRole roleDetail = sysUserRoleDao.selectOne(wrapperRole); registerEntity.setRole_id(Arrays.asList(roleDetail.getRole_id())); // 注册时,审核状态为 待审核 (-1拒绝, 1待审核, 2审核通过) registerEntity.setAudit_status(1); // 注册时,状态为 禁用 // registerEntity.setStatus(-1); // 随机昵称 (6位) registerEntity.setNickname("用户" + RandomUtil.randomStringUpper(6)); // 创建用户 sysUserDao.insertUser(registerEntity); // 初始化用户积分 sysUserIntegralService.init(registerEntity.getId()); return Map.of("user_id", registerEntity.getId()); } /** * 忘记密码/重置密码 */ @Override public Map forgotPassword(SysUserDTO sysUserDTO) { String phone = sysUserDTO.getPhone(); Integer phoneAreaCode = sysUserDTO.getPhone_area_code(); Integer phoneValidCode = sysUserDTO.getPhone_valid_code(); // 判断短信验证码是否正确 String redisKey = APPLICATION_NAME + "-sms-forgotPassword-" + sysUserDTO.getPhone(); Integer smsCode = redisUtil.getCacheObject(redisKey); if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) { throw new CustException("短信验证码错误"); } // [查询] 判断手机号是否存在 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode); SysUser sysUser = sysUserDao.selectOne(queryWrapper); if (sysUser == null) throw new CustException("手机号码不存在"); // 密码二次加密 String password = sysUserDTO.getPassword(); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(password); sysUser.setPassword(encodedPassword); // 编辑密码 sysUserDao.updateById(sysUser); // 更改成功,销毁短信验证码 redisUtil.delete(redisKey); return Map.of("user_id", sysUser.getId()); } /** * 退出登录 */ public Map logout() { Long user_id = httpRequestUtil.getUserId(); if (user_id != null) { tokenUtil.deleteRedisLoginToken(null); } return Map.of("user_id", user_id); } }