diff --git a/pom.xml b/pom.xml index b7cf7a1..2698129 100644 --- a/pom.xml +++ b/pom.xml @@ -123,6 +123,14 @@ 0.12.3 runtime + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + 3.1.499 + + + diff --git a/src/main/java/com/onekeycall/videotablet/controller/SmsController.java b/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java similarity index 94% rename from src/main/java/com/onekeycall/videotablet/controller/SmsController.java rename to src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java index 2da4308..1132080 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/SmsController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java @@ -9,7 +9,6 @@ import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -18,7 +17,7 @@ import java.util.HashMap; import java.util.Map; @RestController -public class SmsController { +public class AliyunSmsController { private static final int LOGIN_CODE_TTL = 5; //引入 redis @@ -28,7 +27,7 @@ public class SmsController { @Autowired private RedisTemplate redisTemplate; - @GetMapping("/public/verify_code") +// @GetMapping("/public/verify_code") public Result getMethodName(@RequestParam("phone") String phone) { if (TextUtils.isEmpty(phone)) { return Result.error().message("phone number is empty"); @@ -42,17 +41,17 @@ public class SmsController { } if (TextUtils.isEmpty(oldCode)) { String code = SendSms.generatedcode(6); - return sendCode(phone, code, false); + return sendAliyunSms(phone, code, false); } else { - return sendCode(phone, oldCode, true); + return sendAliyunSms(phone, oldCode, true); } } else { String code = SendSms.generatedcode(6); - return sendCode(phone, code, false); + return sendAliyunSms(phone, code, false); } } - private Result sendCode(String phone, String code, boolean sent) { + private Result sendAliyunSms(String phone, String code, boolean sent) { try { com.aliyun.dysmsapi20170525.Client client = createClient(); com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest() @@ -120,4 +119,6 @@ public class SmsController { config.endpoint = "dysmsapi.aliyuncs.com"; return new com.aliyun.dysmsapi20170525.Client(config); } + + } diff --git a/src/main/java/com/onekeycall/videotablet/controller/LoginController.java b/src/main/java/com/onekeycall/videotablet/controller/LoginController.java index fa4f82b..fe3a9f1 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/LoginController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/LoginController.java @@ -95,11 +95,14 @@ public class LoginController { return new ResponseEntity<>("code is not same", HttpStatus.BAD_REQUEST); } try { - User user = userService.registerByPhone(phone, code, deviceId,new Date()); + User user = userService.registerByPhone(phone, code, deviceId, new Date()); TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId); + //返回给app保存,access_token用来加入header请求接口,refresh_token用来更换access_token return new ResponseEntity<>(tokenPair, HttpStatus.CREATED); } catch (RuntimeException e) { return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } finally { + redisTemplate.delete(phone); } } else { return new ResponseEntity<>("verify key is expired", HttpStatus.BAD_REQUEST); diff --git a/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java b/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java new file mode 100644 index 0000000..299d590 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java @@ -0,0 +1,197 @@ +package com.onekeycall.videotablet.controller; + +import com.onekeycall.videotablet.result.Result; +import com.onekeycall.videotablet.sms.SendSms; +import com.onekeycall.videotablet.utils.TextUtils; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.common.profile.ClientProfile; +import com.tencentcloudapi.common.profile.HttpProfile; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest; +import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class TencentSmsController { + private static final int LOGIN_CODE_TTL = 5; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private RedisTemplate redisTemplate; + + @GetMapping("/public/verify_code") + public Result getMethodName(@RequestParam(value = "phone") String phone) { + if (TextUtils.isEmpty(phone)) { + return Result.error().message("phone number is empty"); + } + Map map = (Map) redisTemplate.opsForValue().get(phone); + if (map != null) { + String oldCode = (String) map.get("code"); + long sentTime = (Long) map.get("sentAt"); + if (System.currentTimeMillis() - sentTime < Duration.ofMinutes(1).toMillis()) { + return Result.error().message("code has been sent, please try again after 1 minute"); + } + if (TextUtils.isEmpty(oldCode)) { + String code = SendSms.generatedcode(6); + return sendTencetSms(phone, code, false); + } else { + return sendTencetSms(phone, oldCode, true); + } + } else { + String code = SendSms.generatedcode(6); + return sendTencetSms(phone, code, false); + } + } + + public Result sendTencetSms(String phone, String code, boolean sent) { + String phoneNumber = ensurePlus86Prefix(phone); + try { + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId,SecretKey。 + // 为了保护密钥安全,建议将密钥设置在环境变量中或者配置文件中,请参考凭证管理 https://github.com/TencentCloud/tencentcloud-sdk-java?tab=readme-ov-file#%E5%87%AD%E8%AF%81%E7%AE%A1%E7%90%86。 + // 硬编码密钥到代码中有可能随代码泄露而暴露,有安全隐患,并不推荐。 + // SecretId、SecretKey 查询: https://console.cloud.tencent.com/cam/capi + Credential cred = new Credential("AKIDJXDqJk2963sUuAE7oIsQtAD4jANNBmCG", "znQq58i8NTQAhR2Qi1KRO9i5HG2jDWcX"); +// Credential cred = new Credential(System.getenv("TENCENTCLOUD_SECRET_ID"), System.getenv("TENCENTCLOUD_SECRET_KEY")); + + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + HttpProfile httpProfile = new HttpProfile(); + // 从3.0.96版本开始, 单独设置 HTTP 代理(无需要直接忽略) + // httpProfile.setProxyHost("真实代理ip"); + // httpProfile.setProxyPort(真实代理端口); + httpProfile.setReqMethod("GET"); // get请求(默认为post请求) + httpProfile.setConnTimeout(10); // 请求连接超时时间,单位为秒(默认60秒) + httpProfile.setWriteTimeout(10); // 设置写入超时时间,单位为秒(默认0秒) + httpProfile.setReadTimeout(10); // 设置读取超时时间,单位为秒(默认0秒) + + /* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */ + httpProfile.setEndpoint("sms.tencentcloudapi.com"); + + /* 非必要步骤: + * 实例化一个客户端配置对象,可以指定超时时间等配置 */ + ClientProfile clientProfile = new ClientProfile(); + /* SDK默认用TC3-HMAC-SHA256进行签名 + * 非必要请不要修改这个字段 */ + clientProfile.setSignMethod("HmacSHA256"); + clientProfile.setHttpProfile(httpProfile); + /* 实例化要请求产品(以sms为例)的client对象 + * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */ + SmsClient client = new SmsClient(cred, "ap-guangzhou", clientProfile); + /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 + * 您可以直接查询SDK源码确定接口有哪些属性可以设置 + * 属性可能是基本类型,也可能引用了另一个数据结构 + * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ + SendSmsRequest req = new SendSmsRequest(); + + /* 填充请求参数,这里request对象的成员变量即对应接口的入参 + * 您可以通过官网接口文档或跳转到request对象的定义处查看请求参数的定义 + * 基本类型的设置: + * 帮助链接: + * 短信控制台: https://console.cloud.tencent.com/smsv2 + * 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */ + + /* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */ + // 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看 + String sdkAppId = "1401023068"; + req.setSmsSdkAppId(sdkAppId); + + /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */ + // 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看 + String signName = "腾讯云"; + req.setSignName(signName); + + /* 模板 ID: 必须填写已审核通过的模板 ID */ + // 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看 + String templateId = "2496464"; + req.setTemplateId(templateId); + + /* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空 */ + String[] templateParamSet = {"{" + code + "}"}; + req.setTemplateParamSet(templateParamSet); + + /* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号] + * 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 */ + String[] phoneNumberSet = {phoneNumber}; + req.setPhoneNumberSet(phoneNumberSet); + + /* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ + String sessionContext = ""; + req.setSessionContext(sessionContext); + + /* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */ + String extendCode = ""; + req.setExtendCode(extendCode); + + /* 国内短信无需填写该项;国际/港澳台短信已申请独立 SenderId 需要填写该字段,默认使用公共 SenderId,无需填写该字段。注:月度使用量达到指定量级可申请独立 SenderId 使用,详情请联系 [腾讯云短信小助手](https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81)。*/ + String senderid = ""; + req.setSenderId(senderid); + + /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 + * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */ + SendSmsResponse res = client.SendSms(req); + + // 输出json格式的字符串回包 + System.out.println(SendSmsResponse.toJsonString(res)); + + // 也可以取出单个值,您可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义 + // System.out.println(res.getRequestId()); + + /* 当出现以下错误码时,快速解决方案参考 + * [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * [FailedOperation.TemplateIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.templateincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * [UnauthorizedOperation.SmsSdkAppIdVerifyFail](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunauthorizedoperation.smssdkappidverifyfail-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * [UnsupportedOperation.ContainDomesticAndInternationalPhoneNumber](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunsupportedoperation.containdomesticandinternationalphonenumber-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) + * 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms) + */ + String randomString = RandomStringUtils.randomAlphanumeric(32); + long now = System.currentTimeMillis(); + long expireAt = now + Duration.ofMinutes(LOGIN_CODE_TTL).toMillis(); + Map map = new HashMap<>(); + map.put("code", code); + map.put("sms", res.getRequestId()); + map.put("verifyKey", randomString); + map.put("sentAt", now); + map.put("expireAt", expireAt); + //4.保存验证码到Redis,并且设置有效期5分钟 + if (!sent) { + redisTemplate.opsForValue().set(phone, map, Duration.ofMinutes(5)); + } + return Result.ok().data(map); + } catch (TencentCloudSDKException e) { + e.printStackTrace(); + return Result.error().message(e.getMessage()); + } + } + + /** + * 检查并添加 +86 前缀 + * + * @param phoneNumber 输入的手机号码字符串 + * @return 如果原号码以 +86 开头则返回原字符串,否则返回添加 +86 后的新字符串 + */ + public static String ensurePlus86Prefix(String phoneNumber) { + // 处理 null 或空字符串的情况 + if (phoneNumber == null || phoneNumber.isEmpty()) { + return "+86"; + } + + // 检查是否以 "+86" 开头 + if (phoneNumber.startsWith("+86")) { + return phoneNumber; // 直接返回原号码 + } else { + return "+86" + phoneNumber; // 添加前缀 + } + } +} diff --git a/src/main/java/com/onekeycall/videotablet/dto/TokenPair.java b/src/main/java/com/onekeycall/videotablet/dto/TokenPair.java index 6e97eb9..d85a0bf 100644 --- a/src/main/java/com/onekeycall/videotablet/dto/TokenPair.java +++ b/src/main/java/com/onekeycall/videotablet/dto/TokenPair.java @@ -12,31 +12,31 @@ import lombok.Data; @Data public class TokenPair { // 访问令牌(用于API请求认证) - private String accessToken; + private String access_token; // 刷新令牌(用于获取新AccessToken) - private String refreshToken; + private String refresh_token; // AccessToken过期时间戳(毫秒) - private long accessExpiresAt; + private long access_expires; // RefreshToken过期时间戳(毫秒) - private long refreshExpiresAt; + private long refresh_expires; // 关联设备指纹(防御中间人攻击)[1](@ref) private String deviceId; /** * 全参数构造器(安全增强版) - * @param accessToken JWT格式访问令牌 - * @param refreshToken JWT格式刷新令牌 + * @param access_token JWT格式访问令牌 + * @param refresh_token JWT格式刷新令牌 * @param accessExpireMs AccessToken有效期(毫秒) * @param refreshExpireMs RefreshToken有效期(毫秒) * @param deviceId 客户端设备指纹 */ - public TokenPair(String accessToken, String refreshToken, + public TokenPair(String access_token, String refresh_token, long accessExpireMs, long refreshExpireMs, String deviceId) { - this.accessToken = accessToken; - this.refreshToken = refreshToken; - this.accessExpiresAt = System.currentTimeMillis() + accessExpireMs; - this.refreshExpiresAt = System.currentTimeMillis() + refreshExpireMs; + this.access_token = access_token; + this.refresh_token = refresh_token; + this.access_expires = System.currentTimeMillis() + accessExpireMs; + this.refresh_expires = System.currentTimeMillis() + refreshExpireMs; this.deviceId = deviceId; } @@ -62,7 +62,7 @@ public class TokenPair { * @return true=已过期,false=有效 */ public boolean isAccessExpired() { - return System.currentTimeMillis() > accessExpiresAt; + return System.currentTimeMillis() > access_expires; } /** @@ -70,7 +70,7 @@ public class TokenPair { * @return true=已过期,false=有效 */ public boolean isRefreshExpired() { - return System.currentTimeMillis() > refreshExpiresAt; + return System.currentTimeMillis() > refresh_expires; } /** @@ -83,8 +83,8 @@ public class TokenPair { return new TokenPair( newAccessToken, newRefreshToken, - this.accessExpiresAt - System.currentTimeMillis(), // 剩余时间延续 - this.refreshExpiresAt - System.currentTimeMillis(), + this.access_expires - System.currentTimeMillis(), // 剩余时间延续 + this.refresh_expires - System.currentTimeMillis(), this.deviceId // 保持设备一致性 ); }