From d5ec858b06f77d82c27a32e4a23a1730594c61b8 Mon Sep 17 00:00:00 2001 From: bao <19271189822@163.com> Date: Wed, 24 Dec 2025 23:13:50 +0800 Subject: [PATCH] =?UTF-8?q?init:=20=E6=B7=BB=E5=8A=A0=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 32 +++ .../com/bao/dating/common/aliyun/SmsUtil.java | 149 +++++++++++++ .../java/com/bao/dating/util/EmailUtil.java | 178 +++++++++++++++ .../java/com/bao/dating/util/JwtUtil.java | 204 ++++++++++++++++++ .../java/com/bao/dating/util/MD5Util.java | 91 ++++++++ src/main/resources/application.yml | 26 +++ .../com/bao/dating/util/EmailAndSmsTest.java | 75 +++++++ 7 files changed, 755 insertions(+) create mode 100644 src/main/java/com/bao/dating/common/aliyun/SmsUtil.java create mode 100644 src/main/java/com/bao/dating/util/EmailUtil.java create mode 100644 src/main/java/com/bao/dating/util/JwtUtil.java create mode 100644 src/main/java/com/bao/dating/util/MD5Util.java create mode 100644 src/test/java/com/bao/dating/util/EmailAndSmsTest.java diff --git a/pom.xml b/pom.xml index 751755b..aa960d4 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,38 @@ 0.2.8 + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + com.aliyun + dysmsapi20170525 + 3.0.0 + + + + + org.springframework.boot + spring-boot-starter-mail + + diff --git a/src/main/java/com/bao/dating/common/aliyun/SmsUtil.java b/src/main/java/com/bao/dating/common/aliyun/SmsUtil.java new file mode 100644 index 0000000..1083218 --- /dev/null +++ b/src/main/java/com/bao/dating/common/aliyun/SmsUtil.java @@ -0,0 +1,149 @@ +package com.bao.dating.common.aliyun; + +import com.aliyun.dysmsapi20170525.Client; +import com.aliyun.dysmsapi20170525.models.SendSmsRequest; +import com.aliyun.dysmsapi20170525.models.SendSmsResponse; +import com.aliyun.teaopenapi.models.Config; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 阿里云短信服务工具类 + * @author KilLze + */ +@Data +@Slf4j +@Component +@ConfigurationProperties(prefix = "aliyun.sms") +public class SmsUtil { + + /** + * 访问密钥ID + */ + private String accessKeyId; + + /** + * 访问密钥Secret + */ + private String accessKeySecret; + + /** + * 短信服务区域节点 + */ + private String regionId = "cn-hangzhou"; + + /** + * 默认签名名称 + */ + private String signName; + + /** + * 默认模板代码 + */ + private String templateCode; + + /** + * 创建短信客户端 + * @return Client对象 + * @throws Exception 创建失败时抛出异常 + */ + private Client createClient() throws Exception { + Config config = new Config() + .setAccessKeyId(accessKeyId) + .setAccessKeySecret(accessKeySecret) + .setEndpoint("dysmsapi.aliyuncs.com"); + return new Client(config); + } + + /** + * 发送短信 + * @param phoneNumber 手机号码 + * @param templateCode 模板代码(如果为空则使用默认模板代码) + * @param templateParam 模板参数(JSON格式字符串,如:{"code":"123456"}) + * @return 是否发送成功 + */ + public boolean sendSms(String phoneNumber, String templateCode, String templateParam) { + try { + Client client = createClient(); + SendSmsRequest sendSmsRequest = new SendSmsRequest() + .setPhoneNumbers(phoneNumber) + .setSignName(signName) + .setTemplateCode(templateCode != null ? templateCode : this.templateCode) + .setTemplateParam(templateParam); + + SendSmsResponse response = client.sendSms(sendSmsRequest); + + if ("OK".equals(response.getBody().getCode())) { + log.info("短信发送成功,手机号:{},请求ID:{}", phoneNumber, response.getBody().getRequestId()); + return true; + } else { + log.error("短信发送失败,手机号:{},错误码:{},错误信息:{}", + phoneNumber, response.getBody().getCode(), response.getBody().getMessage()); + return false; + } + } catch (Exception e) { + log.error("发送短信异常,手机号:{},异常信息:{}", phoneNumber, e.getMessage(), e); + return false; + } + } + + /** + * 发送短信(使用默认模板代码) + * @param phoneNumber 手机号码 + * @param templateParam 模板参数(JSON格式字符串) + * @return 是否发送成功 + */ + public boolean sendSms(String phoneNumber, String templateParam) { + return sendSms(phoneNumber, null, templateParam); + } + + /** + * 发送验证码短信 + * @param phoneNumber 手机号码 + * @param code 验证码 + * @return 是否发送成功 + */ + public boolean sendVerificationCode(String phoneNumber, String code) { + String templateParam = String.format("{\"code\":\"%s\"}", code); + return sendSms(phoneNumber, templateParam); + } + + /** + * 发送验证码短信(使用指定模板代码) + * @param phoneNumber 手机号码 + * @param code 验证码 + * @param templateCode 模板代码 + * @return 是否发送成功 + */ + public boolean sendVerificationCode(String phoneNumber, String code, String templateCode) { + String templateParam = String.format("{\"code\":\"%s\"}", code); + return sendSms(phoneNumber, templateCode, templateParam); + } + + /** + * 发送短信(使用Map参数) + * @param phoneNumber 手机号码 + * @param templateCode 模板代码 + * @param params 模板参数Map + * @return 是否发送成功 + */ + public boolean sendSmsWithParams(String phoneNumber, String templateCode, Map params) { + StringBuilder jsonBuilder = new StringBuilder("{"); + boolean first = true; + for (Map.Entry entry : params.entrySet()) { + if (!first) { + jsonBuilder.append(","); + } + jsonBuilder.append("\"").append(entry.getKey()).append("\":\"") + .append(entry.getValue()).append("\""); + first = false; + } + jsonBuilder.append("}"); + return sendSms(phoneNumber, templateCode, jsonBuilder.toString()); + } +} + diff --git a/src/main/java/com/bao/dating/util/EmailUtil.java b/src/main/java/com/bao/dating/util/EmailUtil.java new file mode 100644 index 0000000..afcb813 --- /dev/null +++ b/src/main/java/com/bao/dating/util/EmailUtil.java @@ -0,0 +1,178 @@ +package com.bao.dating.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.File; + +/** + * 邮箱工具类 + * 提供邮件发送功能(支持普通文本邮件和HTML邮件) + * @author KilLze + */ +@Slf4j +@Component +public class EmailUtil { + + /** + * 邮件发送器(由Spring自动注入) + */ + private final JavaMailSender mailSender; + + /** + * 发件人邮箱地址(从配置中读取) + */ + private final String from; + + /** + * 构造函数,用于注入JavaMailSender和配置 + * @param mailSender 邮件发送器 + * @param from 发件人邮箱地址 + */ + public EmailUtil(JavaMailSender mailSender, @Value("${spring.mail.username}") String from) { + this.mailSender = mailSender; + this.from = from; + } + + /** + * 发送简单文本邮件 + * @param to 收件人邮箱地址 + * @param subject 邮件主题 + * @param text 邮件内容 + * @return 是否发送成功 + */ + public boolean sendSimpleMail(String to, String subject, String text) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(from); + message.setTo(to); + message.setSubject(subject); + message.setText(text); + mailSender.send(message); + log.info("简单邮件发送成功,收件人:{},主题:{}", to, subject); + return true; + } catch (Exception e) { + log.error("简单邮件发送失败,收件人:{},异常信息:{}", to, e.getMessage(), e); + return false; + } + } + + /** + * 发送HTML格式邮件 + * @param to 收件人邮箱地址 + * @param subject 邮件主题 + * @param htmlContent HTML内容 + * @return 是否发送成功 + */ + public boolean sendHtmlMail(String to, String subject, String htmlContent) { + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + helper.setFrom(from); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(htmlContent, true); + mailSender.send(message); + log.info("HTML邮件发送成功,收件人:{},主题:{}", to, subject); + return true; + } catch (MessagingException e) { + log.error("HTML邮件发送失败,收件人:{},异常信息:{}", to, e.getMessage(), e); + return false; + } + } + + /** + * 发送带附件的邮件 + * @param to 收件人邮箱地址 + * @param subject 邮件主题 + * @param text 邮件内容 + * @param attachmentPath 附件文件路径 + * @param attachmentName 附件显示名称 + * @return 是否发送成功 + */ + public boolean sendMailWithAttachment(String to, String subject, String text, + String attachmentPath, String attachmentName) { + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + helper.setFrom(from); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(text); + + File file = new File(attachmentPath); + if (file.exists()) { + helper.addAttachment(attachmentName, file); + } else { + log.warn("附件文件不存在:{}", attachmentPath); + return false; + } + + mailSender.send(message); + log.info("带附件邮件发送成功,收件人:{},主题:{},附件:{}", to, subject, attachmentName); + return true; + } catch (MessagingException e) { + log.error("带附件邮件发送失败,收件人:{},异常信息:{}", to, e.getMessage(), e); + return false; + } + } + + /** + * 发送验证码邮件 + * @param to 收件人邮箱地址 + * @param code 验证码 + * @return 是否发送成功 + */ + public boolean sendVerificationCode(String to, String code) { + String subject = "验证码"; + String htmlContent = String.format( + "" + + "

您的验证码

" + + "

验证码:%s

" + + "

验证码有效期为10分钟,请勿泄露给他人。

" + + "", code); + return sendHtmlMail(to, subject, htmlContent); + } + + /** + * 发送验证码邮件(自定义主题) + * @param to 收件人邮箱地址 + * @param code 验证码 + * @param subject 邮件主题 + * @return 是否发送成功 + */ + public boolean sendVerificationCode(String to, String code, String subject) { + String htmlContent = String.format( + "" + + "

您的验证码

" + + "

验证码:%s

" + + "

验证码有效期为10分钟,请勿泄露给他人。

" + + "", code); + return sendHtmlMail(to, subject, htmlContent); + } + + /** + * 批量发送邮件 + * @param toList 收件人邮箱地址数组 + * @param subject 邮件主题 + * @param text 邮件内容 + * @return 成功发送的数量 + */ + public int sendBatchMail(String[] toList, String subject, String text) { + int successCount = 0; + for (String to : toList) { + if (sendSimpleMail(to, subject, text)) { + successCount++; + } + } + log.info("批量邮件发送完成,总数:{},成功:{}", toList.length, successCount); + return successCount; + } +} + diff --git a/src/main/java/com/bao/dating/util/JwtUtil.java b/src/main/java/com/bao/dating/util/JwtUtil.java new file mode 100644 index 0000000..002b887 --- /dev/null +++ b/src/main/java/com/bao/dating/util/JwtUtil.java @@ -0,0 +1,204 @@ +package com.bao.dating.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT工具类 + * 提供JWT Token的生成、解析和验证功能 + * @author KilLze + */ +public class JwtUtil { + + /** + * 默认密钥(建议从配置文件读取) + */ + private static final String DEFAULT_SECRET = "dating-application-secret-key-for-jwt-token-generation-2025"; + + /** + * 默认过期时间(毫秒)- 7天 + */ + private static final long DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000L; + + /** + * 生成JWT Token + * @param subject 主题(通常是用户ID或用户名) + * @return JWT Token字符串 + */ + public static String generateToken(String subject) { + return generateToken(subject, DEFAULT_EXPIRATION); + } + + /** + * 生成JWT Token(自定义过期时间) + * @param subject 主题(通常是用户ID或用户名) + * @param expiration 过期时间(毫秒) + * @return JWT Token字符串 + */ + public static String generateToken(String subject, long expiration) { + return generateToken(subject, expiration, null); + } + + /** + * 生成JWT Token(带自定义claims) + * @param subject 主题(通常是用户ID或用户名) + * @param expiration 过期时间(毫秒) + * @param claims 自定义claims + * @return JWT Token字符串 + */ + public static String generateToken(String subject, long expiration, Map claims) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + SecretKey key = getSecretKey(); + + if (claims == null) { + claims = new HashMap<>(); + } + + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 生成JWT Token(使用自定义密钥) + * @param subject 主题 + * @param expiration 过期时间(毫秒) + * @param secret 自定义密钥 + * @return JWT Token字符串 + */ + public static String generateTokenWithSecret(String subject, long expiration, String secret) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); + + SecretKey key = getSecretKey(secret); + + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 从Token中获取Claims + * @param token JWT Token + * @return Claims对象 + */ + public static Claims getClaimsFromToken(String token) { + return getClaimsFromToken(token, DEFAULT_SECRET); + } + + /** + * 从Token中获取Claims(使用自定义密钥) + * @param token JWT Token + * @param secret 密钥 + * @return Claims对象 + */ + public static Claims getClaimsFromToken(String token, String secret) { + try { + SecretKey key = getSecretKey(secret); + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + throw new RuntimeException("解析JWT Token失败", e); + } + } + + /** + * 从Token中获取主题(通常是用户ID) + * @param token JWT Token + * @return 主题字符串 + */ + public static String getSubjectFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getSubject(); + } + + /** + * 验证Token是否有效 + * @param token JWT Token + * @return 是否有效 + */ + public static boolean validateToken(String token) { + return validateToken(token, DEFAULT_SECRET); + } + + /** + * 验证Token是否有效(使用自定义密钥) + * @param token JWT Token + * @param secret 密钥 + * @return 是否有效 + */ + public static boolean validateToken(String token, String secret) { + try { + Claims claims = getClaimsFromToken(token, secret); + return !isTokenExpired(claims); + } catch (Exception e) { + return false; + } + } + + /** + * 检查Token是否过期 + * @param claims Claims对象 + * @return 是否过期 + */ + private static boolean isTokenExpired(Claims claims) { + Date expiration = claims.getExpiration(); + return expiration.before(new Date()); + } + + /** + * 获取Token的过期时间 + * @param token JWT Token + * @return 过期时间 + */ + public static Date getExpirationDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 获取默认密钥的SecretKey对象 + * @return SecretKey对象 + */ + private static SecretKey getSecretKey() { + return getSecretKey(DEFAULT_SECRET); + } + + /** + * 根据字符串密钥生成SecretKey对象 + * @param secret 密钥字符串 + * @return SecretKey对象 + */ + private static SecretKey getSecretKey(String secret) { + // 确保密钥长度至少为256位(32字节)以支持HS256算法 + byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8); + if (keyBytes.length < 32) { + // 如果密钥太短,进行填充 + byte[] paddedKey = new byte[32]; + System.arraycopy(keyBytes, 0, paddedKey, 0, Math.min(keyBytes.length, 32)); + return Keys.hmacShaKeyFor(paddedKey); + } + return Keys.hmacShaKeyFor(keyBytes); + } +} + diff --git a/src/main/java/com/bao/dating/util/MD5Util.java b/src/main/java/com/bao/dating/util/MD5Util.java new file mode 100644 index 0000000..cfc5518 --- /dev/null +++ b/src/main/java/com/bao/dating/util/MD5Util.java @@ -0,0 +1,91 @@ +package com.bao.dating.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * MD5工具类 + * 提供MD5加密功能 + * @author KilLze + */ +public class MD5Util { + + /** + * 对字符串进行MD5加密 + * @param input 待加密的字符串 + * @return MD5加密后的32位小写字符串 + */ + public static String encrypt(String input) { + if (input == null || input.isEmpty()) { + return null; + } + + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(input.getBytes()); + return bytesToHex(digest); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5算法不可用", e); + } + } + + /** + * 对字符串进行MD5加密(带盐值) + * @param input 待加密的字符串 + * @param salt 盐值 + * @return MD5加密后的32位小写字符串 + */ + public static String encryptWithSalt(String input, String salt) { + if (input == null || input.isEmpty()) { + return null; + } + if (salt == null) { + salt = ""; + } + return encrypt(input + salt); + } + + /** + * 验证字符串与MD5值是否匹配 + * @param input 原始字符串 + * @param md5Hash MD5哈希值 + * @return 是否匹配 + */ + public static boolean verify(String input, String md5Hash) { + if (input == null || md5Hash == null) { + return false; + } + return encrypt(input).equalsIgnoreCase(md5Hash); + } + + /** + * 验证字符串与MD5值是否匹配(带盐值) + * @param input 原始字符串 + * @param salt 盐值 + * @param md5Hash MD5哈希值 + * @return 是否匹配 + */ + public static boolean verifyWithSalt(String input, String salt, String md5Hash) { + if (input == null || md5Hash == null) { + return false; + } + if (salt == null) { + salt = ""; + } + return encryptWithSalt(input, salt).equalsIgnoreCase(md5Hash); + } + + /** + * 将字节数组转换为十六进制字符串 + * @param bytes 字节数组 + * @return 十六进制字符串 + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 79d366e..8bcdeac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,6 +7,25 @@ spring: username: root password: JoyeeServe2025 driver-class-name: com.mysql.cj.jdbc.Driver + # 邮箱SMTP配置 + mail: + host: smtp.163.com # QQ邮箱SMTP服务器地址 + port: 465 # SMTP端口 + username: 19271189822@163.com # 发件人邮箱 + password: CAwXh39PXajy3fyH # 邮箱授权码 + default-encoding: UTF-8 + properties: + mail: + smtp: + auth: true + starttls: + enable: true + required: true + ssl: + enable: true # 使用587端口时设为false,使用465端口时设为true + connectiontimeout: 10000 # 连接超时时间(毫秒) + timeout: 10000 # 读取超时时间(毫秒) + writetimeout: 10000 # 写入超时时间(毫秒) # MyBatis 配置 mybatis: @@ -26,3 +45,10 @@ aliyun: accessKeyId: LTAI5t5vpcbCZwweNHEDDDaF secret: bBHBAPiCqGyVBHUv07348wsHXkKqrk scenes: antispam + # 阿里云短信服务配置 +# sms: +# access-key-id: LTAI5t5vpcbCZwweNHEDDDaF +# access-key-secret: bBHBAPiCqGyVBHUv07348wsHXkKqrk +# region-id: cn-hangzhou +# sign-name: +# template-code: SMS_123456789 diff --git a/src/test/java/com/bao/dating/util/EmailAndSmsTest.java b/src/test/java/com/bao/dating/util/EmailAndSmsTest.java new file mode 100644 index 0000000..49c80d9 --- /dev/null +++ b/src/test/java/com/bao/dating/util/EmailAndSmsTest.java @@ -0,0 +1,75 @@ +package com.bao.dating.util; + +import com.bao.dating.common.aliyun.SmsUtil; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.HashMap; +import java.util.Map; + +/** + * 邮箱和短信发送测试类 + * @author KilLze + */ +@SpringBootTest +public class EmailAndSmsTest { + + @Autowired + private EmailUtil emailUtil; + + @Autowired + private SmsUtil smsUtil; + + /** + * 测试发送简单文本邮件 + */ + @Test + public void testSendSimpleMail() { + String to = "n_1127@qq.com"; + String subject = "测试邮件 - 简单文本"; + String text = "这是一封测试邮件,用于测试简单文本邮件发送功能。\n\n如果您收到此邮件,说明邮件发送功能正常。"; + + boolean result = emailUtil.sendSimpleMail(to, subject, text); + System.out.println("简单邮件发送结果: " + (result ? "成功" : "失败")); + } + + /** + * 测试发送HTML格式邮件 + */ + @Test + public void testSendHtmlMail() { + String to = "n_1127@qq.com"; + String subject = "测试邮件 - HTML格式"; + String htmlContent = "" + + "

欢迎使用我们的服务

" + + "

这是一封HTML格式的测试邮件。

" + + "

邮件内容支持:

" + + "
    " + + "
  • HTML标签
  • " + + "
  • 样式设置
  • " + + "
  • 富文本内容
  • " + + "
" + + "

感谢您的使用!

" + + ""; + + boolean result = emailUtil.sendHtmlMail(to, subject, htmlContent); + System.out.println("HTML邮件发送结果: " + (result ? "成功" : "失败")); + } + + /** + * 测试发送验证码邮件 + */ + @Test + public void testSendVerificationCodeEmail() { + String to = "n_1127@qq.com"; + String code = "123456"; + + boolean result = emailUtil.sendVerificationCode(to, code); + System.out.println("验证码邮件发送结果: " + (result ? "成功" : "失败")); + System.out.println("验证码: " + code); + } + + +} +