Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e518d5d0a0 | ||
|
|
ffcea3104e | ||
|
|
af707e1bb4 |
@@ -0,0 +1,202 @@
|
|||||||
|
import java.util.Base64;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
|
public class KeyAnalyzer {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String privateKeyBase64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcxAwrhZF+izz1gxQKOr4jnC05obIBHbl0DzHOcd4CaaDV7Kv1NRJKi33JhdDsW4JGgu16e5Rtzq1VzU5VWz5EKgGL46maOFwCkngJUTP/LC3JVf/wtJmYCm8xNE8C7GNNIqKzYEvUOEfXqagpVvVrGsQ9FpzFp2rP9hBmHY3yizVyPX/uT9S+af5TRxiaItj3SSJGgloaEMrnKOpb/EH7JwPSS0liAyT/NxPfOyyZHc22AvaAIOE5y+0PMUIKPuIdfpOrej3LVpO1Arc2hSgmdB+YIPSiBVYPXa6AuAmil9mpbtSikQJ7Uu7lX4JyTW4QxQ06rPFKnFWVKkzivAElAgMBAAECggEAJd0AJ37iTlMpDQ90xqe7hvRQxAu256gbQ9nrqLY97g0/KIw6WEZSPakFX6gvdvb/NzKmUyAIEKGLoh6tXdZk6qfOqc/6BeK47nIcBfwT9/zerjNUVvn34w4aHyNINieMMHQ+Id8PUZmqWH+Euz9ilVTosuyEPwUZulLvUQqwXzU5VnwVghURbUhDd+ecBJACWgemRun6d5241PQXNYAdH1k7cETd8GfIi3qclhhJrxi7tu5tq4YGCXQIoz7HCLim7GIvT0M+FRgSw2EOrHnAQNFeQ/vQbP71ttLoTxehL6Se9dfWrV5OI+Y/T7vR2F84Qt0iNbaxyJGir7siKDFIwQKBgQDXDzinx3/TasplM78pR/0CtuuKr1Ch02LOPrTosJ1qf1OohxQThowhOTxMsBlgYSKu9s1QRffUUXEqYXxd2B6lzDKfggwO6U2XxIcxWeNow0xoFfqcXYSg7Ga2sCr9uhdwxIdFQNF7SNBpT8ht4fJrRX6mWY1nHybpyTDQ4xoQNQKBgQC6m+yiOoi5JD3zVSSJq/iq5DJPA5B4aoP+t5u9lp2Q7iVO0QI5ilBlEKGE4VOU0glnXlDTfuqEYooMY85ekl4WGb3AOT0PhLL2i+gO2nlWBzf4HPzB/hibjfyPyniRM03cHkG3HXucL7Sne6FwERcfjEqjUd2cdP1l89PNrq4rMQKBgQCMmjABSWYh8/y1I5rEQ4OAJdVjC3GdC1Xa35ZpVybjvLEWSpHunhW5lvD8dllw8LC7UTI0XDpGPqTM/4VO2YBYB2PFc0Gs8g0/v0ZgFpOeJ6kpl80MM/wFNemFYTIKRoMSv/psZY9PmfBgGcBBTuquBXZjDcNr+yr2yAm5V/DvTQKBgHqRi94KoF8q5N39IKCkqhJlDH5FkxDktYoKw2rFkPzuzuZz9gghRyj6wXxsG9/2DWMt2dzw0czehFoa/CO188KEadPmRKr6uCmkP2nyKhxNZX+8WnB5G2Sg4DD6BjMpBYz8+qDx5ozx8LDJTYI0V4HLPgMD9JGdbgsXGhlREOkhAoGALr6IQOXnviWNAhCdc7rrsaMLMPbLZ1wqzWtQUG1JxDobbpzEP4CW/mvW5pMn58mSBg5qbXhyDI4fFP0CPb98QIz2tGnIzYyFzdKmF5Z1N7X1OF9O+tsSqASoBZzTqB4fr/o4mz0s9JCeriBR2LWjsbsDU13DTLsfQpWbtOnIy70=";
|
||||||
|
|
||||||
|
System.out.println("========== 密钥分析开始 ==========");
|
||||||
|
System.out.println("密钥长度: " + privateKeyBase64.length());
|
||||||
|
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
|
||||||
|
System.out.println("解码后长度: " + keyBytes.length + " bytes");
|
||||||
|
|
||||||
|
// 打印前30字节
|
||||||
|
System.out.print("前30字节(hex): ");
|
||||||
|
for (int i = 0; i < 30 && i < keyBytes.length; i++) {
|
||||||
|
System.out.printf("%02X ", keyBytes[i]);
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
// 解析ASN.1结构
|
||||||
|
try {
|
||||||
|
parseASN1(keyBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("解析失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseASN1(byte[] keyBytes) throws Exception {
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(keyBytes);
|
||||||
|
|
||||||
|
// 读取SEQUENCE
|
||||||
|
int tag = bis.read();
|
||||||
|
System.out.println("第一个tag: 0x" + Integer.toHexString(tag));
|
||||||
|
|
||||||
|
if (tag == 0x30) {
|
||||||
|
System.out.println("这是SEQUENCE");
|
||||||
|
|
||||||
|
// 读取长度
|
||||||
|
int len = readASN1Length(bis);
|
||||||
|
System.out.println("SEQUENCE长度: " + len);
|
||||||
|
|
||||||
|
// 检查下一个tag
|
||||||
|
int nextTag = bis.read();
|
||||||
|
System.out.println("下一个tag: 0x" + Integer.toHexString(nextTag));
|
||||||
|
|
||||||
|
if (nextTag == 0x02) {
|
||||||
|
// INTEGER
|
||||||
|
int intLen = readASN1Length(bis);
|
||||||
|
byte[] versionBytes = new byte[intLen];
|
||||||
|
bis.read(versionBytes);
|
||||||
|
int version = new BigInteger(versionBytes).intValue();
|
||||||
|
System.out.println("版本INTEGER值: " + version);
|
||||||
|
|
||||||
|
// 检查这是PKCS#8还是PKCS#1
|
||||||
|
// PKCS#8: version=0后是SEQUENCE(算法标识符)
|
||||||
|
// PKCS#1: version=0后是INTEGER(modulus)
|
||||||
|
int afterVersionTag = bis.read();
|
||||||
|
System.out.println("版本后的tag: 0x" + Integer.toHexString(afterVersionTag));
|
||||||
|
|
||||||
|
if (afterVersionTag == 0x30) {
|
||||||
|
System.out.println(">>> 这是PKCS#8格式 (version后是SEQUENCE)");
|
||||||
|
|
||||||
|
// 解析PKCS#8
|
||||||
|
// AlgorithmIdentifier: SEQUENCE { OID, NULL }
|
||||||
|
int algLen = readASN1Length(bis);
|
||||||
|
System.out.println("AlgorithmIdentifier长度: " + algLen);
|
||||||
|
|
||||||
|
// 跳过AlgorithmIdentifier内容
|
||||||
|
byte[] algBytes = new byte[algLen];
|
||||||
|
bis.read(algBytes);
|
||||||
|
|
||||||
|
// 打印OID
|
||||||
|
System.out.print("OID bytes: ");
|
||||||
|
for (int i = 0; i < algBytes.length; i++) {
|
||||||
|
System.out.printf("%02X ", algBytes[i]);
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
// 下一个应该是OCTET STRING (包含私钥)
|
||||||
|
int octetTag = bis.read();
|
||||||
|
System.out.println("下一个tag: 0x" + Integer.toHexString(octetTag));
|
||||||
|
|
||||||
|
if (octetTag == 0x04) {
|
||||||
|
System.out.println("这是OCTET STRING (包含私钥数据)");
|
||||||
|
int octetLen = readASN1Length(bis);
|
||||||
|
System.out.println("OCTET STRING长度: " + octetLen);
|
||||||
|
|
||||||
|
// OCTET STRING内容是PKCS#1私钥
|
||||||
|
byte[] pkcs1Bytes = new byte[octetLen];
|
||||||
|
bis.read(pkcs1Bytes);
|
||||||
|
|
||||||
|
System.out.print("PKCS#1私钥前20字节: ");
|
||||||
|
for (int i = 0; i < 20; i++) {
|
||||||
|
System.out.printf("%02X ", pkcs1Bytes[i]);
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
// 解析PKCS#1私钥
|
||||||
|
ByteArrayInputStream pkcs1Stream = new ByteArrayInputStream(pkcs1Bytes);
|
||||||
|
int pkcs1Tag = pkcs1Stream.read();
|
||||||
|
System.out.println("PKCS#1第一个tag: 0x" + Integer.toHexString(pkcs1Tag));
|
||||||
|
|
||||||
|
if (pkcs1Tag == 0x30) {
|
||||||
|
int pkcs1Len = readASN1Length(pkcs1Stream);
|
||||||
|
System.out.println("PKCS#1 SEQUENCE长度: " + pkcs1Len);
|
||||||
|
|
||||||
|
// version
|
||||||
|
int vTag = pkcs1Stream.read();
|
||||||
|
int vLen = readASN1Length(pkcs1Stream);
|
||||||
|
byte[] vBytes = new byte[vLen];
|
||||||
|
pkcs1Stream.read(vBytes);
|
||||||
|
System.out.println("PKCS#1版本: " + new BigInteger(vBytes));
|
||||||
|
|
||||||
|
// 解析私钥参数
|
||||||
|
parsePKCS1(pkcs1Stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (afterVersionTag == 0x02) {
|
||||||
|
System.out.println(">>> 这是PKCS#1格式 (version后是INTEGER)");
|
||||||
|
|
||||||
|
// 继续解析PKCS#1
|
||||||
|
parsePKCS1(bis);
|
||||||
|
} else {
|
||||||
|
System.out.println(">>> 未知格式 (tag: 0x" + Integer.toHexString(afterVersionTag) + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parsePKCS1(ByteArrayInputStream bis) throws Exception {
|
||||||
|
System.out.println("\n========== 解析PKCS#1私钥参数 ==========");
|
||||||
|
|
||||||
|
// modulus
|
||||||
|
BigInteger modulus = readASN1Integer(bis);
|
||||||
|
System.out.println("modulus长度: " + modulus.bitLength() + " bits");
|
||||||
|
|
||||||
|
// publicExponent
|
||||||
|
BigInteger publicExponent = readASN1Integer(bis);
|
||||||
|
System.out.println("publicExponent: " + publicExponent);
|
||||||
|
|
||||||
|
// privateExponent
|
||||||
|
BigInteger privateExponent = readASN1Integer(bis);
|
||||||
|
System.out.println("privateExponent长度: " + privateExponent.bitLength() + " bits");
|
||||||
|
|
||||||
|
// prime1
|
||||||
|
BigInteger prime1 = readASN1Integer(bis);
|
||||||
|
System.out.println("prime1长度: " + prime1.bitLength() + " bits");
|
||||||
|
|
||||||
|
// prime2
|
||||||
|
BigInteger prime2 = readASN1Integer(bis);
|
||||||
|
System.out.println("prime2长度: " + prime2.bitLength() + " bits");
|
||||||
|
|
||||||
|
// 验证 prime1 * prime2 == modulus
|
||||||
|
BigInteger calculatedModulus = prime1.multiply(prime2);
|
||||||
|
boolean valid = calculatedModulus.equals(modulus);
|
||||||
|
System.out.println("\n验证 prime1 * prime2 == modulus: " + valid);
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
System.out.println(">>> 密钥数学关系不正确!这是无效的RSA私钥");
|
||||||
|
System.out.println("计算得到的modulus长度: " + calculatedModulus.bitLength() + " bits");
|
||||||
|
}
|
||||||
|
|
||||||
|
// exponent1
|
||||||
|
BigInteger exponent1 = readASN1Integer(bis);
|
||||||
|
System.out.println("exponent1长度: " + exponent1.bitLength() + " bits");
|
||||||
|
|
||||||
|
// exponent2
|
||||||
|
BigInteger exponent2 = readASN1Integer(bis);
|
||||||
|
System.out.println("exponent2长度: " + exponent2.bitLength() + " bits");
|
||||||
|
|
||||||
|
// coefficient
|
||||||
|
BigInteger coefficient = readASN1Integer(bis);
|
||||||
|
System.out.println("coefficient长度: " + coefficient.bitLength() + " bits");
|
||||||
|
|
||||||
|
System.out.println("\n========== 解析完成 ==========");
|
||||||
|
}
|
||||||
|
|
||||||
|
static int readASN1Length(ByteArrayInputStream bis) throws Exception {
|
||||||
|
int firstByte = bis.read();
|
||||||
|
if ((firstByte & 0x80) == 0) {
|
||||||
|
return firstByte;
|
||||||
|
}
|
||||||
|
int numBytes = firstByte & 0x7F;
|
||||||
|
int length = 0;
|
||||||
|
for (int i = 0; i < numBytes; i++) {
|
||||||
|
length = (length << 8) | (bis.read() & 0xFF);
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BigInteger readASN1Integer(ByteArrayInputStream bis) throws Exception {
|
||||||
|
int tag = bis.read();
|
||||||
|
if (tag != 0x02) throw new Exception("期望INTEGER tag: 0x02, 实际: 0x" + Integer.toHexString(tag));
|
||||||
|
int len = readASN1Length(bis);
|
||||||
|
byte[] data = new byte[len];
|
||||||
|
bis.read(data);
|
||||||
|
return new BigInteger(1, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,11 @@
|
|||||||
<version>1.0.0</version>
|
<version>1.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
|
||||||
<artifactId>gym-auth</artifactId>
|
<artifactId>gym-auth</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>Gym Auth</name>
|
<name>Gym Auth</name>
|
||||||
<description>Authentication module for Gym Management System</description>
|
<description>Phone Authentication Module - Phone Number Login Services</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -30,46 +29,93 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>gym-member</artifactId>
|
<artifactId>manage-sys</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>manage-sys</artifactId>
|
<artifactId>gym-member</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-validation</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-commons</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.25</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- 阿里云一键登录服务 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aliyun</groupId>
|
||||||
|
<artifactId>aliyun-java-sdk-dypnsapi</artifactId>
|
||||||
|
<version>1.2.12</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>com.aliyun</groupId>
|
||||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||||
<version>4.6.3</version>
|
<version>4.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>com.aliyun</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>dysmsapi20170525</artifactId>
|
||||||
<optional>true</optional>
|
<version>2.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>default-jar</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>jar</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.11.0</version>
|
<version>3.11.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>${java.version}</source>
|
<source>21</source>
|
||||||
<target>${java.version}</target>
|
<target>21</target>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package cn.novalon.gym.manage.auth.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AuthConfig {
|
||||||
|
}
|
||||||
+26
-2
@@ -4,20 +4,44 @@ import lombok.Data;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云短信配置属性类
|
||||||
|
*
|
||||||
|
* @author auto-generated
|
||||||
|
* @date 2026-06-20
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
|
@ConfigurationProperties(prefix = "alibaba.cloud.sms")
|
||||||
public class SmsProperties {
|
public class SmsProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问密钥ID
|
||||||
|
*/
|
||||||
private String accessKeyId;
|
private String accessKeyId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 访问密钥密钥
|
||||||
|
*/
|
||||||
private String accessKeySecret;
|
private String accessKeySecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信签名名称
|
||||||
|
*/
|
||||||
private String signName;
|
private String signName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信模板CODE
|
||||||
|
*/
|
||||||
private String templateCode;
|
private String templateCode;
|
||||||
|
|
||||||
private int codeLength = 4;
|
/**
|
||||||
|
* 短信验证码长度(默认6位)
|
||||||
|
*/
|
||||||
|
private int codeLength = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信验证码有效期(秒,默认300秒=5分钟)
|
||||||
|
*/
|
||||||
private long codeExpireSeconds = 300;
|
private long codeExpireSeconds = 300;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -7,6 +7,9 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号验证码登录请求DTO
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
+4
@@ -7,6 +7,10 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号一键登录请求DTO
|
||||||
|
* uniapp官方一键登录流程:前端通过云函数获取手机号,直接传给后端
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package cn.novalon.gym.manage.auth.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码请求DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SendCodeRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String phone;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package cn.novalon.gym.manage.auth.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信验证码登录请求DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class SmsLoginDto {
|
||||||
|
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@NotBlank(message = "验证码不能为空")
|
||||||
|
private String code;
|
||||||
|
}
|
||||||
+23
-1
@@ -1,15 +1,37 @@
|
|||||||
package cn.novalon.gym.manage.auth.service;
|
package cn.novalon.gym.manage.auth.service;
|
||||||
|
|
||||||
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
|
||||||
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
import cn.novalon.gym.manage.auth.dto.PhoneLoginDto;
|
||||||
|
import cn.novalon.gym.manage.auth.dto.PhoneCodeLoginDto;
|
||||||
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
|
import cn.novalon.gym.manage.auth.vo.PhoneLoginVO;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号认证服务接口
|
||||||
|
*/
|
||||||
public interface PhoneAuthService {
|
public interface PhoneAuthService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号一键登录(uniapp官方运营商认证)
|
||||||
|
* 已注册则直接登录,未注册则自动注册后登录
|
||||||
|
*
|
||||||
|
* @param request 登录请求
|
||||||
|
* @return 登录响应
|
||||||
|
*/
|
||||||
Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request);
|
Mono<PhoneLoginVO> oneClickLogin(PhoneLoginDto request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码(阿里云)
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return 是否发送成功
|
||||||
|
*/
|
||||||
Mono<Boolean> sendSmsCode(String phone);
|
Mono<Boolean> sendSmsCode(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号验证码登录(阿里云)
|
||||||
|
*
|
||||||
|
* @param request 登录请求
|
||||||
|
* @return 登录响应
|
||||||
|
*/
|
||||||
Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request);
|
Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request);
|
||||||
}
|
}
|
||||||
+29
-6
@@ -1,12 +1,35 @@
|
|||||||
package cn.novalon.gym.manage.auth.service;
|
package cn.novalon.gym.manage.auth.service;
|
||||||
|
|
||||||
import reactor.core.publisher.Mono;
|
/**
|
||||||
|
* 短信服务接口
|
||||||
|
*
|
||||||
|
* @author auto-generated
|
||||||
|
* @date 2026-06-20
|
||||||
|
*/
|
||||||
public interface SmsService {
|
public interface SmsService {
|
||||||
|
|
||||||
Mono<Boolean> sendVerificationCode(String phone);
|
/**
|
||||||
|
* 发送短信验证码
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return 发送结果
|
||||||
|
*/
|
||||||
|
boolean sendVerificationCode(String phone);
|
||||||
|
|
||||||
Mono<Boolean> verifyCode(String phone, String code);
|
/**
|
||||||
|
* 验证短信验证码
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @param code 验证码
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
boolean verifyCode(String phone, String code);
|
||||||
|
|
||||||
Mono<String> getVerificationCode(String phone);
|
/**
|
||||||
}
|
* 获取验证码(用于测试或特殊场景)
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return 验证码
|
||||||
|
*/
|
||||||
|
String getVerificationCode(String phone);
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -63,14 +63,14 @@ public class PhoneAuthServiceImpl implements PhoneAuthService {
|
|||||||
@Override
|
@Override
|
||||||
public Mono<Boolean> sendSmsCode(String phone) {
|
public Mono<Boolean> sendSmsCode(String phone) {
|
||||||
log.info("发送短信验证码, phone: {}", phone);
|
log.info("发送短信验证码, phone: {}", phone);
|
||||||
return smsService.sendVerificationCode(phone);
|
return Mono.fromCallable(() -> smsService.sendVerificationCode(phone));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request) {
|
public Mono<PhoneLoginVO> codeLogin(PhoneCodeLoginDto request) {
|
||||||
log.info("手机号验证码登录, phone: {}", request.getPhone());
|
log.info("手机号验证码登录, phone: {}", request.getPhone());
|
||||||
|
|
||||||
return smsService.verifyCode(request.getPhone(), request.getCode())
|
return Mono.fromCallable(() -> smsService.verifyCode(request.getPhone(), request.getCode()))
|
||||||
.flatMap(verified -> {
|
.flatMap(verified -> {
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
log.warn("验证码验证失败, phone: {}", request.getPhone());
|
log.warn("验证码验证失败, phone: {}", request.getPhone());
|
||||||
|
|||||||
+119
-169
@@ -1,198 +1,148 @@
|
|||||||
package cn.novalon.gym.manage.auth.service.impl;
|
package cn.novalon.gym.manage.auth.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.RandomUtil;
|
||||||
import cn.novalon.gym.manage.auth.config.SmsProperties;
|
import cn.novalon.gym.manage.auth.config.SmsProperties;
|
||||||
import cn.novalon.gym.manage.auth.service.SmsService;
|
import cn.novalon.gym.manage.auth.service.SmsService;
|
||||||
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
|
import cn.novalon.gym.manage.common.constant.RedisKeyConstants;
|
||||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import com.aliyuncs.CommonRequest;
|
import com.aliyun.dysmsapi20170525.Client;
|
||||||
import com.aliyuncs.CommonResponse;
|
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
|
||||||
import com.aliyuncs.DefaultAcsClient;
|
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
|
||||||
import com.aliyuncs.IAcsClient;
|
import com.aliyun.teaopenapi.models.Config;
|
||||||
import com.aliyuncs.exceptions.ClientException;
|
|
||||||
import com.aliyuncs.http.MethodType;
|
|
||||||
import com.aliyuncs.profile.DefaultProfile;
|
|
||||||
import com.aliyuncs.profile.IClientProfile;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.Duration;
|
||||||
import java.time.ZoneOffset;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信服务实现类
|
||||||
|
*
|
||||||
|
* @author auto-generated
|
||||||
|
* @date 2026-06-20
|
||||||
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SmsServiceImpl implements SmsService {
|
public class SmsServiceImpl implements SmsService {
|
||||||
|
|
||||||
private static final long SEND_INTERVAL_SECONDS = 60;
|
|
||||||
private static final long CODE_EXPIRE_SECONDS = 300;
|
|
||||||
|
|
||||||
private final SmsProperties smsProperties;
|
private final SmsProperties smsProperties;
|
||||||
private final RedisUtil redisUtil;
|
private final RedisUtil redisUtil;
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Boolean> sendVerificationCode(String phone) {
|
public boolean sendVerificationCode(String phone) {
|
||||||
log.info("发送短信验证码, phone: {}", phone);
|
log.info("发送短信验证码, phone: {}", phone);
|
||||||
|
|
||||||
String rateLimitKey = RedisKeyConstants.SMS_CODE + phone + ":rate_limit";
|
|
||||||
|
|
||||||
return redisUtil.get(rateLimitKey, Long.class)
|
|
||||||
.defaultIfEmpty(0L)
|
|
||||||
.flatMap(lastSendTime -> {
|
|
||||||
long currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
|
|
||||||
|
|
||||||
if (currentTime - lastSendTime < SEND_INTERVAL_SECONDS) {
|
|
||||||
long remainingSeconds = SEND_INTERVAL_SECONDS - (currentTime - lastSendTime);
|
|
||||||
log.warn("发送频率限制, phone: {}, 剩余时间: {}秒", phone, remainingSeconds);
|
|
||||||
return Mono.just(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
try {
|
|
||||||
IAcsClient client = createClient();
|
|
||||||
|
|
||||||
CommonRequest request = new CommonRequest();
|
|
||||||
request.setSysMethod(MethodType.POST);
|
|
||||||
request.setSysDomain("dypnsapi.aliyuncs.com");
|
|
||||||
request.setSysVersion("2017-05-25");
|
|
||||||
request.setSysAction("SendSmsVerifyCode");
|
|
||||||
request.putQueryParameter("PhoneNumber", phone);
|
|
||||||
request.putQueryParameter("SignName", smsProperties.getSignName());
|
|
||||||
request.putQueryParameter("TemplateCode", smsProperties.getTemplateCode());
|
|
||||||
request.putQueryParameter("TemplateParam", "{\"code\":\"##code##\",\"min\":\"5\"}");
|
|
||||||
request.putQueryParameter("ReturnVerifyCode", "true");
|
|
||||||
|
|
||||||
log.info("阿里云号码认证请求参数 - signName: {}, templateCode: {}, templateParam: {}",
|
|
||||||
smsProperties.getSignName(),
|
|
||||||
smsProperties.getTemplateCode(),
|
|
||||||
"{\"code\":\"##code##\",\"min\":\"5\"}");
|
|
||||||
|
|
||||||
CommonResponse response = client.getCommonResponse(request);
|
|
||||||
String responseData = response.getData();
|
|
||||||
log.info("阿里云号码认证原始响应: {}", responseData);
|
|
||||||
|
|
||||||
JsonNode jsonNode = objectMapper.readTree(responseData);
|
|
||||||
|
|
||||||
JsonNode codeNode = jsonNode.get("Code");
|
|
||||||
if (codeNode == null) {
|
|
||||||
log.error("阿里云响应中找不到Code字段, 原始响应: {}", responseData);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String code = codeNode.asText();
|
|
||||||
|
|
||||||
if ("OK".equals(code)) {
|
|
||||||
JsonNode requestIdNode = jsonNode.get("RequestId");
|
|
||||||
String requestId = requestIdNode != null ? requestIdNode.asText() : "unknown";
|
|
||||||
log.info("短信验证码发送成功, phone: {}, requestId: {}", phone, requestId);
|
|
||||||
|
|
||||||
// 提取验证码并存入Redis
|
|
||||||
JsonNode modelNode = jsonNode.get("Model");
|
|
||||||
if (modelNode != null) {
|
|
||||||
JsonNode verifyCodeNode = modelNode.get("VerifyCode");
|
|
||||||
if (verifyCodeNode != null) {
|
|
||||||
String verifyCode = verifyCodeNode.asText();
|
|
||||||
String smsCodeKey = RedisKeyConstants.SMS_CODE + phone;
|
|
||||||
redisUtil.setWithExpire(smsCodeKey, verifyCode, CODE_EXPIRE_SECONDS).subscribe();
|
|
||||||
log.info("验证码已存入Redis, phone: {}, key: {}, expire: {}秒", phone, smsCodeKey, CODE_EXPIRE_SECONDS);
|
|
||||||
} else {
|
|
||||||
log.warn("响应中未找到Model.VerifyCode字段, 原始响应: {}", responseData);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.warn("响应中未找到Model字段, 原始响应: {}", responseData);
|
|
||||||
}
|
|
||||||
|
|
||||||
redisUtil.setWithExpire(rateLimitKey, currentTime, SEND_INTERVAL_SECONDS).subscribe();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonNode messageNode = jsonNode.get("Message");
|
|
||||||
String message = messageNode != null ? messageNode.asText() : "unknown";
|
|
||||||
log.error("短信验证码发送失败, phone: {}, code: {}, message: {}", phone, code, message);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("发送短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Boolean> verifyCode(String phone, String code) {
|
|
||||||
log.info("验证短信验证码, phone: {}", phone);
|
|
||||||
|
|
||||||
return Mono.fromCallable(() -> {
|
|
||||||
try {
|
|
||||||
IAcsClient client = createClient();
|
|
||||||
|
|
||||||
CommonRequest request = new CommonRequest();
|
|
||||||
request.setSysMethod(MethodType.POST);
|
|
||||||
request.setSysDomain("dypnsapi.aliyuncs.com");
|
|
||||||
request.setSysVersion("2017-05-25");
|
|
||||||
request.setSysAction("CheckSmsVerifyCode");
|
|
||||||
request.putQueryParameter("PhoneNumber", phone);
|
|
||||||
request.putQueryParameter("VerifyCode", code);
|
|
||||||
|
|
||||||
log.info("阿里云号码认证核验参数 - phone: {}, code: {}", phone, code);
|
|
||||||
|
|
||||||
CommonResponse response = client.getCommonResponse(request);
|
|
||||||
String responseData = response.getData();
|
|
||||||
log.info("阿里云号码认证核验原始响应: {}", responseData);
|
|
||||||
|
|
||||||
JsonNode jsonNode = objectMapper.readTree(responseData);
|
|
||||||
|
|
||||||
JsonNode codeNode = jsonNode.get("Code");
|
|
||||||
if (codeNode == null) {
|
|
||||||
log.error("阿里云核验响应中找不到Code字段, 原始响应: {}", responseData);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String responseCode = codeNode.asText();
|
|
||||||
JsonNode modelNode = jsonNode.get("Model");
|
|
||||||
JsonNode verifyResultNode = modelNode != null ? modelNode.get("VerifyResult") : null;
|
|
||||||
boolean verifyResult = verifyResultNode != null && "PASS".equals(verifyResultNode.asText());
|
|
||||||
|
|
||||||
if ("OK".equals(responseCode) && verifyResult) {
|
|
||||||
log.info("验证码验证成功, phone: {}", phone);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonNode messageNode = jsonNode.get("Message");
|
|
||||||
String message = messageNode != null ? messageNode.asText() : "unknown";
|
|
||||||
log.warn("验证码验证失败, phone: {}, code: {}, message: {}, result: {}",
|
|
||||||
phone, responseCode, message, verifyResult);
|
|
||||||
return false;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("验证短信验证码异常, phone: {}, 异常信息: {}", phone, e.getMessage(), e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<String> getVerificationCode(String phone) {
|
|
||||||
try {
|
try {
|
||||||
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
// 生成验证码
|
||||||
return redisUtil.get(cacheKey, String.class);
|
String code = generateCode();
|
||||||
|
|
||||||
|
// 发送短信
|
||||||
|
boolean sent = sendSms(phone, code);
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
// 将验证码存入Redis,5分钟过期
|
||||||
|
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
||||||
|
redisUtil.setWithExpire(cacheKey, code, smsProperties.getCodeExpireSeconds())
|
||||||
|
.doOnSuccess(result -> log.info("验证码已缓存, phone: {}, code: {}, expire: {}s",
|
||||||
|
phone, code, smsProperties.getCodeExpireSeconds()))
|
||||||
|
.block();
|
||||||
|
|
||||||
|
log.info("短信验证码发送成功, phone: {}", phone);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("短信验证码发送失败, phone: {}", phone);
|
||||||
|
return false;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("获取验证码异常, phone: {}", phone, e);
|
log.error("发送短信验证码异常, phone: {}", phone, e);
|
||||||
return Mono.empty();
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IAcsClient createClient() throws ClientException {
|
@Override
|
||||||
IClientProfile profile = DefaultProfile.getProfile(
|
public boolean verifyCode(String phone, String code) {
|
||||||
"cn-hangzhou",
|
log.info("验证短信验证码, phone: {}", phone);
|
||||||
smsProperties.getAccessKeyId(),
|
|
||||||
smsProperties.getAccessKeySecret());
|
try {
|
||||||
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", "Dypnsapi", "dypnsapi.aliyuncs.com");
|
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
||||||
return new DefaultAcsClient(profile);
|
String cachedCode = redisUtil.get(cacheKey, String.class).block();
|
||||||
|
|
||||||
|
if (cachedCode == null) {
|
||||||
|
log.warn("验证码已过期或不存在, phone: {}", phone);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedCode.equals(code)) {
|
||||||
|
// 验证成功后删除验证码
|
||||||
|
redisUtil.delete(cacheKey).block();
|
||||||
|
log.info("验证码验证成功, phone: {}", phone);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("验证码不匹配, phone: {}, input: {}, cached: {}", phone, code, cachedCode);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("验证短信验证码异常, phone: {}", phone, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public String getVerificationCode(String phone) {
|
||||||
|
try {
|
||||||
|
String cacheKey = RedisKeyConstants.SMS_CODE + phone;
|
||||||
|
return redisUtil.get(cacheKey, String.class).block();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取验证码异常, phone: {}", phone, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成验证码
|
||||||
|
*/
|
||||||
|
private String generateCode() {
|
||||||
|
return RandomUtil.randomNumbers(smsProperties.getCodeLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信
|
||||||
|
*/
|
||||||
|
private boolean sendSms(String phone, String code) throws Exception {
|
||||||
|
log.info("调用阿里云短信API发送短信, phone: {}, code: {}", phone, code);
|
||||||
|
|
||||||
|
// 创建阿里云短信客户端
|
||||||
|
Config config = new Config()
|
||||||
|
.setAccessKeyId(smsProperties.getAccessKeyId())
|
||||||
|
.setAccessKeySecret(smsProperties.getAccessKeySecret())
|
||||||
|
.setEndpoint("dysmsapi.aliyuncs.com");
|
||||||
|
|
||||||
|
Client client = new Client(config);
|
||||||
|
|
||||||
|
// 构建发送短信请求
|
||||||
|
SendSmsRequest request = new SendSmsRequest()
|
||||||
|
.setPhoneNumbers(phone)
|
||||||
|
.setSignName(smsProperties.getSignName())
|
||||||
|
.setTemplateCode(smsProperties.getTemplateCode())
|
||||||
|
.setTemplateParam("{\"code\":\"" + code + "\"}");
|
||||||
|
|
||||||
|
// 发送短信
|
||||||
|
SendSmsResponse response = client.sendSms(request);
|
||||||
|
|
||||||
|
log.info("阿里云短信API响应, phone: {}, code: {}, message: {}",
|
||||||
|
phone, response.getBody().getCode(), response.getBody().getMessage());
|
||||||
|
|
||||||
|
// 判断发送结果
|
||||||
|
if ("OK".equals(response.getBody().getCode())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error("阿里云短信发送失败, phone: {}, code: {}, message: {}",
|
||||||
|
phone, response.getBody().getCode(), response.getBody().getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import lombok.Builder;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号一键登录响应VO
|
||||||
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|||||||
+4
@@ -1 +1,5 @@
|
|||||||
|
cn.novalon.gym.manage.auth.handler.PhoneAuthHandler
|
||||||
|
cn.novalon.gym.manage.auth.service.impl.PhoneAuthServiceImpl
|
||||||
|
cn.novalon.gym.manage.auth.service.impl.SmsServiceImpl
|
||||||
|
cn.novalon.gym.manage.auth.config.AuthConfig
|
||||||
cn.novalon.gym.manage.auth.config.SmsProperties
|
cn.novalon.gym.manage.auth.config.SmsProperties
|
||||||
-43
@@ -1,43 +0,0 @@
|
|||||||
package cn.novalon.gym.manage.checkIn.config;
|
|
||||||
|
|
||||||
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.reactive.HandlerMapping;
|
|
||||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
|
||||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
|
||||||
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class WebSocketConfig {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private MyWebSocketHandler myWebSocketHandler;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册 WebSocket 路由映射
|
|
||||||
* 路径对应前端连接的 ws://xxx/webSocket/checkIn
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public HandlerMapping webSocketMapping() {
|
|
||||||
Map<String, WebSocketHandler> map = new HashMap<>();
|
|
||||||
map.put("/webSocket/checkIn", myWebSocketHandler);
|
|
||||||
|
|
||||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
|
||||||
mapping.setUrlMap(map);
|
|
||||||
mapping.setOrder(10); // 设置优先级
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册 WebSocket 处理器适配器(必须)
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
public WebSocketHandlerAdapter handlerAdapter() {
|
|
||||||
return new WebSocketHandlerAdapter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -206,4 +206,4 @@ public class SignInRecord {
|
|||||||
public void restore() {
|
public void restore() {
|
||||||
this.isDelete = false;
|
this.isDelete = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-5
@@ -2,8 +2,6 @@ package cn.novalon.gym.manage.checkIn.handler;
|
|||||||
|
|
||||||
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
|
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
|
||||||
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
|
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
|
||||||
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
|
|
||||||
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
|
|
||||||
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
import cn.novalon.gym.manage.sys.util.AuthUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -14,7 +12,6 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -39,7 +36,6 @@ public class CheckInHandler {
|
|||||||
public Mono<ServerResponse> checkIn(ServerRequest request) {
|
public Mono<ServerResponse> checkIn(ServerRequest request) {
|
||||||
|
|
||||||
Long memberId = authUtil.getMemberIdOrThrow(request);
|
Long memberId = authUtil.getMemberIdOrThrow(request);
|
||||||
|
|
||||||
return request.bodyToMono(Map.class)
|
return request.bodyToMono(Map.class)
|
||||||
.flatMap(body -> {
|
.flatMap(body -> {
|
||||||
String qrContent = (String) body.get("qrContent");
|
String qrContent = (String) body.get("qrContent");
|
||||||
@@ -194,4 +190,4 @@ public class CheckInHandler {
|
|||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(Map.of("code", 200, "message", "success", "data", stats)));
|
.bodyValue(Map.of("code", 200, "message", "success", "data", stats)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -78,4 +78,4 @@ public interface ICheckInService {
|
|||||||
* @return 签到统计VO
|
* @return 签到统计VO
|
||||||
*/
|
*/
|
||||||
Mono<SignInStatsVO> getDailySignInStats(LocalDate date);
|
Mono<SignInStatsVO> getDailySignInStats(LocalDate date);
|
||||||
}
|
}
|
||||||
+2
-2
@@ -2,8 +2,6 @@ package cn.novalon.gym.manage.checkIn.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.json.JSONUtil;
|
import cn.hutool.json.JSONUtil;
|
||||||
import cn.hutool.extra.qrcode.QrCodeUtil;
|
|
||||||
import cn.hutool.extra.qrcode.QrConfig;
|
|
||||||
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
|
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
|
||||||
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
|
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
|
||||||
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
|
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
|
||||||
@@ -21,6 +19,8 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
|||||||
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
import cn.novalon.gym.manage.member.enums.MemberCardType;
|
||||||
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
||||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||||
|
import cn.hutool.extra.qrcode.QrCodeUtil;
|
||||||
|
import cn.hutool.extra.qrcode.QrConfig;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|||||||
+1
-1
@@ -199,4 +199,4 @@ public class MyWebSocketHandler implements WebSocketHandler {
|
|||||||
cleanupTimeoutConnections();
|
cleanupTimeoutConnections();
|
||||||
return qrContentToSink.size();
|
return qrContentToSink.size();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,11 +35,6 @@
|
|||||||
<artifactId>manage-common</artifactId>
|
<artifactId>manage-common</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
|
||||||
<artifactId>manage-sys</artifactId>
|
|
||||||
<version>${project.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.novalon.gym.manage</groupId>
|
<groupId>cn.novalon.gym.manage</groupId>
|
||||||
<artifactId>manage-db</artifactId>
|
<artifactId>manage-db</artifactId>
|
||||||
@@ -86,8 +81,8 @@
|
|||||||
<artifactId>gym-member</artifactId>
|
<artifactId>gym-member</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- ZXing二维码生成库 -->
|
<!-- ZXing QR Code依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.zxing</groupId>
|
<groupId>com.google.zxing</groupId>
|
||||||
<artifactId>core</artifactId>
|
<artifactId>core</artifactId>
|
||||||
@@ -98,20 +93,14 @@
|
|||||||
<artifactId>javase</artifactId>
|
<artifactId>javase</artifactId>
|
||||||
<version>3.5.3</version>
|
<version>3.5.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 阿里云OSS SDK -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.aliyun.oss</groupId>
|
|
||||||
<artifactId>aliyun-sdk-oss</artifactId>
|
|
||||||
<version>3.17.1</version>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.4.2</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
package cn.novalon.gym.manage.groupcourse.util;
|
package cn.novalon.gym.manage.groupcourse.util;
|
||||||
|
|
||||||
import com.aliyun.oss.OSS;
|
//import com.aliyun.oss.OSS;
|
||||||
import com.aliyun.oss.OSSClientBuilder;
|
//import com.aliyun.oss.OSSClientBuilder;
|
||||||
import com.aliyun.oss.model.PutObjectRequest;
|
//import com.aliyun.oss.model.PutObjectRequest;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|||||||
-16
@@ -71,22 +71,6 @@ public class Member extends BaseEntity {
|
|||||||
@Column("official_open_id")
|
@Column("official_open_id")
|
||||||
private String officialOpenId;
|
private String officialOpenId;
|
||||||
|
|
||||||
// 阿里云号码认证OpenID
|
|
||||||
@Column("dypns_open_id")
|
|
||||||
private String dypnsOpenId;
|
|
||||||
|
|
||||||
// 身份证号码(AES加密存储)
|
|
||||||
@Column("id_card")
|
|
||||||
private String idCard;
|
|
||||||
|
|
||||||
// 真实姓名(AES加密存储)
|
|
||||||
@Column("real_name")
|
|
||||||
private String realName;
|
|
||||||
|
|
||||||
// 注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权
|
|
||||||
@Column("register_channel")
|
|
||||||
private String registerChannel;
|
|
||||||
|
|
||||||
// 软删除
|
// 软删除
|
||||||
@Column("is_deleted")
|
@Column("is_deleted")
|
||||||
private Boolean isDeleted;
|
private Boolean isDeleted;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,5 @@
|
|||||||
package cn.novalon.gym.manage.member.service.impl;
|
package cn.novalon.gym.manage.member.service.impl;
|
||||||
|
|
||||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||||
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
|
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
|
||||||
@@ -16,6 +15,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
|
|||||||
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
|
||||||
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
import cn.novalon.gym.manage.member.service.IMemberCardService;
|
||||||
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
|
||||||
|
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<artifactId>gym-auth</artifactId>
|
<artifactId>gym-auth</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
|||||||
+1
-2
@@ -1,11 +1,10 @@
|
|||||||
package cn.novalon.gym.manage.app.config;
|
package cn.novalon.gym.manage.app.config;
|
||||||
|
|
||||||
|
|
||||||
import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler;
|
|
||||||
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
|
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
|
||||||
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
|
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
|
||||||
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
||||||
|
import cn.novalon.gym.manage.auth.handler.PhoneAuthHandler;
|
||||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler;
|
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler;
|
||||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
|
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
|
||||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler;
|
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseRecommendHandler;
|
||||||
|
|||||||
@@ -36,11 +36,3 @@ logging:
|
|||||||
cn.novalon.manage: DEBUG
|
cn.novalon.manage: DEBUG
|
||||||
org.springframework.r2dbc: DEBUG
|
org.springframework.r2dbc: DEBUG
|
||||||
org.springframework.web: TRACE
|
org.springframework.web: TRACE
|
||||||
|
|
||||||
alibaba:
|
|
||||||
cloud:
|
|
||||||
sms:
|
|
||||||
access-key-id: LTAI5t8GhorWLu5WkEx8MDZz
|
|
||||||
access-key-secret: jNDwb9IHvTIESUezLYHZRT5c5NEaCz
|
|
||||||
sign-name: 云渚科技验证平台
|
|
||||||
template-code: 100001
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ wechat:
|
|||||||
|
|
||||||
# 手机号加密配置
|
# 手机号加密配置
|
||||||
phone-encryption:
|
phone-encryption:
|
||||||
secret-key: ${PHONE_ENCRYPTION_SECRET_KEY:P8539ANjWJWsRbVHZKhM8Q==}
|
secret-key: ${PHONE_ENCRYPTION_SECRET_KEY}
|
||||||
iv: ${PHONE_ENCRYPTION_IV:3tHp07uMRYh1xKsIXvYJMA==}
|
iv: ${PHONE_ENCRYPTION_IV}
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
|
|||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package cn.novalon.gym.manage.common.config;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Component
|
|
||||||
@ConfigurationProperties(prefix = "auth")
|
|
||||||
public class AuthConfig {
|
|
||||||
|
|
||||||
private String accessKeyId;
|
|
||||||
|
|
||||||
private String accessKeySecret;
|
|
||||||
|
|
||||||
private Integer tokenExpireSeconds = 86400;
|
|
||||||
|
|
||||||
private Integer refreshTokenExpireSeconds = 604800;
|
|
||||||
}
|
|
||||||
-54
@@ -1,54 +0,0 @@
|
|||||||
package cn.novalon.gym.manage.common.response;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Builder
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class Result<T> {
|
|
||||||
|
|
||||||
private Integer code;
|
|
||||||
|
|
||||||
private String message;
|
|
||||||
|
|
||||||
private T data;
|
|
||||||
|
|
||||||
private Long timestamp;
|
|
||||||
|
|
||||||
public static <T> Result<T> success(T data) {
|
|
||||||
return Result.<T>builder()
|
|
||||||
.code(200)
|
|
||||||
.message("success")
|
|
||||||
.data(data)
|
|
||||||
.timestamp(System.currentTimeMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> Result<T> success(String message) {
|
|
||||||
return Result.<T>builder()
|
|
||||||
.code(200)
|
|
||||||
.message(message)
|
|
||||||
.timestamp(System.currentTimeMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> Result<T> fail(Integer code, String message) {
|
|
||||||
return Result.<T>builder()
|
|
||||||
.code(code)
|
|
||||||
.message(message)
|
|
||||||
.timestamp(System.currentTimeMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> Result<T> fail(String message) {
|
|
||||||
return Result.<T>builder()
|
|
||||||
.code(500)
|
|
||||||
.message(message)
|
|
||||||
.timestamp(System.currentTimeMillis())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
package cn.novalon.gym.manage.common.util;
|
|
||||||
|
|
||||||
import cn.novalon.gym.manage.common.config.JwtProperties;
|
|
||||||
import io.jsonwebtoken.Claims;
|
|
||||||
import io.jsonwebtoken.Jwts;
|
|
||||||
import io.jsonwebtoken.security.Keys;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class JwtUtil {
|
|
||||||
|
|
||||||
private final JwtProperties jwtProperties;
|
|
||||||
|
|
||||||
private SecretKey getSigningKey() {
|
|
||||||
String secret = jwtProperties.getSecret();
|
|
||||||
if (secret.length() < 32) {
|
|
||||||
StringBuilder sb = new StringBuilder(secret);
|
|
||||||
while (sb.length() < 32) {
|
|
||||||
sb.append(secret);
|
|
||||||
}
|
|
||||||
secret = sb.substring(0, 32);
|
|
||||||
}
|
|
||||||
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateToken(String userId, String phone) {
|
|
||||||
Date now = new Date();
|
|
||||||
Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration());
|
|
||||||
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
|
||||||
claims.put("userId", userId);
|
|
||||||
claims.put("phone", phone);
|
|
||||||
|
|
||||||
return Jwts.builder()
|
|
||||||
.setClaims(claims)
|
|
||||||
.setSubject(userId)
|
|
||||||
.setIssuedAt(now)
|
|
||||||
.setExpiration(expiryDate)
|
|
||||||
.signWith(getSigningKey())
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateRefreshToken(String userId) {
|
|
||||||
Date now = new Date();
|
|
||||||
Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration() * 7);
|
|
||||||
|
|
||||||
return Jwts.builder()
|
|
||||||
.setSubject(userId)
|
|
||||||
.setIssuedAt(now)
|
|
||||||
.setExpiration(expiryDate)
|
|
||||||
.signWith(getSigningKey())
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Claims parseToken(String token) {
|
|
||||||
return Jwts.parserBuilder()
|
|
||||||
.setSigningKey(getSigningKey())
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserIdFromToken(String token) {
|
|
||||||
return parseToken(token).getSubject();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean validateToken(String token) {
|
|
||||||
try {
|
|
||||||
parseToken(token);
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("JWT token validation failed: {}", e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-99
@@ -1,99 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- 签到记录表(sign_in_record)
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- Step 1: 创建 sign_in_record 表
|
|
||||||
CREATE TABLE IF NOT EXISTS sign_in_record (
|
|
||||||
-- ========== 主键和基础字段 ==========
|
|
||||||
id BIGSERIAL PRIMARY KEY, -- 自增主键
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录更新时间
|
|
||||||
|
|
||||||
-- ========== 会员相关字段 ==========
|
|
||||||
member_id BIGINT NOT NULL, -- 会员ID,关联member_user表
|
|
||||||
member_card_id BIGINT, -- 签到时使用的会员卡ID
|
|
||||||
|
|
||||||
-- ========== 签到核心字段 ==========
|
|
||||||
sign_in_time TIMESTAMP NOT NULL, -- 签到入场时间
|
|
||||||
sign_in_type VARCHAR(20) NOT NULL, -- 签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别
|
|
||||||
sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', -- 签到状态:SUCCESS-成功,FAILED-失败
|
|
||||||
|
|
||||||
-- ========== 验证和错误信息 ==========
|
|
||||||
verification_details TEXT, -- JSONB格式,存储会员卡验证时的快照数据
|
|
||||||
fail_reason VARCHAR(500), -- 失败时的具体原因文案
|
|
||||||
|
|
||||||
-- ========== 操作人信息 ==========
|
|
||||||
operator_id BIGINT, -- 操作人ID(前台人员),自助签到时为NULL
|
|
||||||
operator_name VARCHAR(100), -- 操作人姓名冗余
|
|
||||||
|
|
||||||
-- ========== 设备和环境信息 ==========
|
|
||||||
device_info VARCHAR(200), -- 签到设备标识或型号
|
|
||||||
ip_address VARCHAR(50), -- 客户端IP地址
|
|
||||||
source VARCHAR(20) NOT NULL, -- 签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端
|
|
||||||
|
|
||||||
-- ========== 软删除字段 ==========
|
|
||||||
is_delete BOOLEAN DEFAULT FALSE -- 软删除标识:false-未删除,true-已删除
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Step 2: 创建索引
|
|
||||||
-- 会员ID索引(加速按会员查询签到记录)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id);
|
|
||||||
|
|
||||||
-- 签到时间索引(加速按时间范围查询)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time);
|
|
||||||
|
|
||||||
-- 签到状态索引(加速按状态筛选)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status);
|
|
||||||
|
|
||||||
-- 会员卡ID索引(加速按会员卡查询)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id);
|
|
||||||
|
|
||||||
-- 操作人ID索引(加速按操作人查询)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id);
|
|
||||||
|
|
||||||
-- 签到来源索引(加速按来源统计)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source);
|
|
||||||
|
|
||||||
-- 软删除索引(加速查询未删除的记录)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete);
|
|
||||||
|
|
||||||
-- 复合索引:会员ID + 签到时间(加速会员签到历史查询)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time);
|
|
||||||
|
|
||||||
-- 复合索引:签到状态 + 签到时间(加速统计数据查询)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time);
|
|
||||||
|
|
||||||
-- 复合索引:签到日期(用于每日统计,使用表达式索引)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_date ON sign_in_record(DATE(sign_in_time));
|
|
||||||
|
|
||||||
-- Step 3: 添加外键约束(可选,根据业务需求决定)
|
|
||||||
-- 注意:如果member_user表使用了软删除,外键约束可能需要谨慎使用
|
|
||||||
-- ALTER TABLE sign_in_record
|
|
||||||
-- ADD CONSTRAINT fk_sign_in_record_member
|
|
||||||
-- FOREIGN KEY (member_id) REFERENCES member_user(id) ON DELETE SET NULL;
|
|
||||||
|
|
||||||
-- Step 4: 添加注释
|
|
||||||
COMMENT ON TABLE sign_in_record IS '会员到店签到记录表';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.id IS '自增主键';
|
|
||||||
COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间';
|
|
||||||
COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member_user表';
|
|
||||||
COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间';
|
|
||||||
COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别';
|
|
||||||
COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据(包含卡类型、剩余次数/金额、有效期等)';
|
|
||||||
COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL';
|
|
||||||
COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余(避免关联查询)';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号';
|
|
||||||
COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址';
|
|
||||||
COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除';
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- V20: 更新 member_user 表 - 添加阿里云号码认证字段
|
|
||||||
-- 支持一键登录功能
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
-- 添加阿里云号码认证相关字段
|
|
||||||
ALTER TABLE IF EXISTS member_user
|
|
||||||
ADD COLUMN IF NOT EXISTS dypns_open_id VARCHAR(100),
|
|
||||||
ADD COLUMN IF NOT EXISTS id_card VARCHAR(50),
|
|
||||||
ADD COLUMN IF NOT EXISTS real_name VARCHAR(50),
|
|
||||||
ADD COLUMN IF NOT EXISTS register_channel VARCHAR(50) DEFAULT 'SMS';
|
|
||||||
|
|
||||||
-- 创建索引
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_member_user_dypns_open_id ON member_user(dypns_open_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_member_user_register_channel ON member_user(register_channel);
|
|
||||||
|
|
||||||
-- 添加字段注释
|
|
||||||
COMMENT ON COLUMN member_user.dypns_open_id IS '阿里云号码认证OpenID(一键登录用户唯一标识)';
|
|
||||||
COMMENT ON COLUMN member_user.id_card IS '身份证号码(AES加密存储)';
|
|
||||||
COMMENT ON COLUMN member_user.real_name IS '真实姓名(AES加密存储)';
|
|
||||||
COMMENT ON COLUMN member_user.register_channel IS '注册渠道:SMS-短信验证码,ONE_CLICK-一键登录,WECHAT-微信授权';
|
|
||||||
+1
-1
@@ -63,7 +63,7 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactory<JwtAut
|
|||||||
path.equals("/api/auth/login") ||
|
path.equals("/api/auth/login") ||
|
||||||
path.equals("/api/groupCourse/page") ||
|
path.equals("/api/groupCourse/page") ||
|
||||||
path.startsWith("/api/checkIn") ||
|
path.startsWith("/api/checkIn") ||
|
||||||
path.startsWith("/api/payment") ||
|
path.startsWith("/api/payment") ||
|
||||||
path.startsWith("/actuator/info");
|
path.startsWith("/actuator/info");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ signature:
|
|||||||
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
max-age-minutes: ${SIGNATURE_MAX_AGE_MINUTES:5}
|
||||||
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
nonce-cache-size: ${SIGNATURE_NONCE_CACHE_SIZE:10000}
|
||||||
whitelist:
|
whitelist:
|
||||||
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback,/api/groupCourse/**}
|
paths: ${SIGNATURE_WHITELIST_PATHS:/actuator/health,/actuator/info,/api/auth/login,/api/auth/register,/api/member/auth/miniapp/login,/api/member/auth/mp/callback}
|
||||||
|
|
||||||
resilience:
|
resilience:
|
||||||
enabled: ${RESILIENCE_ENABLED:true}
|
enabled: ${RESILIENCE_ENABLED:true}
|
||||||
|
|||||||
+6
-5
@@ -1,4 +1,4 @@
|
|||||||
package cn.novalon.gym.manage.sys.config;
|
package cn.novalon.gym.manage.sys.config;
|
||||||
|
|
||||||
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
|
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
|
||||||
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
|
||||||
@@ -21,7 +21,7 @@ public class SecurityConfig {
|
|||||||
private final OperationLogWebFilter operationLogWebFilter;
|
private final OperationLogWebFilter operationLogWebFilter;
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
|
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
|
||||||
OperationLogWebFilter operationLogWebFilter,
|
OperationLogWebFilter operationLogWebFilter,
|
||||||
Environment environment) {
|
Environment environment) {
|
||||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||||
@@ -33,11 +33,11 @@ public class SecurityConfig {
|
|||||||
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||||
String[] activeProfiles = environment.getActiveProfiles();
|
String[] activeProfiles = environment.getActiveProfiles();
|
||||||
final boolean isDevOrTest;
|
final boolean isDevOrTest;
|
||||||
|
|
||||||
isDevOrTest = java.util.Arrays.stream(activeProfiles)
|
isDevOrTest = java.util.Arrays.stream(activeProfiles)
|
||||||
.anyMatch(profile -> "dev".equals(profile) || "test".equals(profile) || "h2-test".equals(profile));
|
.anyMatch(profile -> "dev".equals(profile) || "test".equals(profile) || "h2-test".equals(profile));
|
||||||
|
|
||||||
logger.info("SecurityConfig初始化: 当前环境={}, Swagger启用状态={}",
|
logger.info("SecurityConfig初始化: 当前环境={}, Swagger启用状态={}",
|
||||||
activeProfiles.length > 0 ? String.join(",", activeProfiles) : "default", isDevOrTest);
|
activeProfiles.length > 0 ? String.join(",", activeProfiles) : "default", isDevOrTest);
|
||||||
|
|
||||||
http
|
http
|
||||||
@@ -66,6 +66,7 @@ public class SecurityConfig {
|
|||||||
.pathMatchers("/api/payment/notify").permitAll()
|
.pathMatchers("/api/payment/notify").permitAll()
|
||||||
.pathMatchers("/api/payment/refund").permitAll();
|
.pathMatchers("/api/payment/refund").permitAll();
|
||||||
|
|
||||||
|
|
||||||
if (isDevOrTest) {
|
if (isDevOrTest) {
|
||||||
spec.pathMatchers("/swagger-ui.html").permitAll()
|
spec.pathMatchers("/swagger-ui.html").permitAll()
|
||||||
.pathMatchers("/swagger-ui/**").permitAll()
|
.pathMatchers("/swagger-ui/**").permitAll()
|
||||||
|
|||||||
Reference in New Issue
Block a user