SysAuthServiceImpl.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. package com.backendsys.service.System;
  2. import cn.hutool.json.JSONUtil;
  3. import com.backendsys.config.Kaptcha.KaptchaUtil;
  4. import com.backendsys.exception.CustException;
  5. import com.backendsys.modules.common.config.redis.utils.RedisUtil;
  6. import com.backendsys.modules.common.config.security.entity.SecurityUserInfo;
  7. import com.backendsys.modules.common.config.security.utils.TokenUtil;
  8. import com.backendsys.modules.system.dao.SysUserInfoDao;
  9. import com.backendsys.entity.System.SysUserDTO;
  10. import com.backendsys.service.SDKService.SDKTencent.SDKTencentSMSService;
  11. import com.backendsys.utils.CountUtil;
  12. import com.backendsys.utils.UserUtils;
  13. import com.backendsys.utils.response.ResultEnum;
  14. import com.backendsys.mapper.System.SysUserMapper;
  15. import com.backendsys.modules.common.config.security.utils.JwtUtil;
  16. import com.google.code.kaptcha.Producer;
  17. import jakarta.servlet.ServletOutputStream;
  18. import jakarta.servlet.http.HttpServletRequest;
  19. import jakarta.servlet.http.HttpServletResponse;
  20. import org.redisson.api.RLock;
  21. import org.redisson.api.RedissonClient;
  22. import org.springframework.beans.factory.annotation.Autowired;
  23. import org.springframework.beans.factory.annotation.Value;
  24. import org.springframework.context.annotation.Lazy;
  25. import org.springframework.core.env.Environment;
  26. import org.springframework.data.redis.core.StringRedisTemplate;
  27. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  28. import org.springframework.stereotype.Service;
  29. import org.springframework.transaction.annotation.Transactional;
  30. import javax.imageio.ImageIO;
  31. import java.awt.image.BufferedImage;
  32. import java.io.ByteArrayOutputStream;
  33. import java.io.IOException;
  34. import java.time.LocalDateTime;
  35. import java.util.*;
  36. import java.util.concurrent.TimeUnit;
  37. @Service
  38. public class SysAuthServiceImpl implements SysAuthService {
  39. @Autowired
  40. private Environment env;
  41. @Value("${tencent.sms.debug}")
  42. private String SMS_DEBUG;
  43. @Autowired
  44. private CountUtil countUtil;
  45. @Lazy
  46. @Autowired
  47. RedissonClient redissonClient;
  48. @Autowired
  49. private RedisUtil redisUtil;
  50. @Autowired
  51. private TokenUtil tokenUtil;
  52. @Autowired
  53. private StringRedisTemplate stringRedisTemplate;
  54. @Autowired
  55. private SysUserMapper sysUserMapper;
  56. @Autowired
  57. private JwtUtil jwtUtil;
  58. @Value("${TOKEN_DURATION_SYSTEM}")
  59. private Long TOKEN_DURATION_SYSTEM;
  60. @Value("${spring.config.name}")
  61. private String configName;
  62. @Value("${CAPTCHA_DURATION}")
  63. private Long CAPTCHA_DURATION;
  64. @Autowired
  65. private Producer captchaProducer;
  66. @Autowired
  67. private SDKTencentSMSService sdkTencentSMSService;
  68. @Autowired
  69. private SysUserInfoDao sysUserInfoDao;
  70. /**
  71. * 渲染 图形验证码
  72. */
  73. public void renderCaptcha(HttpServletRequest request, HttpServletResponse response) throws RuntimeException, IOException {
  74. byte[] captchaChallengeAsJpeg = null;
  75. ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream();
  76. try {
  77. String createText = captchaProducer.createText();
  78. // 获得当前 (UA + IP) 生成的 Key
  79. String captchaRedisKey = KaptchaUtil.getKaptchaKey(request);
  80. // 保存 验证码字符串 到 redis 中
  81. stringRedisTemplate.opsForValue().set(captchaRedisKey, createText, this.CAPTCHA_DURATION, TimeUnit.MILLISECONDS);
  82. // 返回 BufferedImage 对象并转为 byte 写入到 byte 数组中
  83. BufferedImage challenge = captchaProducer.createImage(createText);
  84. ImageIO.write(challenge, "jpg", jpegOutputStream);
  85. //
  86. } catch (Exception e) {
  87. response.sendError(HttpServletResponse.SC_NOT_FOUND);return;
  88. }
  89. // 定义response输出类型为image/jpeg类型,使用response输出流输出图片的byte数组
  90. captchaChallengeAsJpeg = jpegOutputStream.toByteArray();
  91. response.setHeader("Cache-Control", "no-store");
  92. response.setHeader("Pragma", "no-cache");
  93. response.setDateHeader("Expires", 0);
  94. response.setContentType("image/jpeg");
  95. ServletOutputStream responseOutputStream = response.getOutputStream();
  96. responseOutputStream.write(captchaChallengeAsJpeg);
  97. responseOutputStream.flush();
  98. responseOutputStream.close();
  99. }
  100. // [Method] 判断图形验证码是否正确
  101. private Boolean isCaptchaValid(String captcha, String captchaRedisKey) {
  102. // 如果不是本地开发环境,则执行以下判断
  103. String profileActive = env.getProperty("spring.profiles.active");
  104. if (!("local".equals(profileActive))) {
  105. // 判断验证码是否正确 (是否与Redis中的验证码匹配) (测试环境忽略)
  106. String captchaRedisValue = stringRedisTemplate.opsForValue().get(captchaRedisKey);
  107. if (captchaRedisValue == null || !captchaRedisValue.equalsIgnoreCase(captcha)) {
  108. return false;
  109. }
  110. }
  111. return true;
  112. }
  113. // [Method] 判断密码是否正确
  114. private Boolean isUserPasswordValid(Map<String, Object> userDTO, String password) {
  115. if (userDTO == null) return false;
  116. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  117. return encoder.matches(password, (String) userDTO.get("password"));
  118. }
  119. /**
  120. * [封装] 登录成功回调信息
  121. */
  122. private Map<String, Object> loginSuccess(HttpServletRequest request, SysUserDTO sysUserDTO) {
  123. String uuid = String.valueOf(UUID.randomUUID());
  124. Long userId = sysUserDTO.getUser_id();
  125. Map<String, Object> sysUserDetail = sysUserMapper.queryUserDetail(userId);
  126. // 判断用户是否已审核
  127. Integer auditStatus = (Integer) sysUserDetail.get("audit_status");
  128. if (auditStatus == 1) throw new CustException("请等待管理员审核");
  129. if (auditStatus == -1) throw new CustException("审核未通过,请联系客服获取详细信息");
  130. // [Redis] 删除旧 Redis Key
  131. String old_uuid = (String) sysUserDetail.get("last_login_uuid");
  132. if (old_uuid != null) stringRedisTemplate.delete("token:id:" + old_uuid);
  133. // 3.判断用户 status 是否启用
  134. Object status = sysUserDetail.get("status");
  135. if (status != null && (Integer) status == -1) throw new CustException("该用户已被禁用");
  136. // 4.格式化 modules: [{ id: 1, module_code: "xxx" }] 转为 ["1.x.x", "2.x.x"] (减少 Token 长度)
  137. List<Map<String, Object>> roles = (List<Map<String, Object>>) sysUserDetail.get("roles");
  138. if (roles != null) {
  139. List<String> modules = UserUtils.extractModuleCodes(roles);
  140. roles.forEach(role -> role.remove("module_ids"));
  141. sysUserDetail.put("modules", modules);
  142. sysUserDetail.put("roles", roles);
  143. }
  144. //// x.判断用户 del_flag 是否逻辑删除
  145. //Object del_flag = sysUser.get("del_flag");
  146. //if (del_flag != null && (Integer) del_flag == 1) {
  147. // return Result.error(ResultEnum.INVALID_CREDENTIALS.getCode(), "该用户不存在 (flag)");
  148. //}
  149. // 5.生成 Token 并存入 Redis
  150. // 生成 Token 过期时间 (存入Token/带出Result返回值)
  151. Boolean isRemember = sysUserDTO.getIs_remember();
  152. Long tokenDuration = (isRemember != null && isRemember) ? TOKEN_DURATION_SYSTEM * 7 : TOKEN_DURATION_SYSTEM;
  153. Date tokenExpiration = new Date((new Date()).getTime() + tokenDuration);
  154. sysUserDetail.put("token_expiration", tokenExpiration);
  155. // 生成 Token
  156. sysUserDetail.put("last_login_uuid", uuid);
  157. // Map<String, Object> userInfo = new LinkedHashMap<>(sysUserDetail);
  158. // userInfo.remove("modules");
  159. // String token = jwtUtil.createSystemToken(userInfo);
  160. SecurityUserInfo securityUserInfo = JSONUtil.toBean(JSONUtil.parseObj(sysUserDetail), SecurityUserInfo.class);
  161. String token = jwtUtil.createJwtToken(securityUserInfo);
  162. // 存入 Redis:Token、Token过期时间
  163. String tokenRedisKey = "token:id:" + uuid;
  164. stringRedisTemplate.opsForValue().set(tokenRedisKey, token, tokenDuration, TimeUnit.MILLISECONDS);
  165. // 6.[更新] 用户最后登录时间、登录IP
  166. SysUserDTO sysUserLastlogin = new SysUserDTO();
  167. sysUserLastlogin.setUser_id(userId);
  168. sysUserLastlogin.setLast_login_ip(request.getRemoteAddr());
  169. sysUserLastlogin.setLast_login_uuid(uuid);
  170. LocalDateTime now = LocalDateTime.now();
  171. String formattedDateTime = now.toString();
  172. sysUserLastlogin.setLast_login_time(formattedDateTime);
  173. sysUserMapper.updateUserInfo(sysUserLastlogin);
  174. // 7.[格式化] 将 Token 拼接到输出结果
  175. Map<String, Object> result = new LinkedHashMap<>(sysUserDetail);
  176. result.remove("del_flag");
  177. result.put("token", token);
  178. return result;
  179. }
  180. /**
  181. * 登录 系统用户 (用户名)
  182. * @param sysUserDTO(username, password, captcha)
  183. * @return { token, sysUser }
  184. */
  185. @Override
  186. @Transactional
  187. public Map<String, Object> login(HttpServletRequest request, SysUserDTO sysUserDTO) {
  188. String username = sysUserDTO.getUsername();
  189. String password = sysUserDTO.getPassword();
  190. String captcha = sysUserDTO.getCaptcha();
  191. // 判断是否处于 5次的错误状态
  192. countUtil.checkErrorStatus("login-error", username);
  193. // [Redis] 验证码临时密钥
  194. String captchaRedisKey = KaptchaUtil.getKaptchaKey(request);
  195. // [Method] 判断验证码是否正确
  196. if (!isCaptchaValid(captcha, captchaRedisKey)) {
  197. stringRedisTemplate.delete(captchaRedisKey);
  198. throw new CustException("验证码错误", ResultEnum.INVALID_CREDENTIALS.getCode());
  199. }
  200. // [Method] 判断 用户 是否存在 && 密码是否正确
  201. Map<String, Object> sysUserSimple = sysUserMapper.queryUserByIdOrName(null, username, null, null);
  202. if (sysUserSimple != null) {
  203. sysUserDTO.setUser_id((Long) sysUserSimple.get("id"));
  204. }
  205. if (!(sysUserSimple != null && isUserPasswordValid(sysUserSimple, password))) {
  206. stringRedisTemplate.delete(captchaRedisKey);
  207. // 添加错误标记 (2分钟内错误5次,则出现提示)
  208. countUtil.setErrorCount("login-error", username);
  209. //
  210. throw new CustException("用户名或密码错误", ResultEnum.INVALID_CREDENTIALS.getCode());
  211. }
  212. // 1.作废验证码密钥
  213. stringRedisTemplate.delete(captchaRedisKey);
  214. // [登录成功]
  215. Map<String, Object> result = loginSuccess(request, sysUserDTO);
  216. return result;
  217. }
  218. /**
  219. * 登录 系统用户 (手机号码)
  220. * @param sysUserDTO(phone, phone_valid_code)
  221. */
  222. @Override
  223. @Transactional
  224. public Map<String, Object> loginWithPhone(HttpServletRequest request, SysUserDTO sysUserDTO) {
  225. String phone = sysUserDTO.getPhone();
  226. Integer phoneAreaCode = sysUserDTO.getPhone_area_code();
  227. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  228. // 判断是否处于 5次的错误状态
  229. countUtil.checkErrorStatus("login-error", phone);
  230. // 判断手机验证码是否正确
  231. String redisKey = "sms-login-" + phone;
  232. Integer smsCode = redisUtil.getCacheObject(redisKey);
  233. System.out.println("smsCode: " + smsCode);
  234. // 判断短信验证码是否错误
  235. if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) {
  236. // 添加错误标记 (2分钟内错误5次,则出现提示)
  237. countUtil.setErrorCount("login-error", phone);
  238. throw new CustException("短信验证码错误", ResultEnum.INVALID_CREDENTIALS.getCode());
  239. }
  240. // 判断手机号是否存在
  241. Map<String, Object> sysUserSimple = sysUserMapper.queryUserByIdOrName(null, null, phone, phoneAreaCode);
  242. if (sysUserSimple == null) {
  243. throw new CustException("手机号码不存在", ResultEnum.INVALID_CREDENTIALS.getCode());
  244. } else {
  245. // 登录成功,销毁 smsCode
  246. redisUtil.deleteObject(redisKey);
  247. // 登录成功回调
  248. sysUserDTO.setUser_id((Long) sysUserSimple.get("id"));
  249. Map<String, Object> result = loginSuccess(request, sysUserDTO);
  250. return result;
  251. }
  252. }
  253. /**
  254. * 注册 系统用户
  255. * @param sysUserDTO
  256. * @return { user_id }
  257. */
  258. @Override
  259. @Transactional
  260. public Map<String, Object> registerUser(HttpServletRequest request, SysUserDTO sysUserDTO) {
  261. //// 如果可以获得当前 token 的 id,那以后创建分布式锁时,可以按照 userId 来创建锁
  262. //System.out.println("tokenService.getLoginUUID() = " + tokenService.getLoginUUID());
  263. RLock lock = redissonClient.getLock("registerUser");
  264. try { lock.tryLock(3, TimeUnit.SECONDS);
  265. // -- 校验-----------------------------------------------------------------
  266. // 判断图形验证码是否正确
  267. String captcha = sysUserDTO.getCaptcha();
  268. String captchaRedisKey = KaptchaUtil.getKaptchaKey(request);
  269. if (!isCaptchaValid(captcha, captchaRedisKey)) {
  270. stringRedisTemplate.delete(captchaRedisKey);
  271. throw new CustException("图形验证码错误");
  272. }
  273. // 如果是用手机号注册,则还需判断短信验证码是否正确
  274. if (sysUserDTO.getPhone() != null) {
  275. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  276. String redisKey = "sms-register-" + sysUserDTO.getPhone();
  277. Integer smsCode = redisUtil.getCacheObject(redisKey);
  278. // System.out.println("smsCode: " + smsCode);
  279. if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) {
  280. throw new CustException("短信验证码错误");
  281. }
  282. // 判断手机号是否已注册
  283. Map<String, Object> sysUserSimple1 = sysUserMapper.queryUserByIdOrName(null, null, sysUserDTO.getPhone(), sysUserDTO.getPhone_area_code());
  284. if (sysUserSimple1 != null) {
  285. throw new CustException("手机号码已被注册");
  286. }
  287. // 判断用户名是否已注册
  288. Map<String, Object> sysUserSimple2 = sysUserMapper.queryUserByIdOrName(null, sysUserDTO.getUsername(), null, null);
  289. if (sysUserSimple2 != null) {
  290. throw new CustException("用户名已被注册");
  291. }
  292. // 判断密钥是否有效
  293. String inviteCode = sysUserDTO.getInvite_code();
  294. // .. 待做
  295. // 注册成功,销毁 smsCode
  296. redisUtil.deleteObject(redisKey);
  297. //
  298. }
  299. // -- 动作 -----------------------------------------------------------------
  300. // 密码二次加密
  301. String password = sysUserDTO.getPassword();
  302. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  303. String encodedPassword = encoder.encode(password);
  304. sysUserDTO.setPassword(encodedPassword);
  305. // 注册时,默认使用 游客 2L 类型
  306. LinkedHashMap<String, Object> role_id_obj = new LinkedHashMap<>();
  307. role_id_obj.put("id", 2L);
  308. List<LinkedHashMap<String, Object>> convertedList = new ArrayList<>();
  309. convertedList.add(role_id_obj);
  310. // 注册时,仅添加 Username、Password 字段值
  311. SysUserDTO registerSysUserDTO = new SysUserDTO();
  312. registerSysUserDTO.setUsername(sysUserDTO.getUsername());
  313. registerSysUserDTO.setPhone(sysUserDTO.getPhone());
  314. registerSysUserDTO.setPhone_valid_code(sysUserDTO.getPhone_valid_code());
  315. registerSysUserDTO.setPassword(sysUserDTO.getPassword());
  316. registerSysUserDTO.setRoles(convertedList);
  317. registerSysUserDTO.setInvite_code(sysUserDTO.getInvite_code());
  318. sysUserMapper.insertUser(registerSysUserDTO);
  319. return Map.of("user_id", registerSysUserDTO.getId());
  320. } catch (InterruptedException e) { throw new RuntimeException(e);
  321. } finally { lock.unlock(); }
  322. }
  323. /**
  324. * 忘记密码/重置密码
  325. */
  326. @Override
  327. @Transactional
  328. public Map<String, Object> forgotPassword(SysUserDTO sysUserDTO) {
  329. RLock lock = redissonClient.getLock("forgotPassword");
  330. try { lock.tryLock(3, TimeUnit.SECONDS);
  331. // 1.判断短信验证码是否正确
  332. Integer phoneValidCode = sysUserDTO.getPhone_valid_code();
  333. String redisKey = "sms-forgotPassword-" + sysUserDTO.getPhone();
  334. Integer smsCode = redisUtil.getCacheObject(redisKey);
  335. // System.out.println("smsCode: " + smsCode);
  336. if ("false".equals(SMS_DEBUG) && (smsCode == null || !smsCode.equals(phoneValidCode))) {
  337. throw new CustException("短信验证码错误");
  338. }
  339. // 2.判断手机号是否存在
  340. Map<String, Object> sysUserSimple = sysUserMapper.queryUserByIdOrName(null, null, sysUserDTO.getPhone(), sysUserDTO.getPhone_area_code());
  341. if (sysUserSimple == null) {
  342. throw new CustException("手机号码不存在");
  343. }
  344. // 3.更改密码
  345. SysUserDTO updateDTO = new SysUserDTO();
  346. updateDTO.setUser_id((Long) sysUserSimple.get("id"));
  347. // 密码二次加密
  348. String password = sysUserDTO.getPassword();
  349. BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
  350. String encodedPassword = encoder.encode(password);
  351. updateDTO.setPassword(encodedPassword);
  352. sysUserMapper.updateUserPassword(updateDTO);
  353. // 更改成功,销毁 smsCode
  354. redisUtil.deleteObject(redisKey);
  355. return Map.of("user_id", updateDTO.getUser_id());
  356. } catch (InterruptedException e) { throw new RuntimeException(e);
  357. } finally { lock.unlock(); }
  358. }
  359. /**
  360. * 退出登录 (系统用户)
  361. */
  362. @Override
  363. public Map<String, Object> logout(HttpServletRequest request) {
  364. String token = tokenUtil.getToken(request);
  365. if (token != null && !token.isEmpty()) {
  366. // 将 Token 作废
  367. tokenUtil.deleteRedisToken();
  368. return Map.of("message", "退出成功");
  369. }
  370. throw new CustException(ResultEnum.TOKEN_EMPTY_ERROR.getMessage(), ResultEnum.TOKEN_EMPTY_ERROR.getCode());
  371. }
  372. }