diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/api/feign/RemoterThirdPartySmsService.java b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/api/feign/RemoterThirdPartySmsService.java new file mode 100644 index 000000000..2dff4ba9d --- /dev/null +++ b/pjcloud-thirdparty/pjcloud-thirdparty-api/src/main/java/com/bsd/pjcloud/thirdparty/api/feign/RemoterThirdPartySmsService.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (c) 2018-2025, pjcloud All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: pjcloud + * + */ + +package com.bsd.pjcloud.thirdparty.api.feign; + +import com.bsd.pjcloud.common.core.constant.ServiceNameConstants; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.feign.annotation.NoToken; +import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; +import java.util.Map; + +@FeignClient(contextId = "remoterThirdPartySmsService", value = ServiceNameConstants.THIRDPARTY_SERVICE) +public interface RemoterThirdPartySmsService { + + // 简单发送 + @PostMapping("/sendMsg") + public void sendSimpleMsg(@RequestParam String mobile, @RequestParam String templateId, @RequestParam("clientIp") String clientIp); + + // 带参数发送 + @PostMapping("/sendMsgParams") + public void sendParamsMsg( + @RequestParam("mobile") String mobile,@RequestParam("templateId") String templateId, + @RequestBody Map params,@RequestParam("clientIp") String clientIp); +} diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/pom.xml b/pjcloud-thirdparty/pjcloud-thirdparty-biz/pom.xml index 34dfa75ed..c0e8c3e3e 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/pom.xml +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/pom.xml @@ -42,6 +42,10 @@ com.bsd pjcloud-common-data + + com.bsd + + com.bsd diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/SmsController.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/SmsController.java index 7be7fbc35..6b52a7a17 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/SmsController.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/SmsController.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.thirdparty.controller; import com.bsd.pjcloud.common.security.annotation.Inner; import com.bsd.pjcloud.thirdparty.service.SmsService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -12,22 +13,22 @@ import java.util.Map; @RequiredArgsConstructor public class SmsController { + private final SmsService smsService; // 简单发送 @PostMapping("/sendMsg") - public void sendVerifyCode(@RequestParam String mobile) { - smsService.sendSms(mobile, "20657"); + public void sendSimpleMsg(@RequestParam String mobile,@RequestParam String templateId,@RequestParam("clientIp") String clientIp) { + smsService.sendSms(mobile, templateId,clientIp); } // 带参数发送 @PostMapping("/sendMsgParams") @Inner(value = false) - public void sendOrderNotify( - @RequestParam String mobile, - @RequestBody Map params - ) { - smsService.sendSms(mobile, "20657", params); + public void sendParamsMsg( + @RequestParam("mobile") String mobile, @RequestParam("templateId") String templateId, + @RequestBody Map params, @RequestParam("clientIp") String clientIp) { + smsService.sendSms(mobile, templateId, params,clientIp); } } \ No newline at end of file diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java index db8811776..c6ce8cb0f 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/controller/ThirdPartyOAth2Cotroller.java @@ -7,32 +7,36 @@ import com.bsd.pjcloud.admin.api.entity.SysUser; import com.bsd.pjcloud.admin.api.feign.RemoteTokenService; import com.bsd.pjcloud.admin.api.feign.RemoteUserService; import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.core.util.WebUtils; import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.common.security.annotation.Inner; import com.bsd.pjcloud.thirdparty.dto.SHJNSSODTO; import com.bsd.pjcloud.thirdparty.dto.UrlTokenDTO; import com.bsd.pjcloud.thirdparty.dto.ZZDUserDTO; import com.bsd.pjcloud.thirdparty.entity.ThirdPartyUserEntity; +import com.bsd.pjcloud.thirdparty.service.SmsService; import com.bsd.pjcloud.thirdparty.service.ThirdPartyOAuth2Service; import com.bsd.pjcloud.thirdparty.service.ThirdPartyUserService; +import com.bsd.pjcloud.thirdparty.utils.SmsCodeUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.seata.spring.annotation.GlobalTransactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; +import java.util.concurrent.TimeUnit; @RestController @RequiredArgsConstructor @@ -49,6 +53,9 @@ public class ThirdPartyOAth2Cotroller { private final ThirdPartyUserService thirdPartyUserService; + private final SmsService smsService; + @Autowired + private RedisTemplate redisTemplate; // @Operation(summary = "浙政钉token" ) // @GetMapping("/zzd/sso" ) @@ -257,4 +264,21 @@ public class ThirdPartyOAth2Cotroller { } + + @PostMapping("/loginSms") + @Operation(summary = "短信登录认证") + @SysLog("短信登录认证") + @Inner(value = false) + public R loginSms(@RequestParam String mobilePhone) { + Map stringHashMap = new LinkedHashMap<>(); + String s = SmsCodeUtil.generateNumberCode(6); + stringHashMap.put("code",s); + stringHashMap.put("date","5"); + smsService.sendSms(mobilePhone, "20657",stringHashMap, WebUtils.getIP()); + + redisTemplate.opsForSet().add("smsCode_"+mobilePhone,s, 5, TimeUnit.MINUTES); + return R.ok(); + } + + } diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/SmsService.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/SmsService.java index e82f0ac20..24598d9c6 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/SmsService.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/SmsService.java @@ -5,12 +5,16 @@ import com.bsd.pjcloud.thirdparty.config.SmsConfig; import com.bsd.pjcloud.thirdparty.dto.sms.SmsRequest; import com.bsd.pjcloud.thirdparty.dto.sms.SmsResponse; import com.bsd.pjcloud.thirdparty.exception.SmsException; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; @Service @RequiredArgsConstructor @@ -19,13 +23,30 @@ public class SmsService { private final SmsConfig config; private final SmsHttpClient httpClient; + @Autowired + private RedisTemplate redisTemplate; + private static final String SMS_IP_LIMIT_PREFIX = "sms:ip_limit:"; + private static final String SMS_LIMIT_PREFIX = "sms:limit:"; + + /** + * 同一个ip,一分钟内只能请求五次 + */ + private static final int MAX_IP_REQUESTS = 5; + private static final int IP_LIMIT_EXPIRE_SECONDS = 60; + + private static final int LIMIT_EXPIRE_SECONDS = 60; + /** * 发送短信(最简版) * @param mobile 手机号 * @param templateId 模板ID */ - public void sendSms(String mobile, String templateId) { + public void sendSms(String mobile, String templateId, String clientIp) { validateMobile(mobile); + // IP限流检查 + checkIpLimit(clientIp); + + checkPhoneLimit(mobile); SmsConfig.TemplateConfig template = getTemplate(templateId); if (!template.getParams().isEmpty()) { @@ -41,8 +62,15 @@ public class SmsService { * @param templateId 模板ID * @param params 参数Map */ - public void sendSms(String mobile, String templateId, Map params) { + public void sendSms(String mobile, String templateId, Map params,String clientIp) { validateMobile(mobile); + //todo redisson分布式形式检查 + +// IP限流检查 + checkIpLimit(clientIp); + + checkPhoneLimit(mobile); + SmsConfig.TemplateConfig template = getTemplate(templateId); String content = replacePlaceholders(template.getContent(), params, template.getParams()); @@ -95,8 +123,55 @@ public class SmsService { } private void validateMobile(String mobile) { + if (!mobile.matches("^1[3-9]\\d{9}$")) { throw new SmsException("无效的手机号码: " + mobile); } } + + private void checkIpLimit(String clientIp) { + String ipLimitKey = SMS_IP_LIMIT_PREFIX + clientIp; + Long requestCount = redisTemplate.opsForValue().increment(ipLimitKey); + + if (requestCount == 1) { + // 首次设置时添加过期时间 + redisTemplate.expire(ipLimitKey, IP_LIMIT_EXPIRE_SECONDS, TimeUnit.SECONDS); + } + + if (requestCount != null && requestCount > MAX_IP_REQUESTS) { + throw new RuntimeException("当前IP请求过于频繁,请稍后再试"); + } + } + + private void checkPhoneLimit(String phoneNumber) { + String limitKey = SMS_LIMIT_PREFIX + phoneNumber; + if (Boolean.TRUE.equals(redisTemplate.hasKey(limitKey))) { + throw new RuntimeException("手机号操作过于频繁,请稍后再试"); + } + redisTemplate.opsForValue().set(limitKey, "1", LIMIT_EXPIRE_SECONDS, TimeUnit.SECONDS); + } + + + + private String getClientIp(HttpServletRequest request) { + String[] headersToCheck = { + "X-Forwarded-For", + "Proxy-Client-IP", + "WL-Proxy-Client-IP", + "HTTP_CLIENT_IP", + "HTTP_X_FORWARDED_FOR" + }; + + // 遍历代理头 + for (String header : headersToCheck) { + String ip = request.getHeader(header); + if (ip != null && !ip.isEmpty() && !"unknown".equalsIgnoreCase(ip)) { + return ip.split(",")[0].trim(); // 处理多级代理 + } + } + + // 最终回退方案 + return request.getRemoteAddr(); + } + } \ No newline at end of file diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/utils/SmsCodeUtil.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/utils/SmsCodeUtil.java new file mode 100644 index 000000000..5385baa72 --- /dev/null +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/utils/SmsCodeUtil.java @@ -0,0 +1,75 @@ +package com.bsd.pjcloud.thirdparty.utils; + +import java.security.SecureRandom; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * 短信验证码生成工具类 + */ +public class SmsCodeUtil { + + // 数字字符集 + private static final String NUMERIC_CHARS = "0123456789"; + + // 字母数字字符集(排除易混淆字符) + private static final String ALPHANUMERIC_CHARS = + "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"; + + // 使用安全随机数生成器 + private static final SecureRandom random = new SecureRandom(); + + /** + * 生成6位纯数字验证码 + */ + public static String generateNumericCode() { + return generateCustomCode(6, NUMERIC_CHARS); + } + + /** + * 生成6位字母数字验证码(排除易混淆字符) + */ + public static String generateAlphanumericCode() { + return generateCustomCode(6, ALPHANUMERIC_CHARS); + } + + /** + * 自定义验证码生成 + * @param length 验证码长度 + * @param charSet 使用的字符集 + */ + public static String generateCustomCode(int length, String charSet) { + if (length < 1) throw new IllegalArgumentException("验证码长度不能小于1"); + if (charSet == null || charSet.isEmpty()) { + throw new IllegalArgumentException("字符集不能为空"); + } + + return IntStream.range(0, length) + .map(i -> random.nextInt(charSet.length())) + .mapToObj(charSet::charAt) + .collect(Collectors.collectingAndThen( + Collectors.toList(), + list -> { + Collections.shuffle(list); + return list.stream(); + } + )) + .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append) + .toString(); + } + + /** + * 旧版本兼容:生成指定位数的纯数字验证码 + * @param length 验证码长度 (4-9) + */ + public static String generateNumberCode(int length) { + if (length < 4 || length > 9) { + throw new IllegalArgumentException("验证码长度需在4-9位之间"); + } + + int min = (int) Math.pow(10, length - 1); + int max = (int) Math.pow(10, length) - 1; + return String.valueOf(min + random.nextInt(max - min)); + } +} \ No newline at end of file