Przeglądaj źródła

密码登录相关代码提交

zhangchen 6 godzin temu
rodzic
commit
758925a04c

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java

@@ -2,6 +2,7 @@ package shop.alien.entity.store;
 
 import com.baomidou.mybatisplus.annotation.*;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -49,6 +50,11 @@ public class LifeUser implements Serializable {
     @TableField("user_phone")
     private String userPhone;
 
+    @ApiModelProperty(value = "登录密码")
+    @TableField("password")
+    @JsonIgnore
+    private String password;
+
     @ApiModelProperty(value = "用户头像照片")
     @TableField("user_image")
     private String userImage;

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeUserPasswordDto.java

@@ -0,0 +1,25 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("用户端密码相关请求")
+public class LifeUserPasswordDto {
+
+    @ApiModelProperty("手机号")
+    private String phoneNum;
+
+    @ApiModelProperty("密码")
+    private String password;
+
+    @ApiModelProperty("确认密码")
+    private String confirmPassword;
+
+    @ApiModelProperty("短信验证码(忘记密码时使用)")
+    private String code;
+
+    @ApiModelProperty("设备标识(登录时使用,可选)")
+    private String macIp;
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserVo.java

@@ -69,4 +69,7 @@ public class LifeUserVo extends LifeUser {
     @ApiModelProperty(value = "验证码")
     @JsonAlias("code")
     private String verificationCode;
+
+    @ApiModelProperty(value = "是否已设置登录密码")
+    private Boolean hasLoginPassword;
 }

+ 42 - 0
alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserPasswordController.java

@@ -0,0 +1,42 @@
+package shop.alien.gateway.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.entity.store.vo.LifeUserVo;
+import shop.alien.gateway.service.LifeUserPasswordService;
+
+/**
+ * 用户端密码登录(独立接口,不影响验证码登录)
+ */
+@Api(tags = {"一期-用户-密码登录"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/user/password")
+@RequiredArgsConstructor
+public class LifeUserPasswordController {
+
+    private final LifeUserPasswordService lifeUserPasswordService;
+
+    @ApiOperation("用户密码登录")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/login")
+    public R<LifeUserVo> passwordLogin(@RequestBody LifeUserPasswordDto dto) {
+        log.info("LifeUserPasswordController.passwordLogin?phoneNum={}", dto != null ? dto.getPhoneNum() : null);
+        return lifeUserPasswordService.passwordLogin(dto);
+    }
+
+    @ApiOperation("忘记密码")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/forget")
+    public R<String> forgetPassword(@RequestBody LifeUserPasswordDto dto) {
+        log.info("LifeUserPasswordController.forgetPassword?phoneNum={}", dto != null ? dto.getPhoneNum() : null);
+        return lifeUserPasswordService.forgetPassword(dto);
+    }
+}

+ 146 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserPasswordService.java

@@ -0,0 +1,146 @@
+package shop.alien.gateway.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.entity.store.vo.LifeUserVo;
+import shop.alien.gateway.config.BaseRedisService;
+import shop.alien.gateway.mapper.LifeUserGatewayMapper;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 用户端密码登录服务(独立实现,不修改原有验证码登录逻辑)
+ */
+@Service
+@RequiredArgsConstructor
+public class LifeUserPasswordService {
+
+    private final LifeUserGatewayMapper lifeUserMapper;
+
+    private final BaseRedisService baseRedisService;
+
+    private final LifeUserService lifeUserService;
+
+    @Value("${jwt.expiration-time}")
+    private String effectiveTime;
+
+    public R<LifeUserVo> passwordLogin(LifeUserPasswordDto dto) {
+        if (dto == null || StringUtils.isBlank(dto.getPhoneNum()) || StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("手机号和密码不能为空");
+        }
+        LifeUser user = lifeUserService.getUserByPhone(dto.getPhoneNum());
+        if (user == null) {
+            return R.fail("当前账号不存在,请先去注册账号");
+        }
+        if (StringUtils.isBlank(user.getPassword())) {
+            return R.fail("尚未设置登录密码,请使用验证码登录或设置密码");
+        }
+        if (!Objects.equals(dto.getPassword(), user.getPassword())) {
+            return R.fail("密码错误");
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            return R.fail("您的账户因严重违规导致被封禁");
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            return R.fail("你的账号已注销");
+        }
+        return R.data(createLoginVo(user, dto.getPhoneNum(), dto.getMacIp()));
+    }
+
+    public R<String> forgetPassword(LifeUserPasswordDto dto) {
+        if (dto == null || StringUtils.isBlank(dto.getPhoneNum()) || StringUtils.isBlank(dto.getCode())) {
+            return R.fail("手机号和验证码不能为空");
+        }
+        if (StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("密码不能为空");
+        }
+        if (!Objects.equals(dto.getPassword(), dto.getConfirmPassword())) {
+            return R.fail("两次密码输入不一致");
+        }
+        String cacheCode = baseRedisService.getString("verification_user_forget_password_" + dto.getPhoneNum());
+        if (cacheCode == null) {
+            return R.fail("验证码过期或未发送");
+        }
+        if (!cacheCode.trim().equals(dto.getCode().trim())) {
+            return R.fail("验证码错误");
+        }
+        LifeUser user = lifeUserService.getUserByPhone(dto.getPhoneNum());
+        if (user == null) {
+            return R.fail("当前账号不存在,请先去注册账号");
+        }
+        LifeUser update = new LifeUser();
+        update.setId(user.getId());
+        update.setPassword(dto.getPassword());
+        if (lifeUserMapper.updateById(update) <= 0) {
+            return R.fail("重置密码失败");
+        }
+        baseRedisService.delete("verification_user_forget_password_" + dto.getPhoneNum());
+        invalidateUserSessions(dto.getPhoneNum());
+        return R.success("密码重置成功");
+    }
+
+    private LifeUserVo createLoginVo(LifeUser user, String phoneNum, String macIp) {
+        LifeUserVo userVo = new LifeUserVo();
+        BeanUtils.copyProperties(user, userVo);
+        userVo.setPassword(null);
+        Map<String, String> tokenMap = new HashMap<>();
+        tokenMap.put("phone", phoneNum);
+        tokenMap.put("userName", user.getUserName());
+        tokenMap.put("userId", user.getId().toString());
+        tokenMap.put("userType", "user");
+        String token = createToken(phoneNum, user.getUserName(), tokenMap);
+        userVo.setToken(token);
+        addSessionToken(phoneNum, token);
+        lifeUserService.addLifeUserLogInfo(user, macIp);
+        return userVo;
+    }
+
+    private String createToken(String phoneNum, String userName, Map<String, String> tokenMap) {
+        int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
+        String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
+        long effectiveTimeIntLong = 0L;
+        switch (effectiveTimeUnit) {
+            case "s":
+                effectiveTimeIntLong = effectiveTimeInt * 1000L;
+                break;
+            case "m":
+                effectiveTimeIntLong = effectiveTimeInt * 60L * 1000L;
+                break;
+            case "h":
+                effectiveTimeIntLong = effectiveTimeInt * 60L * 60L * 1000L;
+                break;
+            case "d":
+                effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
+                break;
+            default:
+                break;
+        }
+        return JwtUtil.createJWT("user_" + phoneNum, userName, JSONObject.toJSONString(tokenMap), effectiveTimeIntLong);
+    }
+
+    private void addSessionToken(String phone, String token) {
+        String legacyKey = "user_" + phone;
+        String sessionSetKey = "user_sessions:" + phone;
+        String oldSingle = baseRedisService.getString(legacyKey);
+        if (oldSingle != null && !oldSingle.isEmpty()) {
+            baseRedisService.setSetList(sessionSetKey, oldSingle);
+        }
+        baseRedisService.delete(legacyKey);
+        baseRedisService.setSetList(sessionSetKey, token);
+    }
+
+    private void invalidateUserSessions(String phone) {
+        baseRedisService.delete("user_" + phone);
+        baseRedisService.delete("user_sessions:" + phone);
+    }
+}

+ 2 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java

@@ -92,6 +92,7 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
                 addLifeUserSessionToken(phoneNum, token);
                 // 二手平台登录log,同一个macip登录多账号记录
                 addLifeUserLogInfo(user2, macIp);
+                userVo.setHasLoginPassword(StringUtils.isNotBlank(user2.getPassword()));
 
                 return userVo;
             } else {
@@ -110,6 +111,7 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
             addLifeUserSessionToken(phoneNum, token);
             // 二手平台登录log,同一个macip登录多账号记录
             addLifeUserLogInfo(user, macIp);
+            userVo.setHasLoginPassword(StringUtils.isNotBlank(user.getPassword()));
 
             return userVo;
         }

+ 11 - 5
alien-store/src/main/java/shop/alien/store/controller/AliController.java

@@ -14,6 +14,7 @@ import shop.alien.store.service.AliService;
 import shop.alien.store.service.LifeUserService;
 import shop.alien.store.util.ali.AliApi;
 import shop.alien.store.util.ali.AliSms;
+import shop.alien.store.util.ali.SmsSendLimitException;
 import shop.alien.util.ali.AliOSSUtil;
 import shop.alien.util.common.AlipayTradeAppPay;
 import shop.alien.util.common.AlipayTradeRefund;
@@ -119,12 +120,17 @@ public class AliController {
             @RequestParam("appType") Integer appType,
             @RequestParam("businessType") Integer businessType
     ) {
-        Integer code = aliSmsConfig.sendSms(phone, appType, businessType);
-        log.info("AliController.sendSms?phone={}&code={}&businessType={}", phone, code, businessType);
-        if (code != null) {
-            return R.data("短信发送成功");
+        try {
+            Integer code = aliSmsConfig.sendSms(phone, appType, businessType);
+            log.info("AliController.sendSms?phone={}&code={}&businessType={}", phone, code, businessType);
+            if (code != null) {
+                return R.data("短信发送成功");
+            }
+            return R.fail("短信发送失败");
+        } catch (SmsSendLimitException e) {
+            log.warn("AliController.sendSms limit exceeded, phone={}, businessType={}", phone, businessType);
+            return R.fail(e.getMessage());
         }
-        return R.fail("短信发送失败");
     }
 
 

+ 38 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserPasswordController.java

@@ -0,0 +1,38 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.store.service.LifeUserPasswordService;
+import shop.alien.util.common.TokenInfo;
+import springfox.documentation.annotations.ApiIgnore;
+
+/**
+ * 用户端登录密码(设置密码在 store 服务,登录/忘记密码仍在 gateway)
+ */
+@Api(tags = {"一期-用户-密码登录"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/user/password")
+@RequiredArgsConstructor
+public class LifeUserPasswordController {
+
+    private final LifeUserPasswordService lifeUserPasswordService;
+
+    @ApiOperation("设置当前账号登录密码(需登录)")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/set")
+    public R<String> setPassword(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                                 @RequestBody LifeUserPasswordDto dto) {
+        log.info("LifeUserPasswordController.setPassword, userId={}",
+                userLoginInfo == null ? null : userLoginInfo.getUserId());
+        return lifeUserPasswordService.setPassword(userLoginInfo, dto);
+    }
+}

+ 10 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserPasswordService.java

@@ -0,0 +1,10 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+
+public interface LifeUserPasswordService {
+
+    R<String> setPassword(UserLoginInfo login, LifeUserPasswordDto dto);
+}

+ 50 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPasswordServiceImpl.java

@@ -0,0 +1,50 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.store.service.LifeUserPasswordService;
+
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class LifeUserPasswordServiceImpl implements LifeUserPasswordService {
+
+    private final LifeUserMapper lifeUserMapper;
+
+    @Override
+    public R<String> setPassword(UserLoginInfo login, LifeUserPasswordDto dto) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        if (!"user".equals(login.getType()) && !"miniprogram_user".equals(login.getType())) {
+            return R.fail("仅支持用户端操作");
+        }
+        if (dto == null || StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("密码不能为空");
+        }
+        if (!Objects.equals(dto.getPassword(), dto.getConfirmPassword())) {
+            return R.fail("两次密码输入不一致");
+        }
+        LifeUser user = lifeUserMapper.selectById(login.getUserId());
+        if (user == null) {
+            return R.fail("用户不存在");
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            return R.fail("您的账户因严重违规导致被封禁");
+        }
+        LifeUser update = new LifeUser();
+        update.setId(login.getUserId());
+        update.setPassword(dto.getPassword());
+        if (lifeUserMapper.updateById(update) <= 0) {
+            return R.fail("设置密码失败");
+        }
+        return R.success("设置密码成功");
+    }
+}

+ 14 - 0
alien-store/src/main/java/shop/alien/store/util/ali/AliSms.java

@@ -36,6 +36,8 @@ public class AliSms {
 
     private final StoreVerificationCodeMapper storeVerificationCodeMapper;
 
+    private final UserForgetPasswordSmsDailyLimiter userForgetPasswordSmsDailyLimiter;
+
     @Value("${ali.sms.accessKeyId}")
     private String accessKeyId;
 
@@ -83,6 +85,9 @@ public class AliSms {
      */
     public Integer sendSms(String phone, Integer appType, Integer businessType) {
         log.info("AliSmsConfig.sendSms?phone={}&appType={}&businessType={}", phone, appType, businessType);
+        if (userForgetPasswordSmsDailyLimiter.isUserForgetPassword(appType, businessType)) {
+            userForgetPasswordSmsDailyLimiter.checkLimit(phone);
+        }
         try {
             String appTypeStr = appType == 0 ? "user" : (appType == 1 ? "store" : "store_platform");
             String businessTypeStr;
@@ -158,8 +163,11 @@ public class AliSms {
             // 验证码发送成功,将验证码保存到redis中 设置60秒过期
             baseRedisService.setString("verification_" + appTypeStr + "_" + businessTypeStr + "_" + phone, code.toString(), codeTimeOut);
             saveVerificationCode(appType, businessType, phone, code.toString());
+            recordUserForgetPasswordSend(appType, businessType, phone);
 
             return code;
+        } catch (SmsSendLimitException e) {
+            throw e;
         } catch (Exception e) {
             log.error("AliSmsConfig.sendSms ERROR Msg={}", e.getMessage());
             return null;
@@ -257,6 +265,12 @@ public class AliSms {
         }
     }
 
+    private void recordUserForgetPasswordSend(Integer appType, Integer businessType, String phone) {
+        if (userForgetPasswordSmsDailyLimiter.isUserForgetPassword(appType, businessType)) {
+            userForgetPasswordSmsDailyLimiter.recordSend(phone);
+        }
+    }
+
     /**
      * JSON字符串转义,防止特殊字符破坏JSON格式
      */

+ 11 - 0
alien-store/src/main/java/shop/alien/store/util/ali/SmsSendLimitException.java

@@ -0,0 +1,11 @@
+package shop.alien.store.util.ali;
+
+/**
+ * 短信发送受限(如忘记密码每日次数上限)
+ */
+public class SmsSendLimitException extends RuntimeException {
+
+    public SmsSendLimitException(String message) {
+        super(message);
+    }
+}

+ 81 - 0
alien-store/src/main/java/shop/alien/store/util/ali/UserForgetPasswordSmsDailyLimiter.java

@@ -0,0 +1,81 @@
+package shop.alien.store.util.ali;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 用户端忘记密码短信每日发送次数限制(仅 appType=0, businessType=6)
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class UserForgetPasswordSmsDailyLimiter {
+
+    private static final int MAX_DAILY_COUNT = 5;
+    private static final int USER_APP_TYPE = 0;
+    private static final int FORGET_PASSWORD_BUSINESS_TYPE = 6;
+    private static final String KEY_PREFIX = "sms_user_forget_password_daily:";
+    private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    public boolean isUserForgetPassword(Integer appType, Integer businessType) {
+        return USER_APP_TYPE == appType && FORGET_PASSWORD_BUSINESS_TYPE == businessType;
+    }
+
+    /**
+     * 发送前校验,超限则抛出异常
+     */
+    public void checkLimit(String phone) {
+        if (phone == null || phone.trim().isEmpty()) {
+            return;
+        }
+        String key = buildDailyKey(phone.trim());
+        String countStr = stringRedisTemplate.opsForValue().get(key);
+        int count = 0;
+        if (countStr != null && !countStr.isEmpty()) {
+            try {
+                count = Integer.parseInt(countStr);
+            } catch (NumberFormatException e) {
+                log.warn("UserForgetPasswordSmsDailyLimiter invalid count, key={}, value={}", key, countStr);
+            }
+        }
+        if (count >= MAX_DAILY_COUNT) {
+            throw new SmsSendLimitException("今日忘记密码验证码发送次数已达上限,请明日再试");
+        }
+    }
+
+    /**
+     * 短信发送成功后累计次数
+     */
+    public void recordSend(String phone) {
+        if (phone == null || phone.trim().isEmpty()) {
+            return;
+        }
+        String key = buildDailyKey(phone.trim());
+        Long count = stringRedisTemplate.opsForValue().increment(key);
+        if (count != null && count == 1) {
+            stringRedisTemplate.expire(key, secondsUntilEndOfDay(), TimeUnit.SECONDS);
+        }
+    }
+
+    private String buildDailyKey(String phone) {
+        String date = LocalDate.now(ZONE).format(DateTimeFormatter.BASIC_ISO_DATE);
+        return KEY_PREFIX + phone + ":" + date;
+    }
+
+    private long secondsUntilEndOfDay() {
+        ZonedDateTime now = ZonedDateTime.now(ZONE);
+        ZonedDateTime endOfDay = now.toLocalDate().plusDays(1).atStartOfDay(ZONE);
+        return Math.max(Duration.between(now, endOfDay).getSeconds() + 1, 1);
+    }
+}