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.NumberUtil; 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.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.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.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.util.*; import java.util.concurrent.TimeUnit; @Service public class SysAuthServiceImpl implements SysAuthService { @Autowired private JwtUtil jwtUtil; @Autowired private RedisUtil redisUtil; @Autowired private TokenUtil tokenUtil; @Autowired private HttpRequestUtil httpRequestUtil; @Autowired private CountUtilV2 countUtilV2; @Autowired private CaptchaUtil captchaUtil; @Autowired private Producer captchaProducer; @Autowired private SysUserDao sysUserDao; @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("${REDIS_LOGIN_PERMISSION_PREFIX}") private String REDIS_LOGIN_PERMISSION_PREFIX; @Value("${spring.application.name}") private String APPLICATION_NAME; private String redisKeyOfLogin = APPLICATION_NAME + "-sms-login"; private String redisKeyOfRegister = APPLICATION_NAME + "-sms-register"; private String redisKeyOfLoginFail = APPLICATION_NAME + "-login-error"; private String redisKeyOfRegisterFail = APPLICATION_NAME + "-register-error"; @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 List getMobileAreaList(SysMobileArea sysMobileArea) { return sysMobileAreaDao.selectMobileAreaList(sysMobileArea); } // [方法] 登录失败 (errMsg: 错误提示文本, username: 用户名, intercept: 是否拦截) private void loginFail(String errMsg, String username, Boolean isIntercept) { // 删除图形验证码 redisUtil.delete(httpRequestUtil.getKaptchaKey()); // 添加登录错误的冻结标记 if (isIntercept) countUtilV2.setErrorCount(redisKeyOfLoginFail, username); throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode()); } // [方法] 登录成功 private SysUserInfo loginSuccess(Long user_id, Integer is_remember) { // [查询] 登录的用户信息 SysUserInfo sysUserInfo = sysUserService.selectUserInfo(user_id); // 删除图形验证码缓存 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 = String.valueOf(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")); 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; Long token_duration_milliseconds = (is_remember != null && is_remember.equals(1)) ? SEVEN_DAY_MILLISECONDS : DEFAULT_MILLISECONDS; Integer token_duration_hours = 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")); // 生成 Token SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserInfo), SecurityUserInfo.class); String token = jwtUtil.createSystemJwtToken(securityUserInfo); String token_redis_key = REDIS_LOGIN_TOKEN_PREFIX + uuid; sysUserInfo.setToken(token); // [Redis] 将 Token 存入缓存 redisUtil.setCacheObject(token_redis_key, token, token_duration_hours, TimeUnit.HOURS); // [Redis] 将 Permission 存入缓存 List permission_ids_list = sysUserInfo.getPermission_ids(); String permission_ids = String.join(",", permission_ids_list); String permission_redis_key = REDIS_LOGIN_PERMISSION_PREFIX + uuid; redisUtil.setCacheObject(permission_redis_key, permission_ids, token_duration_hours, TimeUnit.HOURS); return sysUserInfo; } /** * 登录 (用户名) */ @Override @Transactional public SysUserInfo login(SysAuth sysAuth) { String username = sysAuth.getUsername(); String password = sysAuth.getPassword(); String captcha = sysAuth.getCaptcha(); // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示) countUtilV2.checkErrorStatus(redisKeyOfLoginFail, username); // 判断图形验证码是否正确 if (!captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey())) { loginFail("验证码错误", username, false); return null; } // [Method] 判断 用户 是否存在 && 密码是否正确 SysUser sysUser = sysUserDao.selectOne(new LambdaQueryWrapper().eq(SysUser::getUsername, username)); if (sysUser == null) { // [登录失败] 用户不存在 loginFail("用户名或密码错误", username, true); return null; } else { // [登录失败] 密码不正确 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); if (!encoder.matches(password, sysUser.getPassword())) { loginFail("用户名或密码错误", username, true); } // [登录成功] return loginSuccess(sysUser.getId(), sysAuth.getIs_remember()); } } /** * 登录 (手机号码) */ @Override @Transactional public SysUserInfo loginWithPhone(SysAuthPhone sysAuthPhone) { String phone = sysAuthPhone.getPhone(); Integer phoneAreaCode = sysAuthPhone.getPhone_area_code(); Integer phoneValidCode = sysAuthPhone.getPhone_valid_code(); // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示) countUtilV2.checkErrorStatus(redisKeyOfLoginFail, phone); // 判断短信验证码是否正确 String redisKey = redisKeyOfLogin + "-" + 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 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(); // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示) countUtilV2.checkErrorStatus(redisKeyOfRegisterFail, username); countUtilV2.checkErrorStatus(redisKeyOfRegisterFail, 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 + ") 已被注册"); // 判断短信验证码是否正确 String redisKey = redisKeyOfLogin + "-" + 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()); // 注册时,默认使用 游客 2L 权限 registerEntity.setRole_id(Arrays.asList(2L)); registerEntity.setInvite_code(sysUserDTO.getInvite_code()); // 注册时,状态为禁用 registerEntity.setStatus(-1); // 创建用户 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 = "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); } }