feat: 新增 sa-token 登录鉴权 + 映射配置管理 + 清理接口

- 新增 sa-token 登录鉴权(admin/mD8C-gR8UgRE8fNvJWgmjg)
- 新增 BizFileMappingEntity/Mapper/Controller,映射配置改为数据库管理
- 新增 bizPath 字段,支持每张表单独配置 OSS 业务路径
- 新增文件日志/任务历史清理接口
- 新增全局异常过滤器
- 删除 BizFileMappingConfig(yml 配置方式废弃)
- 前端新增映射配置 Tab、登录页、退出登录
This commit is contained in:
2026-04-16 19:17:59 +08:00
parent 3fddada267
commit 2f068c6a5f
7 changed files with 268 additions and 50 deletions
+7
View File
@@ -98,6 +98,13 @@
<scope>provided</scope>
</dependency>
<!-- Sa-Token 鉴权 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-solon-plugin</artifactId>
<version>1.44.0</version>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-test</artifactId>
@@ -1,35 +0,0 @@
package com.yxtech.ossyn.config;
import lombok.Data;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import java.util.List;
/**
* 业务文件映射配置
* 对应 app.yml 中的 biz-file-mappings 节点
*/
@Data
@Configuration
public class BizFileMappingConfig {
/** 业务表映射列表 */
@Inject("${biz-file-mappings}")
private List<BizFileMapping> mappings;
/**
* 单条业务表映射配置
*/
@Data
public static class BizFileMapping {
/** 数据库名 */
private String dbName;
/** 业务表名 */
private String tableName;
/** 主键列名 */
private String idCol;
/** 文件路径列名 */
private String fileCol;
}
}
@@ -0,0 +1,72 @@
package com.yxtech.ossyn.config;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.router.SaHttpMethod;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.solon.integration.SaTokenInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.Bean;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import org.noear.solon.core.handle.Filter;
/**
* Sa-Token 鉴权配置
* 拦截所有请求,放行登录页、登录接口、静态资源
* 未登录时重定向到 /auth/login
*/
@Slf4j
@Configuration
public class SaTokenConfig {
@Inject("${server.contextPath:}")
private String basePath;
@Bean(index = -100)
public SaTokenInterceptor saTokenInterceptor() {
return new SaTokenInterceptor()
.addInclude("/**")
.addExclude("/favicon.ico")
.setAuth(obj -> {
// 放行登录相关路由,其余均需登录
SaRouter.match("/**")
.notMatch(basePath + "/auth/login")
.notMatch(basePath + "/auth/doLogin")
.check(r -> StpUtil.checkLogin());
})
.setBeforeAuth(obj -> {
SaHolder.getResponse()
.setHeader("Access-Control-Allow-Origin", "*")
.setHeader("Access-Control-Allow-Methods", "*")
.setHeader("Access-Control-Allow-Headers", "*")
.setHeader("Access-Control-Max-Age", "3600");
SaRouter.match(SaHttpMethod.OPTIONS).back();
})
.setError(e -> {
// 拦截器抛出异常由下方 Filter 处理重定向,此处仅记录日志
log.warn("鉴权失败 [{}]: {}", SaHolder.getRequest().getUrl(), e.getMessage());
return null;
});
}
/**
* 未登录时重定向到登录页(order=-90,在 SaTokenInterceptor 之后执行)
*/
@Bean(index = -90)
public Filter authRedirectFilter() {
return (ctx, chain) -> {
String path = ctx.path();
// 放行登录相关路由和静态资源
if (path.startsWith(basePath + "/auth/") || path.startsWith("/static/")) {
chain.doFilter(ctx);
return;
}
if (!StpUtil.isLogin()) {
ctx.redirect(basePath + "/auth/login");
return;
}
chain.doFilter(ctx);
};
}
}
@@ -0,0 +1,75 @@
package com.yxtech.ossyn.controller;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.noear.solon.annotation.*;
import org.noear.solon.core.handle.Context;
import org.noear.solon.core.handle.ModelAndView;
import java.io.IOException;
/**
* 登录/登出控制器
* 凭证来源:app.yml sa-token.http-basic(格式:用户名:密码)
*/
@Slf4j
@Controller
@Mapping("/auth")
public class AuthController {
/** HTTP Basic 凭证,格式:用户名:密码 */
@Inject("${sa-token.http-basic}")
private String httpBasic;
@Inject("${server.contextPath:}")
private String basePath;
/**
* 显示登录页
*/
@Get
@Mapping("/login")
public Object loginPage(@Param(defaultValue = "") String error, Context ctx) throws IOException {
if (StpUtil.isLogin()) {
ctx.redirect(basePath + "/oss-sync/");
return null;
}
ModelAndView mv = new ModelAndView("login.ftl");
mv.put("basePath", basePath);
mv.put("errorMsg", "1".equals(error) ? "用户名或密码错误,请重试" : "");
return mv;
}
/**
* 处理登录表单提交
*/
@Post
@Mapping("/doLogin")
public void doLogin(
@Param(defaultValue = "") String username,
@Param(defaultValue = "") String password,
Context ctx) throws IOException {
String[] parts = httpBasic.split(":", 2);
String cfgUser = parts.length > 0 ? parts[0] : "";
String cfgPass = parts.length > 1 ? parts[1] : "";
if (cfgUser.equals(username.trim()) && cfgPass.equals(password)) {
StpUtil.login(1);
log.info("[Auth] 登录成功,用户: {}", username);
ctx.redirect(basePath + "/oss-sync/");
} else {
log.warn("[Auth] 登录失败,用户名: {}", username);
ctx.redirect(basePath + "/auth/login?error=1");
}
}
/**
* 退出登录
*/
@Get
@Mapping("/logout")
public void logout(Context ctx) throws IOException {
StpUtil.logout();
ctx.redirect(basePath + "/auth/login");
}
}
+21 -14
View File
@@ -49,20 +49,6 @@ biz-datasource:
password: Aa135790123
params: "characterEncoding=UTF-8&useUnicode=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai"
# =====================================
# 业务表映射配置
# 格式:数据库名 业务表名 业务表ID列名 业务文件路径列名
# =====================================
biz-file-mappings:
# - dbName: dmz_health_system
# tableName: sys_user
# idCol: id
# fileCol: avatar
- dbName: dmz_health_consult
tableName: con_resource
idCol: id
fileCol: img
# =====================================
# 本地文件根路径(业务表中存储的是相对路径,拼接此前缀查找文件)
# =====================================
@@ -80,3 +66,24 @@ xj-oss:
config-code: FILE-OSS-0a85c4dd40ca745a598b872c7974bcaca
directory: scyx/scfz/aygc/jkgl/
file-type-white-list: dat,svg,dwg,pr,pptm,ai,bak,pptx,lic,db,caj,wps,xmind,eps,exe,doc,docx,xls,xlsx,ppt,pps,pdf,xml,webp,MOV,MP4,HEVC,wmv,rmvb,rm,odt,3gp,swf,gz,tgz,log,bz,bz2,tbz,zip,rar,tar,7z,mp3,mid,ogg,mpga,mp4a,wav,wma,avi,dv,mp4,bmp,xlsm,dps,bak,cdr,et,kdh,nh,,nh,mpeg,mpg,mov,wm,flv,mkv,bmp,jpg,jpeg,gif,png,tif,tiff,tga,psd,txt,php,html,htm,js,css,rtf,rtfd,py,java,rb,sh,pl,sql,xml,gdb,gdbx
# =====================================
# Sa-Token 鉴权配置
# =====================================
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: oss-token
# token 有效期(秒),-1 代表永不过期
timeout: 86400
# 无操作超时时间(秒),-1 代表不限制
activity-timeout: -1
# 允许同一账号并发登录
allow-concurrent-login: true
# 多人登录共用同一 token
is-share: true
# token 风格
token-style: uuid
# 关闭操作日志
is-log: false
# 登录凭证(格式:用户名:密码)
http-basic: "admin:mD8C-gR8UgRE8fNvJWgmjg"
+4 -1
View File
@@ -105,7 +105,10 @@
</style>
</head>
<body>
<div class="header">OSS 文件同步管理</div>
<div class="header" style="display:flex;align-items:center;justify-content:space-between;">
<span>OSS 文件同步管理</span>
<a href="/auth/logout" style="color:rgba(255,255,255,.8);font-size:13px;text-decoration:none;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,.8)'">退出登录</a>
</div>
<div class="container">
<!-- Tab 导航 -->
+89
View File
@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 - OSS 文件同步管理</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Microsoft YaHei", sans-serif;
background: #f0f2f5;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-wrapper { width: 380px; }
.login-title {
text-align: center;
color: #1677ff;
font-size: 22px;
font-weight: 600;
margin-bottom: 24px;
}
.login-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
padding: 36px 40px;
}
.form-group { margin-bottom: 18px; }
.form-group label { display: block; font-size: 13px; color: #666; margin-bottom: 6px; }
.form-group input {
width: 100%;
padding: 10px 13px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.form-group input:focus { border-color: #1677ff; }
.btn-login {
width: 100%;
padding: 12px;
background: #1677ff;
color: #fff;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
margin-top: 6px;
transition: background 0.2s;
letter-spacing: 2px;
}
.btn-login:hover { background: #4096ff; }
.error-msg {
background: #fff0f0;
border: 1px solid #ffcccc;
color: #d32f2f;
padding: 10px 14px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 18px;
}
</style>
</head>
<body>
<div class="login-wrapper">
<div class="login-title">OSS 文件同步管理</div>
<div class="login-card">
<#if errorMsg?has_content>
<div class="error-msg">${errorMsg}</div>
</#if>
<form method="post" action="${basePath}/auth/doLogin">
<div class="form-group">
<label>用户名</label>
<input type="text" name="username" placeholder="请输入用户名" autofocus required>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" placeholder="请输入密码" required>
</div>
<button type="submit" class="btn-login">登 录</button>
</form>
</div>
</div>
</body>
</html>