package com.backendsys.service.System; import cn.hutool.json.JSONUtil; import com.backendsys.config.Kaptcha.KaptchaUtil; 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.TokenUtil; import com.backendsys.modules.system.dao.SysUserInfoDao; import com.backendsys.entity.System.SysUserDTO; import com.backendsys.service.SDKService.SDKTencent.SDKTencentSMSService; import com.backendsys.utils.CountUtil; import com.backendsys.utils.UserUtils; import com.backendsys.utils.response.ResultEnum; import com.backendsys.mapper.System.SysUserMapper; import com.backendsys.modules.common.config.security.utils.JwtUtil; import com.google.code.kaptcha.Producer; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.core.env.Environment; import org.springframework.data.redis.core.StringRedisTemplate; 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.util.*; import java.util.concurrent.TimeUnit; @Service public class SysAuthServiceImpl implements SysAuthService { @Autowired private Environment env; @Value("${tencent.sms.debug}") private String SMS_DEBUG; @Autowired private CountUtil countUtil; @Lazy @Autowired RedissonClient redissonClient; @Autowired private RedisUtil redisUtil; @Autowired private TokenUtil tokenUtil; @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private SysUserMapper sysUserMapper; @Autowired private JwtUtil jwtUtil; @Value("${TOKEN_DURATION_SYSTEM}") private Long TOKEN_DURATION_SYSTEM; @Value("${spring.config.name}") private String configName; @Value("${CAPTCHA_DURATION}") private Long CAPTCHA_DURATION; @Autowired private Producer captchaProducer; @Autowired private SDKTencentSMSService sdkTencentSMSService; @Autowired private SysUserInfoDao sysUserInfoDao; /** * 渲染 图形验证码 */ public void renderCaptcha(HttpServletRequest request, HttpServletResponse response) throws RuntimeException, IOException { byte[] captchaChallengeAsJpeg = null; ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream(); try { String createText = captchaProducer.createText(); // 获得当前 (UA + IP) 生成的 Key String captchaRedisKey = KaptchaUtil.getKaptchaKey(request); // 保存 验证码字符串 到 redis 中 stringRedisTemplate.opsForValue().set(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);return; } // 定义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(); } // [Method] 判断图形验证码是否正确 private Boolean isCaptchaValid(String captcha, String captchaRedisKey) { // 如果不是本地开发环境,则执行以下判断 String profileActive = env.getProperty("spring.profiles.active"); if (!("local".equals(profileActive))) { // 判断验证码是否正确 (是否与Redis中的验证码匹配) (测试环境忽略) String captchaRedisValue = stringRedisTemplate.opsForValue().get(captchaRedisKey); if (captchaRedisValue == null || !captchaRedisValue.equalsIgnoreCase(captcha)) { return false; } } return true; } // [Method] 判断密码是否正确 private Boolean isUserPasswordValid(Map userDTO, String password) { if (userDTO == null) return false; BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); return encoder.matches(password, (String) userDTO.get("password")); } /** * [封装] 登录成功回调信息 */ private Map loginSuccess(HttpServletRequest request, SysUserDTO sysUserDTO) { String uuid = String.valueOf(UUID.randomUUID()); Long userId = sysUserDTO.getUser_id(); Map sysUserDetail = sysUserMapper.queryUserDetail(userId); // 判断用户是否已审核 Integer auditStatus = (Integer) sysUserDetail.get("audit_status"); if (auditStatus == 1) throw new CustException("请等待管理员审核"); if (auditStatus == -1) throw new CustException("审核未通过,请联系客服获取详细信息"); // [Redis] 删除旧 Redis Key String old_uuid = (String) sysUserDetail.get("last_login_uuid"); if (old_uuid != null) stringRedisTemplate.delete("token:id:" + old_uuid); // 3.判断用户 status 是否启用 Object status = sysUserDetail.get("status"); if (status != null && (Integer) status == -1) throw new CustException("该用户已被禁用"); // 4.格式化 modules: [{ id: 1, module_code: "xxx" }] 转为 ["1.x.x", "2.x.x"] (减少 Token 长度) List> roles = (List>) sysUserDetail.get("roles"); if (roles != null) { List modules = UserUtils.extractModuleCodes(roles); roles.forEach(role -> role.remove("module_ids")); sysUserDetail.put("modules", modules); sysUserDetail.put("roles", roles); } //// x.判断用户 del_flag 是否逻辑删除 //Object del_flag = sysUser.get("del_flag"); //if (del_flag != null && (Integer) del_flag == 1) { // return Result.error(ResultEnum.INVALID_CREDENTIALS.getCode(), "该用户不存在 (flag)"); //} // 5.生成 Token 并存入 Redis // 生成 Token 过期时间 (存入Token/带出Result返回值) Boolean isRemember = sysUserDTO.getIs_remember(); Long tokenDuration = (isRemember != null && isRemember) ? TOKEN_DURATION_SYSTEM * 7 : TOKEN_DURATION_SYSTEM; Date tokenExpiration = new Date((new Date()).getTime() + tokenDuration); sysUserDetail.put("token_expiration", tokenExpiration); // 生成 Token sysUserDetail.put("last_login_uuid", uuid); // Map userInfo = new LinkedHashMap<>(sysUserDetail); // userInfo.remove("modules"); // String token = jwtUtil.createSystemToken(userInfo); SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserDetail), SecurityUserInfo.class); String token = jwtUtil.createSystemJwtToken(securityUserInfo); // 存入 Redis:Token、Token过期时间 String tokenRedisKey = "token:id:" + uuid; stringRedisTemplate.opsForValue().set(tokenRedisKey, token, tokenDuration, TimeUnit.MILLISECONDS); // 6.[更新] 用户最后登录时间、登录IP SysUserDTO sysUserLastlogin = new SysUserDTO(); sysUserLastlogin.setUser_id(userId); sysUserLastlogin.setLast_login_ip(request.getRemoteAddr()); sysUserLastlogin.setLast_login_uuid(uuid); LocalDateTime now = LocalDateTime.now(); String formattedDateTime = now.toString(); sysUserLastlogin.setLast_login_time(formattedDateTime); sysUserMapper.updateUserInfo(sysUserLastlogin); // 7.[格式化] 将 Token 拼接到输出结果 Map result = new LinkedHashMap<>(sysUserDetail); result.remove("del_flag"); result.put("token", token); return result; } /** * 登录 系统用户 (用户名) * @param sysUserDTO(username, password, captcha) * @return { token, sysUser } */ @Override @Transactional public Map login(HttpServletRequest request, SysUserDTO sysUserDTO) { String username = sysUserDTO.getUsername(); String password = sysUserDTO.getPassword(); String captcha = sysUserDTO.getCaptcha(); // 判断是否处于 5次的错误状态 countUtil.checkErrorStatus("login-error", username); // [Redis] 验证码临时密钥 String captchaRedisKey = KaptchaUtil.getKaptchaKey(request); // [Method] 判断验证码是否正确 if (!isCaptchaValid(captcha, captchaRedisKey)) { stringRedisTemplate.delete(captchaRedisKey); throw new CustException("验证码错误", ResultEnum.INVALID_CREDENTIALS.getCode()); } // [Method] 判断 用户 是否存在 && 密码是否正确 Map sysUserSimple = sysUserMapper.queryUserByIdOrName(null, username, null, null); if (sysUserSimple != null) { sysUserDTO.setUser_id((Long) sysUserSimple.get("id")); } if (!(sysUserSimple != null && isUserPasswordValid(sysUserSimple, password))) { stringRedisTemplate.delete(captchaRedisKey); // 添加错误标记 (2分钟内错误5次,则出现提示) countUtil.setErrorCount("login-error", username); // throw new CustException("用户名或密码错误", ResultEnum.INVALID_CREDENTIALS.getCode()); } // 1.作废验证码密钥 stringRedisTemplate.delete(captchaRedisKey); // [登录成功] Map result = loginSuccess(request, sysUserDTO); return result; } /** * 登录 系统用户 (手机号码) * @param sysUserDTO(phone, phone_valid_code) */ @Override @Transactional public Map loginWithPhone(HttpServletRequest request, SysUserDTO sysUserDTO) { String phone = sysUserDTO.getPhone(); Integer phoneAreaCode = sysUserDTO.getPhone_area_code(); Integer phoneValidCode = sysUserDTO.getPhone_valid_code(); // 判断是否处于 5次的错误状态 countUtil.checkErrorStatus("login-error", phone); // 判断手机验证码是否正确 String redisKey = "sms-login-" + phone; Integer smsCode = redisUtil.getCacheObject(redisKey); System.out.println("smsCode: " + smsCode); // 判断短信验证码是否错误 if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) { // 添加错误标记 (2分钟内错误5次,则出现提示) countUtil.setErrorCount("login-error", phone); throw new CustException("短信验证码错误", ResultEnum.INVALID_CREDENTIALS.getCode()); } // 判断手机号是否存在 Map sysUserSimple = sysUserMapper.queryUserByIdOrName(null, null, phone, phoneAreaCode); if (sysUserSimple == null) { throw new CustException("手机号码不存在", ResultEnum.INVALID_CREDENTIALS.getCode()); } else { // 登录成功,销毁 smsCode redisUtil.delete(redisKey); // 登录成功回调 sysUserDTO.setUser_id((Long) sysUserSimple.get("id")); Map result = loginSuccess(request, sysUserDTO); return result; } } /** * 注册 系统用户 * @param sysUserDTO * @return { user_id } */ @Override @Transactional public Map registerUser(HttpServletRequest request, SysUserDTO sysUserDTO) { //// 如果可以获得当前 token 的 id,那以后创建分布式锁时,可以按照 userId 来创建锁 //System.out.println("tokenService.getLoginUUID() = " + tokenService.getLoginUUID()); RLock lock = redissonClient.getLock("registerUser"); try { lock.tryLock(3, TimeUnit.SECONDS); // -- 校验----------------------------------------------------------------- // 判断图形验证码是否正确 String captcha = sysUserDTO.getCaptcha(); String captchaRedisKey = KaptchaUtil.getKaptchaKey(request); if (!isCaptchaValid(captcha, captchaRedisKey)) { stringRedisTemplate.delete(captchaRedisKey); throw new CustException("图形验证码错误"); } // 如果是用手机号注册,则还需判断短信验证码是否正确 if (sysUserDTO.getPhone() != null) { Integer phoneValidCode = sysUserDTO.getPhone_valid_code(); String redisKey = "sms-register-" + sysUserDTO.getPhone(); Integer smsCode = redisUtil.getCacheObject(redisKey); // System.out.println("smsCode: " + smsCode); if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) { throw new CustException("短信验证码错误"); } // 判断手机号是否已注册 Map sysUserSimple1 = sysUserMapper.queryUserByIdOrName(null, null, sysUserDTO.getPhone(), sysUserDTO.getPhone_area_code()); if (sysUserSimple1 != null) { throw new CustException("手机号码已被注册"); } // 判断用户名是否已注册 Map sysUserSimple2 = sysUserMapper.queryUserByIdOrName(null, sysUserDTO.getUsername(), null, null); if (sysUserSimple2 != null) { throw new CustException("用户名已被注册"); } // 判断密钥是否有效 String inviteCode = sysUserDTO.getInvite_code(); // .. 待做 // 注册成功,销毁 smsCode redisUtil.delete(redisKey); // } // -- 动作 ----------------------------------------------------------------- // 密码二次加密 String password = sysUserDTO.getPassword(); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(password); sysUserDTO.setPassword(encodedPassword); // 注册时,默认使用 游客 2L 类型 LinkedHashMap role_id_obj = new LinkedHashMap<>(); role_id_obj.put("id", 2L); List> convertedList = new ArrayList<>(); convertedList.add(role_id_obj); // 注册时,仅添加 Username、Password 字段值 SysUserDTO registerSysUserDTO = new SysUserDTO(); registerSysUserDTO.setUsername(sysUserDTO.getUsername()); registerSysUserDTO.setPhone(sysUserDTO.getPhone()); registerSysUserDTO.setPhone_valid_code(sysUserDTO.getPhone_valid_code()); registerSysUserDTO.setPassword(sysUserDTO.getPassword()); registerSysUserDTO.setRoles(convertedList); registerSysUserDTO.setInvite_code(sysUserDTO.getInvite_code()); sysUserMapper.insertUser(registerSysUserDTO); return Map.of("user_id", registerSysUserDTO.getId()); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } /** * 忘记密码/重置密码 */ @Override @Transactional public Map forgotPassword(SysUserDTO sysUserDTO) { RLock lock = redissonClient.getLock("forgotPassword"); try { lock.tryLock(3, TimeUnit.SECONDS); // 1.判断短信验证码是否正确 Integer phoneValidCode = sysUserDTO.getPhone_valid_code(); String redisKey = "sms-forgotPassword-" + sysUserDTO.getPhone(); Integer smsCode = redisUtil.getCacheObject(redisKey); // System.out.println("smsCode: " + smsCode); if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) { throw new CustException("短信验证码错误"); } // 2.判断手机号是否存在 Map sysUserSimple = sysUserMapper.queryUserByIdOrName(null, null, sysUserDTO.getPhone(), sysUserDTO.getPhone_area_code()); if (sysUserSimple == null) { throw new CustException("手机号码不存在"); } // 3.更改密码 SysUserDTO updateDTO = new SysUserDTO(); updateDTO.setUser_id((Long) sysUserSimple.get("id")); // 密码二次加密 String password = sysUserDTO.getPassword(); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String encodedPassword = encoder.encode(password); updateDTO.setPassword(encodedPassword); sysUserMapper.updateUserPassword(updateDTO); // 更改成功,销毁 smsCode redisUtil.delete(redisKey); return Map.of("user_id", updateDTO.getUser_id()); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } /** * 退出登录 (系统用户) */ @Override public Map logout(HttpServletRequest request) { String token = tokenUtil.getToken(request); if (token != null && !token.isEmpty()) { // 将 Token 作废 tokenUtil.deleteRedisToken(); return Map.of("message", "退出成功"); } throw new CustException(ResultEnum.TOKEN_EMPTY_ERROR.getMessage(), ResultEnum.TOKEN_EMPTY_ERROR.getCode()); } }