SysAuthServiceImpl.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. package com.backendsys.modules.system.service.impl;
  2. import cn.hutool.core.convert.Convert;
  3. import cn.hutool.core.date.DateUnit;
  4. import cn.hutool.core.date.DateUtil;
  5. import cn.hutool.core.util.RandomUtil;
  6. import cn.hutool.core.util.StrUtil;
  7. import cn.hutool.json.JSONUtil;
  8. import com.backendsys.exception.CustException;
  9. import com.backendsys.modules.common.config.redis.utils.RedisUtil;
  10. import com.backendsys.modules.common.config.security.entity.SecurityUserInfo;
  11. import com.backendsys.modules.common.config.security.utils.*;
  12. import com.backendsys.modules.system.dao.SysMobileAreaDao;
  13. import com.backendsys.modules.system.dao.SysUserDao;
  14. import com.backendsys.modules.system.dao.SysUserInfoDao;
  15. import com.backendsys.modules.system.dao.SysUserRoleDao;
  16. import com.backendsys.modules.system.entity.*;
  17. import com.backendsys.modules.system.service.SysAuthService;
  18. import com.backendsys.modules.system.service.SysCommonService;
  19. import com.backendsys.modules.system.service.SysUserIntegralService;
  20. import com.backendsys.modules.system.service.SysUserService;
  21. import com.backendsys.utils.response.ResultEnum;
  22. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  23. import com.google.code.kaptcha.Producer;
  24. import jakarta.servlet.ServletOutputStream;
  25. import jakarta.servlet.http.HttpServletResponse;
  26. import org.springframework.beans.factory.annotation.Autowired;
  27. import org.springframework.beans.factory.annotation.Value;
  28. import org.springframework.core.env.Environment;
  29. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  30. import org.springframework.stereotype.Service;
  31. import org.springframework.transaction.annotation.Transactional;
  32. import javax.imageio.ImageIO;
  33. import java.awt.image.BufferedImage;
  34. import java.io.ByteArrayOutputStream;
  35. import java.io.IOException;
  36. import java.util.*;
  37. import java.util.concurrent.TimeUnit;
  38. @Service
  39. public class SysAuthServiceImpl implements SysAuthService {
  40. @Autowired
  41. private Environment env;
  42. @Autowired
  43. private JwtUtil jwtUtil;
  44. @Autowired
  45. private RedisUtil redisUtil;
  46. @Autowired
  47. private TokenUtil tokenUtil;
  48. @Autowired
  49. private HttpRequestUtil httpRequestUtil;
  50. @Autowired
  51. private LockStatusUtil lockStatusUtil;
  52. @Autowired
  53. private CaptchaUtil captchaUtil;
  54. @Autowired
  55. private Producer captchaProducer;
  56. @Autowired
  57. private SysUserDao sysUserDao;
  58. @Autowired
  59. private SysUserRoleDao sysUserRoleDao;
  60. @Autowired
  61. private SysUserInfoDao sysUserInfoDao;
  62. @Autowired
  63. private SysUserService sysUserService;
  64. @Autowired
  65. private SysMobileAreaDao sysMobileAreaDao;
  66. @Autowired
  67. private SysUserIntegralService sysUserIntegralService;
  68. @Autowired
  69. private SysCommonService sysCommonService;
  70. @Value("${tencent.sms.debug}")
  71. private String SMS_DEBUG;
  72. @Value("${CAPTCHA_DURATION}")
  73. private Integer CAPTCHA_DURATION;
  74. @Value("${REDIS_LOGIN_TOKEN_PREFIX}")
  75. private String REDIS_LOGIN_TOKEN_PREFIX;
  76. @Value("${spring.application.name}")
  77. private String APPLICATION_NAME;
  78. @Override
  79. public void renderCaptcha(HttpServletResponse response) throws IOException {
  80. byte[] captchaChallengeAsJpeg;
  81. ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
  82. try {
  83. String createText = captchaProducer.createText();
  84. // 获得当前 (UA + IP) 生成的 Key
  85. String captchaRedisKey = httpRequestUtil.getKaptchaKey();
  86. // 保存 验证码字符串 到 redis 中
  87. redisUtil.setCacheObject(captchaRedisKey, createText, this.CAPTCHA_DURATION, TimeUnit.MILLISECONDS);
  88. // 返回 BufferedImage 对象并转为 byte 写入到 byte 数组中
  89. BufferedImage challenge = captchaProducer.createImage(createText);
  90. ImageIO.write(challenge, "jpg", jpegOutputStream);
  91. } catch (Exception e) {
  92. response.sendError(HttpServletResponse.SC_NOT_FOUND);
  93. }
  94. // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
  95. captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
  96. response.setHeader("Cache-Control", "no-store");
  97. response.setHeader("Pragma", "no-cache");
  98. response.setDateHeader("Expires", 0);
  99. response.setContentType("image/jpeg");
  100. ServletOutputStream responseOutputStream = response.getOutputStream();
  101. responseOutputStream.write(captchaChallengeAsJpeg);
  102. responseOutputStream.flush();
  103. responseOutputStream.close();
  104. }
  105. // 判断是否需验证码登录状态
  106. @Override
  107. public Map<String, Object> checkCaptchaRequired(String username) {
  108. if (StrUtil.isEmpty(username)) throw new CustException("username 不能为空");
  109. Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
  110. return Map.of("is_captcha_required", currentCaptchaRequired);
  111. }
  112. @Override
  113. public List<SysMobileArea> getMobileAreaList(SysMobileArea sysMobileArea) {
  114. return sysMobileAreaDao.selectMobileAreaList(sysMobileArea);
  115. }
  116. // [方法] 登录失败 (通用) (errMsg: 错误提示文本, username: 用户名, intercept: 是否拦截)
  117. private void loginFail(String errMsg, String username, Boolean isIntercept) {
  118. // 验证码是否必填
  119. Boolean currentCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
  120. System.out.println("(loginFailByUsername) currentCaptchaRequired = " + currentCaptchaRequired);
  121. // 删除图形验证码
  122. redisUtil.delete(httpRequestUtil.getKaptchaKey());
  123. // 添加登录错误的冻结标记
  124. if (isIntercept) lockStatusUtil.setLockStatus(APPLICATION_NAME + "-login-error", username);
  125. if (currentCaptchaRequired) {
  126. throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode(), Map.of("is_captcha_required", true));
  127. } else {
  128. throw new CustException(errMsg, ResultEnum.INVALID_CREDENTIALS.getCode());
  129. }
  130. }
  131. // [方法] 登录成功
  132. private SysUserInfo loginSuccess(Long user_id, Integer is_remember) {
  133. // [查询] 登录的用户信息
  134. SysUserInfo sysUserInfo = sysUserService.selectUserInfo(user_id);
  135. // 删除图形验证码缓存
  136. redisUtil.delete(httpRequestUtil.getKaptchaKey());
  137. // 删除旧的登录缓存
  138. tokenUtil.deleteRedisLoginToken(sysUserInfo.getLast_login_uuid());
  139. // 判断用户是否审核
  140. Integer audit_status = sysUserInfo.getAudit_status();
  141. if (audit_status != null && audit_status.equals(1)) throw new CustException("用户正在审核中");
  142. if (audit_status != null && audit_status.equals(-1)) throw new CustException("用户审核未通过,请与客服联系");
  143. // 判断用户是否启用
  144. Integer status = sysUserInfo.getStatus();
  145. if (status != null && status.equals(-1)) throw new CustException("该用户已被禁用,请与客服联系");
  146. // 判断用户是否已删除
  147. Integer del_flag = sysUserInfo.getDel_flag();
  148. if (del_flag != null && del_flag.equals(1)) throw new CustException("当前用户不可用,请与客服联系");
  149. // 设置 最后一次的登录信息 (uuid, ip, 登录时间)
  150. String uuid = Convert.toStr(UUID.randomUUID());
  151. sysUserInfo.setLast_login_uuid(uuid);
  152. sysUserInfo.setLast_login_ip(httpRequestUtil.getIpAddr());
  153. sysUserInfo.setLast_login_time(DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
  154. sysUserInfoDao.updateById(sysUserInfo);
  155. // [系统配置] 系统用户默认登录过期时间(小时)
  156. Integer SYSTEM_USER_LOGIN_DURATION_DEFAULT = Convert.toInt(sysCommonService.getCommonByTag("SYSTEM_USER_LOGIN_DURATION_DEFAULT"));
  157. // 将小时转换为毫秒
  158. Long DEFAULT_MILLISECONDS = SYSTEM_USER_LOGIN_DURATION_DEFAULT * DateUnit.HOUR.getMillis();
  159. // 7天 (转毫秒)
  160. Long SEVEN_DAY_MILLISECONDS = 7L * 24 * 60 * 60 * 1000;
  161. Long token_duration_milliseconds = (is_remember != null && is_remember.equals(1)) ? SEVEN_DAY_MILLISECONDS : DEFAULT_MILLISECONDS;
  162. Integer token_duration_hours = Convert.toInt(token_duration_milliseconds / 3600000L);
  163. Date token_expiration = new Date((new Date()).getTime() + token_duration_milliseconds);
  164. sysUserInfo.setToken_expiration(DateUtil.format(token_expiration, "yyyy-MM-dd HH:mm:ss"));
  165. // 生成 Token
  166. SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserInfo), SecurityUserInfo.class);
  167. String token = jwtUtil.createSystemJwtToken(securityUserInfo);
  168. String token_redis_key = REDIS_LOGIN_TOKEN_PREFIX + uuid;
  169. sysUserInfo.setToken(token);
  170. // 生成 PerMissionIds
  171. List<String> permission_ids_list = sysUserInfo.getPermission_ids();
  172. // [Redis] 将 Token 与 Permission 存入缓存
  173. TokenCatch tokenCatch = new TokenCatch(token, permission_ids_list);
  174. redisUtil.setCacheObject(token_redis_key, JSONUtil.toJsonStr(tokenCatch), token_duration_hours, TimeUnit.HOURS);
  175. return sysUserInfo;
  176. }
  177. private void setLoginRequired(String key) {
  178. Object captchaValue = redisUtil.getCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key);
  179. Integer currentErrCount = (captchaValue == null) ? 1 : (Convert.toInt(captchaValue) + 1);
  180. redisUtil.setCacheObject(APPLICATION_NAME + "-login-required-captcha-" + key, currentErrCount, 1, TimeUnit.MINUTES);
  181. System.out.println("currentErrCount: " + currentErrCount);
  182. }
  183. private void cleanLoginRequired(String key) {
  184. redisUtil.delete(APPLICATION_NAME + "-login-required-captcha-" + key);
  185. }
  186. /**
  187. * 登录 (用户名)
  188. */
  189. @Override
  190. @Transactional(rollbackFor = Exception.class)
  191. public SysUserInfo login(SysAuth sysAuth) {
  192. String username = sysAuth.getUsername();
  193. String password = sysAuth.getPassword();
  194. String captcha = sysAuth.getCaptcha();
  195. // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示)
  196. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", username);
  197. // -- 判断是否需要输入验证码 ----------------------------------------------------
  198. // - 当输错 3 次密码时,需要输入验证码
  199. // - 当输错后 1 分钟后重置
  200. Boolean isCaptchaRequired = captchaUtil.isCaptchaRequired(APPLICATION_NAME + "-login-required-captcha-" + username, 3);
  201. if (isCaptchaRequired) {
  202. Boolean isCaptchaEmpty = StrUtil.isEmpty(captcha);
  203. Boolean isCpatchaValid = (captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey()));
  204. if (isCaptchaEmpty) { loginFail("验证码不能为空", username, false); return null; }
  205. if (!isCpatchaValid) { loginFail("验证码错误", username, false); return null; }
  206. }
  207. // --------------------------------------------------------------------------
  208. // [Method] 判断 用户 是否存在 && 密码是否正确
  209. SysUser sysUser = sysUserDao.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
  210. if (sysUser == null) {
  211. // 输入错误时,计数器叠加,并且设置重置时间 (会一直叠加,直到重置 或 登录成功)
  212. setLoginRequired(username);
  213. // [登录失败] 用户不存在
  214. loginFail("用户名或密码错误", username, true);
  215. return null;
  216. } else {
  217. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  218. if (!encoder.matches(password, sysUser.getPassword())) {
  219. // 输入错误时,计数器叠加,并且设置重置时间 (会一直叠加,直到重置 或 登录成功)
  220. setLoginRequired(username);
  221. // [登录失败] 密码不正确
  222. loginFail("用户名或密码错误", username, true);
  223. }
  224. // [登录成功]
  225. cleanLoginRequired(username);
  226. return loginSuccess(sysUser.getId(), sysAuth.getIs_remember());
  227. }
  228. }
  229. /**
  230. * 登录 (手机号码)
  231. */
  232. @Override
  233. @Transactional(rollbackFor = Exception.class)
  234. public SysUserInfo loginWithPhone(SysAuthPhone sysAuthPhone) {
  235. String phone = sysAuthPhone.getPhone();
  236. Integer phoneAreaCode = sysAuthPhone.getPhone_area_code();
  237. Integer phoneValidCode = sysAuthPhone.getPhone_valid_code();
  238. // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示)
  239. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-login-error", phone);
  240. // 判断短信验证码是否正确
  241. String redisKey = APPLICATION_NAME + "-sms-login" + "-" + phone;
  242. Integer smsCode = redisUtil.getCacheObject(redisKey);
  243. // 判断是否发送验证码
  244. if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码");
  245. // 判断短信验证码是否错误
  246. if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true);
  247. // 判断手机号是否存在
  248. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
  249. queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
  250. SysUser sysUser = sysUserDao.selectOne(queryWrapper);
  251. if (sysUser == null) {
  252. // [登录失败] 用户不存在 (并不会销毁短信验证码)
  253. loginFail("手机号码未注册,请先注册", phone, true);
  254. return null;
  255. } else {
  256. // 登录成功,销毁短信验证码
  257. redisUtil.delete(redisKey);
  258. // [登录成功]
  259. return loginSuccess(sysUser.getId(), sysAuthPhone.getIs_remember());
  260. }
  261. }
  262. @Override
  263. @Transactional(rollbackFor = Exception.class)
  264. public Map<String, Object> register(SysUserDTO sysUserDTO) {
  265. // 判断是否允许注册
  266. // [系统配置] 是否允许系统用户注册
  267. Boolean SYSTEM_USER_ALLOW_REGISTER = Convert.toBool(sysCommonService.getCommonByTag("SYSTEM_USER_ALLOW_REGISTER"));
  268. if (!SYSTEM_USER_ALLOW_REGISTER) throw new CustException("系统已禁止注册");
  269. // -- 参数校验 --------------------------------------------------------------
  270. String username = sysUserDTO.getUsername();
  271. String password = sysUserDTO.getPassword();
  272. String captcha = sysUserDTO.getCaptcha();
  273. String phone = sysUserDTO.getPhone();
  274. Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
  275. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  276. String activeProfile = env.getActiveProfiles()[0];
  277. if (!"local".equals(activeProfile)) {
  278. // 判断是否处于登录错误的冻结状态 (2分钟内错误5次,则出现冻结提示)
  279. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", username);
  280. lockStatusUtil.checkLockStatus(APPLICATION_NAME + "-register-error", phone);
  281. // 判断图形验证码是否正确
  282. if (!captchaUtil.isCaptchaValid(captcha, httpRequestUtil.getKaptchaKey())) {
  283. loginFail("验证码错误", username, false);
  284. return null;
  285. }
  286. }
  287. // [查询] 判断用户名是否存在
  288. SysUser sysUser1 = sysUserDao.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, username));
  289. if (sysUser1 != null) throw new CustException("用户名 (" + username + ") 已被注册");
  290. // 判断短信验证码是否正确
  291. if (!"local".equals(activeProfile)) {
  292. String redisKey = APPLICATION_NAME + "-sms-register" + "-" + phone;
  293. Integer smsCode = redisUtil.getCacheObject(redisKey);
  294. // 判断是否发送验证码
  295. if ("false".equals(SMS_DEBUG) && smsCode == null) throw new CustException("请先发送短信验证码");
  296. // 判断短信验证码是否错误
  297. if ("false".equals(SMS_DEBUG) && !smsCode.equals(phoneValidCode)) loginFail("短信验证码错误", phone, true);
  298. }
  299. // [查询] 判断手机号是否存在
  300. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
  301. queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
  302. SysUser sysUser2 = sysUserDao.selectOne(queryWrapper);
  303. if (sysUser2 != null) throw new CustException("手机号码 (+" + phoneAreaCode + " " + phone + ") 已被注册");
  304. // -- 通过校验 --------------------------------------------------------------
  305. // 密码二次加密
  306. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  307. String encodedPassword = encoder.encode(password);
  308. sysUserDTO.setPassword(encodedPassword);
  309. // 注册
  310. SysUserDTO registerEntity = new SysUserDTO();
  311. registerEntity.setUsername(sysUserDTO.getUsername());
  312. registerEntity.setPhone(sysUserDTO.getPhone());
  313. registerEntity.setPhone_area_code(sysUserDTO.getPhone_area_code());
  314. registerEntity.setPassword(sysUserDTO.getPassword());
  315. // 做成后台可控制?
  316. // 邀请码
  317. registerEntity.setInvite_code(sysUserDTO.getInvite_code());
  318. // 注册时,默认使用 权限 (DEFAULT)
  319. String role_sign = "DEFAULT";
  320. // 如果邀请码是 (Material),则注册成为 [素材游客]
  321. if ("Material".equals(sysUserDTO.getInvite_code())) role_sign = "MATERIAL_GUEST";
  322. LambdaQueryWrapper<SysUserRole> wrapperRole = new LambdaQueryWrapper<>();
  323. wrapperRole.eq(SysUserRole::getRole_sign, role_sign);
  324. SysUserRole roleDetail = sysUserRoleDao.selectOne(wrapperRole);
  325. registerEntity.setRole_id(Arrays.asList(roleDetail.getRole_id()));
  326. // 注册时,审核状态为 待审核 (-1拒绝, 1待审核, 2审核通过)
  327. registerEntity.setAudit_status(1);
  328. // 注册时,状态为 禁用
  329. // registerEntity.setStatus(-1);
  330. // 随机昵称 (6位)
  331. registerEntity.setNickname("用户" + RandomUtil.randomStringUpper(6));
  332. // 创建用户
  333. sysUserDao.insertUser(registerEntity);
  334. // 初始化用户积分
  335. sysUserIntegralService.init(registerEntity.getId());
  336. return Map.of("user_id", registerEntity.getId());
  337. }
  338. /**
  339. * 忘记密码/重置密码
  340. */
  341. @Override
  342. public Map<String, Object> forgotPassword(SysUserDTO sysUserDTO) {
  343. String phone = sysUserDTO.getPhone();
  344. Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
  345. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  346. // 判断短信验证码是否正确
  347. String redisKey = APPLICATION_NAME + "-sms-forgotPassword-" + sysUserDTO.getPhone();
  348. Integer smsCode = redisUtil.getCacheObject(redisKey);
  349. if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) {
  350. throw new CustException("短信验证码错误");
  351. }
  352. // [查询] 判断手机号是否存在
  353. LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
  354. queryWrapper.eq(SysUser::getPhone, phone).eq(SysUser::getPhone_area_code, phoneAreaCode);
  355. SysUser sysUser = sysUserDao.selectOne(queryWrapper);
  356. if (sysUser == null) throw new CustException("手机号码不存在");
  357. // 密码二次加密
  358. String password = sysUserDTO.getPassword();
  359. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  360. String encodedPassword = encoder.encode(password);
  361. sysUser.setPassword(encodedPassword);
  362. // 编辑密码
  363. sysUserDao.updateById(sysUser);
  364. // 更改成功,销毁短信验证码
  365. redisUtil.delete(redisKey);
  366. return Map.of("user_id", sysUser.getId());
  367. }
  368. /**
  369. * 退出登录
  370. */
  371. public Map<String, Object> logout() {
  372. Long user_id = httpRequestUtil.getUserId();
  373. if (user_id != null) {
  374. tokenUtil.deleteRedisLoginToken(null);
  375. }
  376. return Map.of("user_id", user_id);
  377. }
  378. }