mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-04-12 11:56:55 +00:00
Compare commits
2 Commits
main
...
copilot/cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24025e0ae6 | ||
|
|
d8bffc19ce |
19
README.md
19
README.md
@@ -24,13 +24,7 @@
|
||||
# netdisk-fast-download 网盘分享链接云解析服务
|
||||
QQ交流群:1017480890
|
||||
|
||||
> netdisk-fast-download网盘直链解析可以把云盘分享链接转为直链,可广泛应用于各类下载站,资源站,个人博客,图床,APP下载更新,视频点播等领域。支持市面各大主流云盘的文件分享以及文件夹分享链接,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||
|
||||
[官方文档](https://nfd-parser.github.io/)
|
||||
[API接入](https://nfdparser.apifox.cn/)
|
||||
[公益解析,lz站](https://lz.qaiu.top)
|
||||
[公益解析,lz0m站](https://lz0.qaiu.top)
|
||||
[专业版189站,注册体验](https://189.qaiu.top)
|
||||
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||
|
||||
## 快速开始
|
||||
命令行下载分享文件:
|
||||
@@ -56,10 +50,15 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
|
||||
|
||||
**Playground功能:** [JS解析器演练场密码保护说明](web-service/doc/PLAYGROUND_PASSWORD_PROTECTION.md)
|
||||
|
||||
## 体验地址
|
||||
[公益解析1](https://lz.qaiu.top)
|
||||
[公益解析2](https://lz0.qaiu.top)
|
||||
[大文件解析专属版,限时开放,注册体验](https://189.qaiu.top)
|
||||
|
||||
**注意⚠️小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
||||
**注意⚠️收到很多用户反馈,小飞机近期封号频繁,请尽可能选择其他网盘分享**
|
||||
**注意⚠️请不要过度依赖 lz.qaiu.top,建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制,遇到解析失败的分享链接不要着急提issues,请先检查分享是否有效,** [lz站](https://lz.qaiu.top) 和 [lz0](https://lz0.qaiu.top) 不支持大文件,请使用 [189站](https://189.qaiu.top) 注册体验。
|
||||
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
|
||||
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
|
||||
**小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
||||
**注意: 请不要过度依赖lz.qaiu.top预览地址服务,建议本地搭建或者云服务器自行搭建。解析次数过多IP会被部分网盘厂商限制,不推荐做公共解析。**
|
||||
|
||||
## 网盘支持情况:
|
||||
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
||||
|
||||
@@ -68,8 +68,8 @@ public enum PanDomainTemplate {
|
||||
t-is.cn
|
||||
*/
|
||||
LZ("蓝奏云",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?(?:" +
|
||||
"(?:lanzoul|" +
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?(" +
|
||||
"lanzoul|" +
|
||||
"lanzouh|" +
|
||||
"lanosso|" +
|
||||
"lanpv|" +
|
||||
@@ -95,16 +95,14 @@ public enum PanDomainTemplate {
|
||||
"lanzv|" +
|
||||
"dmpdmp|" +
|
||||
"lanrar|" +
|
||||
"webgetstore|" +
|
||||
"lanzb|" +
|
||||
"lanzoux|" +
|
||||
"lanzout|" +
|
||||
"lanzouc|" +
|
||||
"lanzoui|" +
|
||||
"lanzoug|" +
|
||||
"lanzoum)\\.com" +
|
||||
"|t-is\\.cn" +
|
||||
")/(?<KEY>.+)"),
|
||||
"lanzoum" +
|
||||
")\\.com/(?<KEY>.+)"),
|
||||
"https://w1.lanzn.com/{shareKey}",
|
||||
LzTool.class),
|
||||
|
||||
@@ -117,7 +115,7 @@ public enum PanDomainTemplate {
|
||||
|
||||
// https://lecloud.lenovo.com/share/
|
||||
LE("联想乐云",
|
||||
compile("https://lecloud\\.lenovo\\.com/share/(?<KEY>.+)"),
|
||||
compile("https://lecloud?\\.lenovo\\.com/share/(?<KEY>.+)"),
|
||||
"https://lecloud.lenovo.com/share/{shareKey}",
|
||||
LeTool.class),
|
||||
|
||||
@@ -243,7 +241,7 @@ public enum PanDomainTemplate {
|
||||
EcTool.class),
|
||||
// https://cowtransfer.com/s/
|
||||
COW("奶牛快传",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?cowtransfer\\.com/s/(?<KEY>.+)"),
|
||||
compile("https://(.*)cowtransfer\\.com/s/(?<KEY>.+)"),
|
||||
"https://cowtransfer.com/s/{shareKey}",
|
||||
CowTool.class),
|
||||
CT("城通网盘",
|
||||
@@ -266,7 +264,7 @@ public enum PanDomainTemplate {
|
||||
PodTool.class),
|
||||
// 404网盘 https://drive.google.com/file/d/xxx/view?usp=sharing
|
||||
PGD("GoogleDrive",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?"),
|
||||
compile("https://drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?"),
|
||||
"https://drive.google.com/file/d/{shareKey}/view?usp=sharing",
|
||||
PgdTool.class),
|
||||
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
|
||||
@@ -276,11 +274,11 @@ public enum PanDomainTemplate {
|
||||
PicTool.class),
|
||||
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
|
||||
PDB("dropbox",
|
||||
compile("https://www\\.dropbox\\.com/scl/fi/(?<KEY>\\w+)/.+?rlkey=(?<PWD>\\w+).*"),
|
||||
compile("https://www.dropbox.com/scl/fi/(?<KEY>\\w+)/.+?rlkey=(?<PWD>\\w+).*"),
|
||||
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
|
||||
PdbTool.class),
|
||||
P115("115网盘",
|
||||
compile("https://(115|anxia)\\.com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
|
||||
compile("https://(115|anxia).com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
|
||||
"https://115.com/s/{shareKey}?password={pwd}",
|
||||
P115Tool.class),
|
||||
// 链接:https://www.yunpan.com/surl_yD7wz4VgU9v(提取码:fc70)
|
||||
@@ -321,7 +319,7 @@ public enum PanDomainTemplate {
|
||||
MnesTool.class),
|
||||
// https://music.163.com/#/song?id=xxx
|
||||
MNE("网易云音乐歌曲详情",
|
||||
compile("https://(y\\.)?music\\.163\\.com/(?:#/|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
|
||||
compile("https://(y.)?music\\.163\\.com/(#|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
|
||||
"https://music.163.com/#/song?id={shareKey}",
|
||||
MnesTool.MneTool.class),
|
||||
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
|
||||
@@ -342,7 +340,7 @@ public enum PanDomainTemplate {
|
||||
MkgsTool.class),
|
||||
// https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4"
|
||||
MKGS2("酷狗音乐分享2",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/share/(?<KEY>.+)\\.html.*"),
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/share/(?<KEY>.+).html.*"),
|
||||
"https://www.kugou.com/share/{shareKey}.html",
|
||||
MkgsTool.Mkgs2Tool.class),
|
||||
// https://www.kugou.com/mixsong/2bi8Fe9CSV3
|
||||
|
||||
@@ -129,130 +129,15 @@ public class PanDomainTemplateTest {
|
||||
wsPattern.matcher("https://www.evil.com/f/abc123").matches());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLzPatternWebgetstore() {
|
||||
Pattern lzPattern = PanDomainTemplate.LZ.getPattern();
|
||||
|
||||
// webgetstore.com 以前遗漏,现已补入
|
||||
Matcher m1 = lzPattern.matcher("https://webgetstore.com/somekey");
|
||||
assertTrue("LZ should match webgetstore.com", m1.find());
|
||||
assertEquals("somekey", m1.group("KEY"));
|
||||
|
||||
Matcher m2 = lzPattern.matcher("https://www.webgetstore.com/somekey");
|
||||
assertTrue("LZ should match www.webgetstore.com", m2.find());
|
||||
assertEquals("somekey", m2.group("KEY"));
|
||||
|
||||
// t-is.cn 以前遗漏,现已补入
|
||||
Matcher m3 = lzPattern.matcher("https://t-is.cn/somekey");
|
||||
assertTrue("LZ should match t-is.cn", m3.find());
|
||||
assertEquals("somekey", m3.group("KEY"));
|
||||
|
||||
Matcher m4 = lzPattern.matcher("https://www.t-is.cn/somekey");
|
||||
assertTrue("LZ should match www.t-is.cn", m4.find());
|
||||
assertEquals("somekey", m4.group("KEY"));
|
||||
|
||||
// 已有域名仍然正常匹配
|
||||
Matcher m5 = lzPattern.matcher("https://www.lanzoul.com/somekey");
|
||||
assertTrue("LZ should match existing domain lanzoul.com", m5.find());
|
||||
assertEquals("somekey", m5.group("KEY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLePatternFix() {
|
||||
Pattern lePattern = PanDomainTemplate.LE.getPattern();
|
||||
|
||||
// lecloud.lenovo.com 应匹配
|
||||
Matcher m1 = lePattern.matcher("https://lecloud.lenovo.com/share/abc123");
|
||||
assertTrue("LE should match lecloud.lenovo.com", m1.find());
|
||||
assertEquals("abc123", m1.group("KEY"));
|
||||
|
||||
// leclou.lenovo.com (去掉'd') 不应匹配(原 lecloud? 的 bug)
|
||||
assertFalse("LE should NOT match leclou.lenovo.com",
|
||||
lePattern.matcher("https://leclou.lenovo.com/share/abc123").find());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCowPatternFix() {
|
||||
Pattern cowPattern = PanDomainTemplate.COW.getPattern();
|
||||
|
||||
// 正常域名
|
||||
Matcher m1 = cowPattern.matcher("https://cowtransfer.com/s/abc123");
|
||||
assertTrue("COW should match cowtransfer.com", m1.find());
|
||||
assertEquals("abc123", m1.group("KEY"));
|
||||
|
||||
Matcher m2 = cowPattern.matcher("https://share.cowtransfer.com/s/abc123");
|
||||
assertTrue("COW should match share.cowtransfer.com", m2.find());
|
||||
assertEquals("abc123", m2.group("KEY"));
|
||||
|
||||
// 潜在的URL注入:`(.*)` 是贪婪捕获组,可匹配 `evil.com/redirect/` 等前缀,
|
||||
// 使形如 `https://evil.com/redirect/cowtransfer.com/s/key` 的 URL 被误识别。
|
||||
// 修复后改为 `(?:[a-zA-Z\d-]+\.)?` 仅匹配一级合法子域名(可选),消除误匹配。
|
||||
assertFalse("COW should NOT match redirect URLs containing cowtransfer.com in path",
|
||||
cowPattern.matcher("https://evil.com/redirect/cowtransfer.com/s/abc").find());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMnePatternFix() {
|
||||
Pattern mnePattern = PanDomainTemplate.MNE.getPattern();
|
||||
|
||||
// 带 #/ 前缀的完整网页链接(修复前因 (y.) 未转义而存在 bug)
|
||||
Matcher m1 = mnePattern.matcher("https://music.163.com/#/song?id=12345");
|
||||
assertTrue("MNE should match #/song format", m1.find());
|
||||
assertEquals("12345", m1.group("KEY"));
|
||||
|
||||
// 带 m/ 前缀的移动端链接
|
||||
Matcher m2 = mnePattern.matcher("https://music.163.com/m/song?id=12345");
|
||||
assertTrue("MNE should match m/song format", m2.find());
|
||||
assertEquals("12345", m2.group("KEY"));
|
||||
|
||||
// y.music.163.com 子域名
|
||||
Matcher m3 = mnePattern.matcher("https://y.music.163.com/song?id=12345");
|
||||
assertTrue("MNE should match y.music.163.com", m3.find());
|
||||
assertEquals("12345", m3.group("KEY"));
|
||||
|
||||
// 原 (y.) 中 `.` 未转义(`.` 匹配任意字符):对于 `yXmusic.163.com`,
|
||||
// `(y.)` 会消费 `yX`(y + 任意字符),剩余 `music.163.com` 再被 `music\.163\.com` 匹配,导致误匹配。
|
||||
// 修复后 `(y\.)` 要求字面 `.`,`yX` 中 X ≠ `.` 无法匹配,不再误匹配。
|
||||
assertFalse("MNE should NOT match yXmusic.163.com (old (y.) could erroneously match via backtracking)",
|
||||
mnePattern.matcher("https://yXmusic.163.com/song?id=12345").find());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testP115PatternFix() {
|
||||
Pattern p115Pattern = PanDomainTemplate.P115.getPattern();
|
||||
|
||||
// 正常匹配
|
||||
Matcher m1 = p115Pattern.matcher("https://115.com/s/abc123");
|
||||
assertTrue("P115 should match 115.com", m1.find());
|
||||
assertEquals("abc123", m1.group("KEY"));
|
||||
|
||||
Matcher m2 = p115Pattern.matcher("https://anxia.com/s/abc123");
|
||||
assertTrue("P115 should match anxia.com", m2.find());
|
||||
assertEquals("abc123", m2.group("KEY"));
|
||||
|
||||
// 原 .com 未转义时 115Xcom 会被误匹配(现已修复)
|
||||
assertFalse("P115 should NOT match 115Xcom",
|
||||
p115Pattern.matcher("https://115Xcom/s/abc123").find());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPgdSubdomain() {
|
||||
Pattern pgdPattern = PanDomainTemplate.PGD.getPattern();
|
||||
|
||||
// 标准链接
|
||||
Matcher m1 = pgdPattern.matcher("https://drive.google.com/file/d/abc123/view?usp=sharing");
|
||||
assertTrue("PGD should match standard drive.google.com", m1.find());
|
||||
assertEquals("abc123", m1.group("KEY"));
|
||||
|
||||
// 带子域名的链接(修复后支持)
|
||||
Matcher m2 = pgdPattern.matcher("https://adsd.drive.google.com/file/d/151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz/view");
|
||||
assertTrue("PGD should match subdomain.drive.google.com", m2.find());
|
||||
assertEquals("151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz", m2.group("KEY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyDuplicates() {
|
||||
|
||||
Matcher matcher = compile("https://(?:[a-zA-Z\\d-]+\\.)?drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?")
|
||||
.matcher("https://adsd.drive.google.com/file/d/151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz/view");
|
||||
if (matcher.find()) {
|
||||
System.out.println(matcher.group());
|
||||
System.out.println(matcher.group("KEY"));
|
||||
}
|
||||
// 校验重复
|
||||
Set<String> collect =
|
||||
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getRegex).collect(Collectors.toSet());
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
package cn.qaiu.lz.common.util;
|
||||
|
||||
import cn.qaiu.lz.web.model.SysUser;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* JWT工具类,用于生成和验证JWT token
|
||||
*/
|
||||
public class JwtUtil {
|
||||
|
||||
private static final long EXPIRE_TIME = 24 * 60 * 60 * 1000; // token过期时间,24小时
|
||||
private static final String SECRET_KEY = "netdisk-fast-download-jwt-secret-key"; // 密钥
|
||||
private static final String ALGORITHM = "HmacSHA256";
|
||||
|
||||
/**
|
||||
* 生成JWT token
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return JWT token
|
||||
*/
|
||||
public static String generateToken(SysUser user) {
|
||||
long expireTime = getExpireTime();
|
||||
|
||||
// Header
|
||||
JsonObject header = new JsonObject()
|
||||
.put("alg", "HS256")
|
||||
.put("typ", "JWT");
|
||||
|
||||
// Payload
|
||||
JsonObject payload = new JsonObject()
|
||||
.put("id", user.getId())
|
||||
.put("username", user.getUsername())
|
||||
.put("role", user.getRole())
|
||||
.put("exp", expireTime)
|
||||
.put("iat", System.currentTimeMillis())
|
||||
.put("iss", "netdisk-fast-download");
|
||||
|
||||
// Base64 encode header and payload
|
||||
String encodedHeader = Base64.getUrlEncoder().withoutPadding().encodeToString(header.encode().getBytes(StandardCharsets.UTF_8));
|
||||
String encodedPayload = Base64.getUrlEncoder().withoutPadding().encodeToString(payload.encode().getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Create signature
|
||||
String signature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
|
||||
|
||||
// Combine to form JWT
|
||||
return encodedHeader + "." + encodedPayload + "." + signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用HMAC-SHA256算法生成签名
|
||||
*
|
||||
* @param data 要签名的数据
|
||||
* @param key 密钥
|
||||
* @return 签名
|
||||
*/
|
||||
private static String hmacSha256(String data, String key) {
|
||||
try {
|
||||
Mac sha256Hmac = Mac.getInstance(ALGORITHM);
|
||||
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
sha256Hmac.init(secretKey);
|
||||
byte[] signedBytes = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(signedBytes);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new RuntimeException("Error creating HMAC SHA256 signature", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT token
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 如果token有效返回true,否则返回false
|
||||
*/
|
||||
public static boolean validateToken(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String encodedHeader = parts[0];
|
||||
String encodedPayload = parts[1];
|
||||
String signature = parts[2];
|
||||
|
||||
// 验证签名
|
||||
String expectedSignature = hmacSha256(encodedHeader + "." + encodedPayload, SECRET_KEY);
|
||||
if (!expectedSignature.equals(signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证过期时间
|
||||
String payload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
JsonObject payloadJson = new JsonObject(payload);
|
||||
long expTime = payloadJson.getLong("exp", 0L);
|
||||
|
||||
return System.currentTimeMillis() < expTime;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户ID
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 用户ID
|
||||
*/
|
||||
public static String getUserIdFromToken(String token) {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Base64解码
|
||||
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
|
||||
JsonObject jsonObject = new JsonObject(payload);
|
||||
return jsonObject.getString("id");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户名
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 用户名
|
||||
*/
|
||||
public static String getUsernameFromToken(String token) {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Base64解码
|
||||
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
|
||||
JsonObject jsonObject = new JsonObject(payload);
|
||||
return jsonObject.getString("username");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户角色
|
||||
*
|
||||
* @param token JWT token
|
||||
* @return 用户角色
|
||||
*/
|
||||
public static String getRoleFromToken(String token) {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Base64解码
|
||||
String payload = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
|
||||
JsonObject jsonObject = new JsonObject(payload);
|
||||
return jsonObject.getString("role");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间
|
||||
*
|
||||
* @return 过期时间戳
|
||||
*/
|
||||
private static long getExpireTime() {
|
||||
return System.currentTimeMillis() + EXPIRE_TIME;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将过期时间戳转换为LocalDateTime
|
||||
*
|
||||
* @param expireTime 过期时间戳
|
||||
* @return LocalDateTime
|
||||
*/
|
||||
public static LocalDateTime getExpireTimeAsLocalDateTime(long expireTime) {
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(expireTime), ZoneId.systemDefault());
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package cn.qaiu.lz.web.model;
|
||||
|
||||
import cn.qaiu.db.ddl.Table;
|
||||
import cn.qaiu.lz.common.ToJson;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.vertx.codegen.annotations.DataObject;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Data
|
||||
@DataObject
|
||||
@NoArgsConstructor
|
||||
@Table("sys_user")
|
||||
public class SysUser implements ToJson {
|
||||
private String id;
|
||||
private String username;
|
||||
|
||||
private String password;
|
||||
|
||||
private String email;
|
||||
private String phone;
|
||||
private String avatar;
|
||||
|
||||
// 用户状态:0-禁用,1-正常
|
||||
private Integer status;
|
||||
|
||||
// 用户角色:admin-管理员,user-普通用户
|
||||
private String role;
|
||||
|
||||
// 最后登录时间
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime lastLoginTime;
|
||||
|
||||
private Integer age;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
public SysUser(JsonObject json) {
|
||||
this.id = json.getString("id");
|
||||
this.username = json.getString("username");
|
||||
this.password = json.getString("password");
|
||||
this.email = json.getString("email");
|
||||
this.phone = json.getString("phone");
|
||||
this.avatar = json.getString("avatar");
|
||||
this.status = json.getInteger("status");
|
||||
this.role = json.getString("role");
|
||||
this.age = json.getInteger("age");
|
||||
if (json.getString("createTime") != null) {
|
||||
this.createTime = LocalDateTime.parse(json.getString("createTime"));
|
||||
}
|
||||
if (json.getString("lastLoginTime") != null) {
|
||||
this.lastLoginTime = LocalDateTime.parse(json.getString("lastLoginTime"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package cn.qaiu.lz.web.service;
|
||||
|
||||
import cn.qaiu.lz.web.model.SysUser;
|
||||
import cn.qaiu.vx.core.base.BaseAsyncService;
|
||||
import io.vertx.codegen.annotations.ProxyGen;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
/**
|
||||
* 用户服务接口
|
||||
* <br>Create date 2021/8/27 14:06
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
@ProxyGen
|
||||
public interface UserService extends BaseAsyncService {
|
||||
/**
|
||||
* 用户登录
|
||||
* @param user 包含用户名和密码的用户对象
|
||||
* @return 登录成功返回用户信息和token,失败返回错误信息
|
||||
*/
|
||||
Future<JsonObject> login(SysUser user);
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户信息
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
*/
|
||||
Future<SysUser> getUserByUsername(String username);
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* @param user 用户信息
|
||||
* @return 创建成功返回用户信息,失败返回错误信息
|
||||
*/
|
||||
Future<SysUser> createUser(SysUser user);
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param user 用户信息
|
||||
* @return 更新成功返回用户信息,失败返回错误信息
|
||||
*/
|
||||
Future<SysUser> updateUser(SysUser user);
|
||||
|
||||
/**
|
||||
* 验证token
|
||||
* @param token JWT token
|
||||
* @return 验证成功返回用户信息,失败返回错误信息
|
||||
*/
|
||||
Future<JsonObject> validateToken(String token);
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
package cn.qaiu.lz.web.service.impl;
|
||||
|
||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||
import cn.qaiu.lz.common.util.JwtUtil;
|
||||
import cn.qaiu.lz.common.util.PasswordUtil;
|
||||
import cn.qaiu.lz.web.model.SysUser;
|
||||
import cn.qaiu.lz.web.service.UserService;
|
||||
import cn.qaiu.vx.core.annotaions.Service;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.jdbcclient.JDBCPool;
|
||||
import io.vertx.sqlclient.Row;
|
||||
import io.vertx.sqlclient.RowSet;
|
||||
import io.vertx.sqlclient.Tuple;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 用户服务实现类
|
||||
* <br>Create date 2021/8/27 14:09
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
private final JDBCPool jdbcPool = JDBCPoolInit.instance().getPool();
|
||||
|
||||
// 初始化方法,确保管理员用户存在
|
||||
public void init() {
|
||||
// 检查管理员用户是否存在
|
||||
getUserByUsername("admin")
|
||||
.onSuccess(user -> {
|
||||
log.info("管理员用户已存在");
|
||||
})
|
||||
.onFailure(err -> {
|
||||
// 创建管理员用户
|
||||
SysUser admin = new SysUser();
|
||||
admin.setId(UUID.randomUUID().toString());
|
||||
admin.setUsername("admin");
|
||||
admin.setPassword(PasswordUtil.hashPassword("admin123"));
|
||||
admin.setEmail("admin@example.com");
|
||||
admin.setRole("admin");
|
||||
admin.setStatus(1);
|
||||
admin.setCreateTime(LocalDateTime.now());
|
||||
|
||||
createUser(admin)
|
||||
.onSuccess(result -> log.info("管理员用户创建成功"))
|
||||
.onFailure(error -> log.error("管理员用户创建失败", error));
|
||||
});
|
||||
}
|
||||
|
||||
// 新增一个工具方法来过滤敏感信息
|
||||
private SysUser filterSensitiveInfo(SysUser user) {
|
||||
if (user != null) {
|
||||
SysUser filtered = new SysUser();
|
||||
// 复制除密码外的所有字段
|
||||
filtered.setId(user.getId());
|
||||
filtered.setUsername(user.getUsername());
|
||||
filtered.setEmail(user.getEmail());
|
||||
filtered.setPhone(user.getPhone());
|
||||
filtered.setAvatar(user.getAvatar());
|
||||
filtered.setRole(user.getRole());
|
||||
filtered.setStatus(user.getStatus());
|
||||
filtered.setCreateTime(user.getCreateTime());
|
||||
filtered.setLastLoginTime(user.getLastLoginTime());
|
||||
return filtered;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将Row转换为SysUser对象
|
||||
private SysUser rowToUser(Row row) {
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SysUser user = new SysUser();
|
||||
user.setId(row.getString("id"));
|
||||
user.setUsername(row.getString("username"));
|
||||
user.setPassword(row.getString("password"));
|
||||
user.setEmail(row.getString("email"));
|
||||
user.setPhone(row.getString("phone"));
|
||||
user.setAvatar(row.getString("avatar"));
|
||||
user.setRole(row.getString("role"));
|
||||
user.setStatus(row.getInteger("status"));
|
||||
|
||||
// 处理日期时间字段
|
||||
LocalDateTime createTime = row.getLocalDateTime("create_time");
|
||||
if (createTime != null) {
|
||||
user.setCreateTime(createTime);
|
||||
}
|
||||
|
||||
LocalDateTime lastLoginTime = row.getLocalDateTime("last_login_time");
|
||||
if (lastLoginTime != null) {
|
||||
user.setLastLoginTime(lastLoginTime);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<JsonObject> login(SysUser user) {
|
||||
// 参数校验
|
||||
if (user == null || user.getUsername() == null || user.getPassword() == null) {
|
||||
return Future.succeededFuture(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "用户名和密码不能为空"));
|
||||
}
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
// 查询用户
|
||||
String sql = "SELECT * FROM sys_user WHERE username = ?";
|
||||
|
||||
jdbcPool.preparedQuery(sql)
|
||||
.execute(Tuple.of(user.getUsername()))
|
||||
.onSuccess(rows -> {
|
||||
if (rows.size() == 0) {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "用户不存在"));
|
||||
return;
|
||||
}
|
||||
|
||||
Row row = rows.iterator().next();
|
||||
SysUser existUser = rowToUser(row);
|
||||
|
||||
// 验证密码
|
||||
if (!PasswordUtil.checkPassword(user.getPassword(), existUser.getPassword())) {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "密码错误"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
existUser.setLastLoginTime(now);
|
||||
|
||||
// 更新数据库中的最后登录时间
|
||||
String updateSql = "UPDATE sys_user SET last_login_time = ? WHERE username = ?";
|
||||
jdbcPool.preparedQuery(updateSql)
|
||||
.execute(Tuple.of(
|
||||
Timestamp.from(now.atZone(ZoneId.systemDefault()).toInstant()),
|
||||
existUser.getUsername()
|
||||
))
|
||||
.onFailure(err -> log.error("更新最后登录时间失败", err));
|
||||
|
||||
// 生成token
|
||||
String token = JwtUtil.generateToken(existUser);
|
||||
|
||||
// 返回用户信息和token
|
||||
JsonObject value = JsonObject.mapFrom(existUser);
|
||||
value.remove("password");
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", true)
|
||||
.put("message", "登录成功")
|
||||
.put("token", token)
|
||||
.put("user", value));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("登录查询失败", err);
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "登录失败: " + err.getMessage()));
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<SysUser> getUserByUsername(String username) {
|
||||
if (username == null || username.isEmpty()) {
|
||||
return Future.failedFuture("用户名不能为空");
|
||||
}
|
||||
|
||||
Promise<SysUser> promise = Promise.promise();
|
||||
|
||||
String sql = "SELECT * FROM sys_user WHERE username = ?";
|
||||
|
||||
jdbcPool.preparedQuery(sql)
|
||||
.execute(Tuple.of(username))
|
||||
.onSuccess(rows -> {
|
||||
if (rows.size() == 0) {
|
||||
promise.fail("用户不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
Row row = rows.iterator().next();
|
||||
SysUser user = rowToUser(row);
|
||||
promise.complete(filterSensitiveInfo(user));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("查询用户失败", err);
|
||||
promise.fail("查询用户失败: " + err.getMessage());
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<SysUser> createUser(SysUser user) {
|
||||
// 参数校验
|
||||
if (user == null || user.getUsername() == null || user.getPassword() == null) {
|
||||
return Future.failedFuture("用户名和密码不能为空");
|
||||
}
|
||||
|
||||
Promise<SysUser> promise = Promise.promise();
|
||||
|
||||
// 先检查用户是否已存在
|
||||
String checkSql = "SELECT COUNT(*) as count FROM sys_user WHERE username = ?";
|
||||
|
||||
jdbcPool.preparedQuery(checkSql)
|
||||
.execute(Tuple.of(user.getUsername()))
|
||||
.onSuccess(rows -> {
|
||||
Row row = rows.iterator().next();
|
||||
long count = row.getLong("count");
|
||||
|
||||
if (count > 0) {
|
||||
promise.fail("用户名已存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置用户ID和创建时间
|
||||
if (user.getId() == null) {
|
||||
user.setId(UUID.randomUUID().toString());
|
||||
}
|
||||
if (user.getCreateTime() == null) {
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
// 设置默认角色和状态
|
||||
if (user.getRole() == null) {
|
||||
user.setRole("user");
|
||||
}
|
||||
if (user.getStatus() == null) {
|
||||
user.setStatus(1);
|
||||
}
|
||||
|
||||
// 对密码进行加密
|
||||
String plainPassword = user.getPassword();
|
||||
user.setPassword(PasswordUtil.hashPassword(plainPassword));
|
||||
|
||||
// 插入用户
|
||||
String insertSql = "INSERT INTO sys_user (id, username, password, email, phone, avatar, role, status, create_time) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
jdbcPool.preparedQuery(insertSql)
|
||||
.execute(Tuple.of(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getPassword(),
|
||||
user.getEmail(),
|
||||
user.getPhone(),
|
||||
user.getAvatar(),
|
||||
user.getRole(),
|
||||
user.getStatus(),
|
||||
Timestamp.from(user.getCreateTime().atZone(ZoneId.systemDefault()).toInstant())
|
||||
))
|
||||
.onSuccess(result -> {
|
||||
promise.complete(filterSensitiveInfo(user));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("创建用户失败", err);
|
||||
promise.fail("创建用户失败: " + err.getMessage());
|
||||
});
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("检查用户是否存在失败", err);
|
||||
promise.fail("创建用户失败: " + err.getMessage());
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<SysUser> updateUser(SysUser user) {
|
||||
// 参数校验
|
||||
if (user == null || user.getUsername() == null) {
|
||||
return Future.failedFuture("用户名不能为空");
|
||||
}
|
||||
|
||||
Promise<SysUser> promise = Promise.promise();
|
||||
|
||||
// 先检查用户是否存在
|
||||
String checkSql = "SELECT * FROM sys_user WHERE username = ?";
|
||||
|
||||
jdbcPool.preparedQuery(checkSql)
|
||||
.execute(Tuple.of(user.getUsername()))
|
||||
.onSuccess(rows -> {
|
||||
if (rows.size() == 0) {
|
||||
promise.fail("用户不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
Row row = rows.iterator().next();
|
||||
SysUser existUser = rowToUser(row);
|
||||
|
||||
// 构建更新SQL
|
||||
StringBuilder updateSql = new StringBuilder("UPDATE sys_user SET ");
|
||||
Tuple params = Tuple.tuple();
|
||||
|
||||
if (user.getEmail() != null) {
|
||||
updateSql.append("email = ?, ");
|
||||
params.addValue(user.getEmail());
|
||||
}
|
||||
|
||||
if (user.getPhone() != null) {
|
||||
updateSql.append("phone = ?, ");
|
||||
params.addValue(user.getPhone());
|
||||
}
|
||||
|
||||
if (user.getAvatar() != null) {
|
||||
updateSql.append("avatar = ?, ");
|
||||
params.addValue(user.getAvatar());
|
||||
}
|
||||
|
||||
if (user.getStatus() != null) {
|
||||
updateSql.append("status = ?, ");
|
||||
params.addValue(user.getStatus());
|
||||
}
|
||||
|
||||
if (user.getRole() != null) {
|
||||
updateSql.append("role = ?, ");
|
||||
params.addValue(user.getRole());
|
||||
}
|
||||
|
||||
if (user.getPassword() != null) {
|
||||
updateSql.append("password = ?, ");
|
||||
params.addValue(PasswordUtil.hashPassword(user.getPassword()));
|
||||
}
|
||||
|
||||
// 移除最后的逗号和空格
|
||||
String sql = updateSql.toString();
|
||||
if (sql.endsWith(", ")) {
|
||||
sql = sql.substring(0, sql.length() - 2);
|
||||
}
|
||||
|
||||
// 如果没有要更新的字段,直接返回
|
||||
if (params.size() == 0) {
|
||||
promise.complete(filterSensitiveInfo(existUser));
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加WHERE条件
|
||||
sql += " WHERE username = ?";
|
||||
params.addValue(user.getUsername());
|
||||
|
||||
// 执行更新
|
||||
jdbcPool.preparedQuery(sql)
|
||||
.execute(params)
|
||||
.onSuccess(result -> {
|
||||
// 重新查询用户信息
|
||||
getUserByUsername(user.getUsername())
|
||||
.onSuccess(promise::complete)
|
||||
.onFailure(promise::fail);
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("更新用户失败", err);
|
||||
promise.fail("更新用户失败: " + err.getMessage());
|
||||
});
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("查询用户失败", err);
|
||||
promise.fail("更新用户失败: " + err.getMessage());
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<JsonObject> validateToken(String token) {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return Future.succeededFuture(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "Token不能为空"));
|
||||
}
|
||||
|
||||
// 验证token
|
||||
boolean isValid = JwtUtil.validateToken(token);
|
||||
if (!isValid) {
|
||||
return Future.succeededFuture(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "Token无效或已过期"));
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
String username = JwtUtil.getUsernameFromToken(token);
|
||||
|
||||
Promise<JsonObject> promise = Promise.promise();
|
||||
|
||||
getUserByUsername(username)
|
||||
.onSuccess(user -> {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", true)
|
||||
.put("message", "Token有效")
|
||||
.put("user", JsonObject.mapFrom(user)));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
promise.complete(new JsonObject()
|
||||
.put("success", false)
|
||||
.put("message", "用户不存在"));
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user