123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- 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<String, Object> userDTO, String password) {
- if (userDTO == null) return false;
- BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
- return encoder.matches(password, (String) userDTO.get("password"));
- }
- /**
- * [封装] 登录成功回调信息
- */
- private Map<String, Object> loginSuccess(HttpServletRequest request, SysUserDTO sysUserDTO) {
- String uuid = String.valueOf(UUID.randomUUID());
- Long userId = sysUserDTO.getUser_id();
- Map<String, Object> 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<Map<String, Object>> roles = (List<Map<String, Object>>) sysUserDetail.get("roles");
- if (roles != null) {
- List<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> result = loginSuccess(request, sysUserDTO);
- return result;
- }
- /**
- * 登录 系统用户 (手机号码)
- * @param sysUserDTO(phone, phone_valid_code)
- */
- @Override
- @Transactional
- public Map<String, Object> 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<String, Object> 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<String, Object> result = loginSuccess(request, sysUserDTO);
- return result;
- }
- }
- /**
- * 注册 系统用户
- * @param sysUserDTO
- * @return { user_id }
- */
- @Override
- @Transactional
- public Map<String, Object> 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<String, Object> sysUserSimple1 = sysUserMapper.queryUserByIdOrName(null, null, sysUserDTO.getPhone(), sysUserDTO.getPhone_area_code());
- if (sysUserSimple1 != null) {
- throw new CustException("手机号码已被注册");
- }
- // 判断用户名是否已注册
- Map<String, Object> 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<String, Object> role_id_obj = new LinkedHashMap<>();
- role_id_obj.put("id", 2L);
- List<LinkedHashMap<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> 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());
- }
- }
|