Compare commits

..

No commits in common. "72ff1c7b38ebd9449cabbf42719e36888a64fa9c" and "d91c141c6d67dd12c2910c581e2d5a0f7b345717" have entirely different histories.

16 changed files with 10 additions and 1141 deletions

194
README.md
View File

@ -1,194 +1,8 @@
# CCDemo - Spring Boot 基础项目模板
# youfool-framework-springboot 基础项目模板
基于 youfool-framework-springboot 3.3.1 的 Spring Boot 项目模板,集成常用功能和最佳实践。
## 数据源
## 项目特性
当前版本3.0版本以上)的基础框架使用了`苞米豆`的[动态数据源](https://github.com/baomidou/dynamic-datasource-spring-boot-starter) 。
- 🛡️ **安全防护**:集成 XSS 防护、SM3 加密、RSA 加密
- 🔐 **认证授权**:基于 Sa-Token 的权限管理
- 📊 **API 文档**:集成 Knife4j (Swagger) 接口文档
- 📈 **监控日志**:集成 Druid 数据源监控和 REST 日志记录
- 🚦 **限流控制**:内置漏桶算法限流器
- 🗄️ **数据源**:支持动态数据源切换
- 📁 **文件管理**:支持文件上传功能
使用事务时需注意使用`@DSTransactional`代替`@Transactional`来使用注解式事务。
## 技术栈
- **框架**Spring Boot 2.3.5
- **基础框架**youfool-framework-springboot 3.3.1
- **数据库**PostgreSQL / MySQL
- **安全**Sa-Token、RSA、SM3
- **工具**Lombok、Junit
- **文档**Knife4j (Swagger)
## 项目结构
```
src/main/java/io/lroyia/ccdemo/
├── annotation/ # 自定义注解
│ └── RateLimit.java # 限流注解
├── aspect/ # AOP 切面
│ └── RateLimiterAspect.java # 限流切面
├── config/ # 配置类
│ ├── PrjDataSource.java # 数据源配置
│ ├── RateLimiterConfig.java # 限流器配置
│ ├── SpringMvcConfig.java # MVC 配置
│ └── SwaggerKnife4j.java # Swagger 配置
├── controller/ # 控制器
│ └── LoginController.java # 登录相关接口
├── common/ # 通用组件
│ ├── constants/ # 常量定义
│ └── util/ # 工具类
│ ├── StringUtils.java
│ └── LeakyBucketRateLimiter.java # 漏桶限流器
├── exception/ # 异常处理
│ └── RateLimitExceededException.java
└── dev/ # 开发工具
```
## 核心功能
### 1. 数据源管理
当前版本使用了`苞米豆`的[动态数据源](https://github.com/baomidou/dynamic-datasource-spring-boot-starter)。
**注意事项**:使用事务时需使用 `@DSTransactional` 代替 `@Transactional`
### 2. 限流控制
实现了基于漏桶算法的限流器,支持:
- 桶容量配置
- 流出速率配置
- 自定义异常处理
- AOP 无侵入式集成
**使用方式**
```java
@RateLimit
public RestResult<?> doLogin(String username, String password, Boolean encrypt) {
// 方法实现
}
```
**配置参数**
```yaml
rate:
limiter:
enabled: true # 是否启用限流
capacity: 100 # 桶容量
rate: 10 # 每秒处理数量
rate-interval: 1 # 时间间隔(秒)
```
### 3. 认证授权
基于 Sa-Token 实现的认证授权系统:
- Token 认证
- 权限管理
- 会话管理
- 登录状态控制
### 4. 安全防护
- **XSS 防护**:过滤恶意脚本,保护应用安全
- **SM3 加密**:用户密码加密存储
- **RSA 加密**:敏感数据传输加密
- **CSP 策略**:内容安全策略配置
### 5. API 文档
集成 Knife4j (Swagger) 提供在线 API 文档:
- 自动生成接口文档
- 支持在线测试
- 权限控制访问
## 配置说明
### 主要配置项
```yaml
# 应用配置
spring:
application:
name: youfool-prj-springboot-template
# 数据源配置
datasource:
dynamic:
primary: master
datasource:
master:
# 主数据源配置
youfool:
# 框架数据源配置
# 限流器配置
rate:
limiter:
enabled: true
capacity: 100
rate: 10
rate-interval: 1
# Sa-Token 配置
sa-token:
token-name: satoken
timeout: 2592000
is-concurrent: true
is-share: false
token-style: uuid
# 安全配置
security:
xss:
enabled: true
validate-input: true
sanitize-output: true
```
## 开发指南
### 启动项目
```bash
mvn spring-boot:run
```
### 访问地址
- **应用首页**http://localhost:8080/prj
- **API 文档**http://localhost:8080/prj/doc.html
- **Druid 监控**http://localhost:8080/prj/druid
### 代码规范
项目集成了 CheckStyle 代码风格检查:
```bash
mvn checkstyle:check
```
### 构建部署
```bash
# 编译打包
mvn clean package
# 跳过测试打包
mvn clean package -DskipTests
```
## 注意事项
1. **事务管理**:使用 `@DSTransactional` 而非 `@Transactional`
2. **安全配置**:生产环境请修改默认密码和密钥
3. **限流配置**:根据业务需求调整限流参数
4. **数据源**:确保数据库连接配置正确
5. **权限管理**:根据实际业务配置用户权限
## 许可证
本项目遵循 Apache License 2.0 开源协议。

10
pom.xml
View File

@ -12,12 +12,12 @@
<url>https://www.chinaweal.com.cn</url>
<description>boot基础的后台模板</description>
<properties>
<java.version>21</java.version>
<java.version>1.8</java.version>
<skipTests>true</skipTests>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.compilerVersion>21</maven.compiler.compilerVersion>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
<failOnMissingWebXml>false</failOnMissingWebXml>
<spring.boot.version>2.3.5.RELEASE</spring.boot.version>
<skipTests>true</skipTests>
@ -51,7 +51,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>

View File

@ -1,17 +0,0 @@
package io.lroyia.ccdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 限流注解
*
* @author lroyia
* @since 1.0.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
}

View File

@ -1,33 +0,0 @@
package io.lroyia.ccdemo.aspect;
import io.lroyia.ccdemo.common.util.LeakyBucketRateLimiter;
import io.lroyia.ccdemo.exception.RateLimitExceededException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 限流器切面
*
* @author lroyia
* @since 1.0.0
*/
@Aspect
@Component
public class RateLimiterAspect {
@Autowired(required = false)
private LeakyBucketRateLimiter leakyBucketRateLimiter;
@Around("@annotation(io.lroyia.ccdemo.annotation.RateLimit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
if (leakyBucketRateLimiter != null) {
if (!leakyBucketRateLimiter.tryAcquire()) {
throw new RateLimitExceededException("请求过于频繁,请稍后再试");
}
}
return joinPoint.proceed();
}
}

View File

@ -1,133 +0,0 @@
package io.lroyia.ccdemo.common.util;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 漏桶算法限流器
*
* 漏桶算法通过控制请求的流出速率来实现限流请求以任意速率进入桶中但以固定速率流出
* 如果桶满了新的请求会被丢弃
*
* @author lroyia
* @since 1.0.0
*/
public class LeakyBucketRateLimiter {
private final long capacity;
private final long rate;
private final long rateIntervalNanos;
private final AtomicLong water;
private volatile long lastLeakTime;
public LeakyBucketRateLimiter(long capacity, long rate, long rateInterval, TimeUnit timeUnit) {
if (capacity <= 0) {
throw new IllegalArgumentException("Capacity must be positive");
}
if (rate <= 0) {
throw new IllegalArgumentException("Rate must be positive");
}
if (rateInterval <= 0) {
throw new IllegalArgumentException("Rate interval must be positive");
}
this.capacity = capacity;
this.rate = rate;
this.rateIntervalNanos = timeUnit.toNanos(rateInterval);
this.water = new AtomicLong(0);
this.lastLeakTime = System.nanoTime();
}
public boolean tryAcquire() {
return tryAcquire(1);
}
public boolean tryAcquire(int permits) {
if (permits <= 0) {
throw new IllegalArgumentException("Permits must be positive");
}
long now = System.nanoTime();
long waterLevel = water.get();
leak(now, waterLevel);
if (waterLevel + permits <= capacity) {
if (water.compareAndSet(waterLevel, waterLevel + permits)) {
return true;
}
}
return false;
}
private void leak(long now, long currentWaterLevel) {
long timeSinceLastLeak = now - lastLeakTime;
if (timeSinceLastLeak >= rateIntervalNanos) {
long leakAmount = (timeSinceLastLeak / rateIntervalNanos) * rate;
if (leakAmount > 0) {
long newWaterLevel = Math.max(0, currentWaterLevel - leakAmount);
if (water.compareAndSet(currentWaterLevel, newWaterLevel)) {
lastLeakTime = now;
}
}
}
}
public long getCurrentWaterLevel() {
long now = System.nanoTime();
long waterLevel = water.get();
leak(now, waterLevel);
return water.get();
}
public long getCapacity() {
return capacity;
}
public long getRate() {
return rate;
}
public void reset() {
water.set(0);
lastLeakTime = System.nanoTime();
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private long capacity = 100;
private long rate = 10;
private long rateInterval = 1;
private TimeUnit timeUnit = TimeUnit.SECONDS;
public Builder capacity(long capacity) {
this.capacity = capacity;
return this;
}
public Builder rate(long rate) {
this.rate = rate;
return this;
}
public Builder rateInterval(long rateInterval) {
this.rateInterval = rateInterval;
return this;
}
public Builder timeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
return this;
}
public LeakyBucketRateLimiter build() {
return new LeakyBucketRateLimiter(capacity, rate, rateInterval, timeUnit);
}
}
}

View File

@ -1,69 +0,0 @@
package io.lroyia.ccdemo.config;
import io.lroyia.ccdemo.common.util.LeakyBucketRateLimiter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 限流器配置类
*
* @author lroyia
* @since 1.0.0
*/
@Configuration
@ConfigurationProperties(prefix = "rate.limiter")
public class RateLimiterConfig {
private boolean enabled = true;
private long capacity = 100;
private long rate = 10;
private long rateInterval = 1;
@Bean
public LeakyBucketRateLimiter leakyBucketRateLimiter() {
if (!enabled) {
return null;
}
return LeakyBucketRateLimiter.builder()
.capacity(capacity)
.rate(rate)
.rateInterval(rateInterval)
.timeUnit(TimeUnit.SECONDS)
.build();
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public long getCapacity() {
return capacity;
}
public void setCapacity(long capacity) {
this.capacity = capacity;
}
public long getRate() {
return rate;
}
public void setRate(long rate) {
this.rate = rate;
}
public long getRateInterval() {
return rateInterval;
}
public void setRateInterval(long rateInterval) {
this.rateInterval = rateInterval;
}
}

View File

@ -8,7 +8,6 @@ import com.chinaweal.youfool.framework.springboot.common.util.RSAUtil;
import com.chinaweal.youfool.framework.springboot.rest.RestResult;
import com.chinaweal.youfool.framework.springboot.rest.ResultCode;
import com.chinaweal.youfool.framework.springboot.user.entity.UserBase;
import io.lroyia.ccdemo.annotation.RateLimit;
import io.lroyia.ccdemo.common.constants.SessionConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
@ -42,7 +41,6 @@ public class LoginController extends BaseController {
* @since 2022年4月20日 15:47:35
*/
@PostMapping("login")
@RateLimit
public RestResult<?> doLogin(String username, String password, Boolean encrypt) {
AssertUtils.isNotBlank(username, password);
if (encrypt != null && encrypt) {

View File

@ -1,18 +0,0 @@
package io.lroyia.ccdemo.exception;
/**
* 限流超出异常
*
* @author lroyia
* @since 1.0.0
*/
public class RateLimitExceededException extends RuntimeException {
public RateLimitExceededException(String message) {
super(message);
}
public RateLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -1,33 +0,0 @@
package io.lroyia.ccdemo.security.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import io.lroyia.ccdemo.security.filter.XSSFilter;
import io.lroyia.ccdemo.security.filter.SecurityHeadersFilter;
@Configuration
public class XSSProtectionConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<XSSFilter> xssFilterRegistration() {
FilterRegistrationBean<XSSFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XSSFilter());
registration.addUrlPatterns("/*");
registration.setName("xssFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registration;
}
@Bean
public FilterRegistrationBean<SecurityHeadersFilter> securityHeadersFilterRegistration() {
FilterRegistrationBean<SecurityHeadersFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SecurityHeadersFilter());
registration.addUrlPatterns("/*");
registration.setName("securityHeadersFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}

View File

@ -1,73 +0,0 @@
package io.lroyia.ccdemo.security.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "security.xss")
public class XSSProtectionProperties {
private boolean enabled = true;
private boolean validateInput = true;
private boolean sanitizeOutput = true;
private boolean enableCSP = true;
private String cspPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';";
private String[] excludedPaths = {"/druid/**", "/swagger-resources/**", "/v2/api-docs/**", "/webjars/**"};
private String[] staticResourcePaths = {"**/*.css", "**/*.js", "**/*.jpg", "**/*.png", "**/*.gif", "**/*.ico", "**/*.woff", "**/*.woff2", "**/*.ttf", "**/*.eot"};
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isValidateInput() {
return validateInput;
}
public void setValidateInput(boolean validateInput) {
this.validateInput = validateInput;
}
public boolean isSanitizeOutput() {
return sanitizeOutput;
}
public void setSanitizeOutput(boolean sanitizeOutput) {
this.sanitizeOutput = sanitizeOutput;
}
public boolean isEnableCSP() {
return enableCSP;
}
public void setEnableCSP(boolean enableCSP) {
this.enableCSP = enableCSP;
}
public String getCspPolicy() {
return cspPolicy;
}
public void setCspPolicy(String cspPolicy) {
this.cspPolicy = cspPolicy;
}
public String[] getExcludedPaths() {
return excludedPaths;
}
public void setExcludedPaths(String[] excludedPaths) {
this.excludedPaths = excludedPaths;
}
public String[] getStaticResourcePaths() {
return staticResourcePaths;
}
public void setStaticResourcePaths(String[] staticResourcePaths) {
this.staticResourcePaths = staticResourcePaths;
}
}

View File

@ -1,53 +0,0 @@
package io.lroyia.ccdemo.security.filter;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class SecurityHeadersFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
response.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
String csp = "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"media-src 'self'; " +
"object-src 'none'; " +
"frame-src 'none'; " +
"base-uri 'self'; " +
"form-action 'self'; " +
"frame-ancestors 'none'; " +
"require-trusted-types-for 'script';";
response.setHeader("Content-Security-Policy", csp);
response.setHeader("Content-Security-Policy-Report-Only", csp);
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/prj/druid") ||
path.startsWith("/prj/swagger-resources") ||
path.startsWith("/prj/v2/api-docs") ||
path.startsWith("/prj/webjars");
}
}

View File

@ -1,139 +0,0 @@
package io.lroyia.ccdemo.security.filter;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.regex.Pattern;
@Component
public class XSSFilter extends OncePerRequestFilter {
private static final String[] XSS_PATTERNS = {
"<script.*?>.*?</script.*?>",
"<iframe.*?>.*?</iframe.*?>",
"<object.*?>.*?</object.*?>",
"<embed.*?>.*?</embed.*?>",
"<applet.*?>.*?</applet.*?>",
"<form.*?>.*?</form.*?>",
"<input.*?>",
"<img.*?>",
"<a.*?>.*?</a.*?>",
"javascript:",
"vbscript:",
"onload=",
"onerror=",
"onclick=",
"onmouseover=",
"onmouseout=",
"onfocus=",
"onblur=",
"onchange=",
"onsubmit=",
"onreset=",
"onselect=",
"onunload=",
"onabort=",
"onkeydown=",
"onkeyup=",
"onkeypress=",
"ondblclick=",
"onmousedown=",
"onmouseup=",
"onmousemove=",
"expression\\(",
"<.*?\\+.*?>",
"<.*?\\|.*?>",
"<.*?&.*?>",
"<.*?\\$.*?>"
};
private static final Pattern[] XSS_REGEX_PATTERNS = new Pattern[XSS_PATTERNS.length];
static {
for (int i = 0; i < XSS_PATTERNS.length; i++) {
XSS_REGEX_PATTERNS[i] = Pattern.compile(XSS_PATTERNS[i], Pattern.CASE_INSENSITIVE);
}
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
XSSRequestWrapper wrappedRequest = new XSSRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
}
private static class XSSRequestWrapper extends HttpServletRequestWrapper {
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
return sanitizeXSS(value);
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) {
return null;
}
String[] sanitizedValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
sanitizedValues[i] = sanitizeXSS(values[i]);
}
return sanitizedValues;
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return sanitizeXSS(value);
}
private String sanitizeXSS(String value) {
if (value == null) {
return null;
}
String cleanValue = value;
for (Pattern pattern : XSS_REGEX_PATTERNS) {
cleanValue = pattern.matcher(cleanValue).replaceAll("");
}
cleanValue = cleanValue.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#x27;")
.replaceAll("/", "&#x2F;");
return cleanValue;
}
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/prj/druid") ||
path.startsWith("/prj/swagger-resources") ||
path.startsWith("/prj/v2/api-docs") ||
path.startsWith("/prj/webjars") ||
path.endsWith(".css") ||
path.endsWith(".js") ||
path.endsWith(".jpg") ||
path.endsWith(".png") ||
path.endsWith(".gif") ||
path.endsWith(".ico");
}
}

View File

@ -1,100 +0,0 @@
package io.lroyia.ccdemo.security.util;
import org.apache.commons.text.StringEscapeUtils;
import org.owasp.encoder.Encode;
public class OutputEncoder {
public static String forHtml(String input) {
if (input == null) {
return null;
}
return Encode.forHtml(input);
}
public static String forHtmlContent(String input) {
if (input == null) {
return null;
}
return Encode.forHtmlContent(input);
}
public static String forHtmlAttribute(String input) {
if (input == null) {
return null;
}
return Encode.forHtmlAttribute(input);
}
public static String forJavaScript(String input) {
if (input == null) {
return null;
}
return Encode.forJavaScript(input);
}
public static String forJavaScriptBlock(String input) {
if (input == null) {
return null;
}
return Encode.forJavaScriptBlock(input);
}
public static String forCssString(String input) {
if (input == null) {
return null;
}
return Encode.forCssString(input);
}
public static String forCssUrl(String input) {
if (input == null) {
return null;
}
return Encode.forCssUrl(input);
}
public static String forUri(String input) {
if (input == null) {
return null;
}
return Encode.forUri(input);
}
public static String forXmlAttribute(String input) {
if (input == null) {
return null;
}
return Encode.forXmlAttribute(input);
}
public static String forJava(String input) {
if (input == null) {
return null;
}
return StringEscapeUtils.escapeJava(input);
}
public static String forJson(String input) {
if (input == null) {
return null;
}
return StringEscapeUtils.escapeJson(input);
}
public static String forXml(String input) {
if (input == null) {
return null;
}
return StringEscapeUtils.escapeXml11(input);
}
public static String forSql(String input) {
if (input == null) {
return null;
}
return input.replace("'", "''")
.replace("\"", "\"\"")
.replace("\\", "\\\\");
}
}

View File

@ -1,161 +0,0 @@
package io.lroyia.ccdemo.security.validator;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
@Component
public class XSSValidator {
private static final String[] MALICIOUS_PATTERNS = {
"<script.*?>.*?</script.*?>",
"<iframe.*?>.*?</iframe.*?>",
"<object.*?>.*?</object.*?>",
"<embed.*?>.*?</embed.*?>",
"<applet.*?>.*?</applet.*?>",
"<form.*?>.*?</form.*?>",
"<input.*?>",
"<img.*?>",
"<a.*?>.*?</a.*?>",
"javascript:",
"vbscript:",
"data:text/html",
"onload=",
"onerror=",
"onclick=",
"onmouseover=",
"onmouseout=",
"onfocus=",
"onblur=",
"onchange=",
"onsubmit=",
"onreset=",
"onselect=",
"onunload=",
"onabort=",
"onkeydown=",
"onkeyup=",
"onkeypress=",
"ondblclick=",
"onmousedown=",
"onmouseup=",
"onmousemove=",
"expression\\(",
"eval\\(",
"alert\\(",
"confirm\\(",
"prompt\\(",
"document\\.",
"window\\.",
"location\\.",
"cookie\\.",
"sessionStorage\\.",
"localStorage\\.",
"<.*?\\+.*?>",
"<.*?\\|.*?>",
"<.*?&.*?>",
"<.*?\\$.*?>",
"<.*?%.*?>",
"<.*?#.*?>",
"<.*?@.*?>",
"<.*?!.*?>",
"<.*?\\*.*?>",
"<.*?\\^.*?>",
"<.*?~.*?>",
"<.*?`.*?>",
"<.*?\\{.*?>",
"<.*?\\}.*?>",
"<.*?\\[.*?>",
"<.*?\\].*?>",
"<.*?\\(.*?>",
"<.*?\\).*?>",
"<.*?\\\\.*?>",
"<.*?\\/.*?>"
};
private static final Pattern[] MALICIOUS_REGEX_PATTERNS = new Pattern[MALICIOUS_PATTERNS.length];
static {
for (int i = 0; i < MALICIOUS_PATTERNS.length; i++) {
MALICIOUS_REGEX_PATTERNS[i] = Pattern.compile(MALICIOUS_PATTERNS[i], Pattern.CASE_INSENSITIVE);
}
}
public boolean containsXSS(String input) {
if (input == null || input.isEmpty()) {
return false;
}
for (Pattern pattern : MALICIOUS_REGEX_PATTERNS) {
if (pattern.matcher(input).find()) {
return true;
}
}
return false;
}
public void validateInput(String input, String fieldName) throws SecurityException {
if (containsXSS(input)) {
throw new SecurityException("Potential XSS attack detected in field: " + fieldName);
}
}
public void validateInput(String[] inputs, String fieldName) throws SecurityException {
if (inputs == null) {
return;
}
for (String input : inputs) {
if (containsXSS(input)) {
throw new SecurityException("Potential XSS attack detected in field: " + fieldName);
}
}
}
public String sanitizeInput(String input) {
if (input == null) {
return null;
}
String cleanValue = input;
for (Pattern pattern : MALICIOUS_REGEX_PATTERNS) {
cleanValue = pattern.matcher(cleanValue).replaceAll("");
}
cleanValue = cleanValue.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#x27;")
.replaceAll("/", "&#x2F;");
return cleanValue;
}
public boolean isValidInput(String input) {
return !containsXSS(input);
}
public boolean isSafeHtml(String input) {
if (input == null || input.isEmpty()) {
return true;
}
String lowerInput = input.toLowerCase();
if (lowerInput.contains("<script") || lowerInput.contains("</script>")) {
return false;
}
if (lowerInput.contains("javascript:")) {
return false;
}
if (lowerInput.contains("onload=") || lowerInput.contains("onerror=")) {
return false;
}
return true;
}
}

View File

@ -1,78 +0,0 @@
package io.lroyia.ccdemo.system.controller;
import com.chinaweal.youfool.framework.springboot.common.base.BaseController;
import com.chinaweal.youfool.framework.springboot.rest.RestResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 文件上传接口
*
* @author lroyia
* @since 2025/9/20
**/
@Slf4j
@RestController
@RequestMapping("/system/file")
public class FileUploadController extends BaseController {
@Value("${file.upload.path}")
private String uploadPath;
/**
* 文件上传接口
*
* @param file 多部件文件
* @return 上传结果
* @author lroyia
* @since 2025年9月20日
*/
@PostMapping("/upload")
public RestResult<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return RestResult.error("文件不能为空");
}
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || originalFilename.isEmpty()) {
return RestResult.error("文件名不能为空");
}
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) {
uploadDir.mkdirs();
}
String fileExtension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex > 0) {
fileExtension = originalFilename.substring(dotIndex);
}
String uniqueFileName = System.currentTimeMillis() + "_" + originalFilename;
Path filePath = Paths.get(uploadPath, uniqueFileName);
Files.copy(file.getInputStream(), filePath);
log.info("文件上传成功: {} -> {}", originalFilename, filePath.toString());
return RestResult.ok("文件上传成功");
} catch (IOException e) {
log.error("文件上传失败", e);
return RestResult.error("文件上传失败: " + e.getMessage());
}
}
}

View File

@ -66,18 +66,6 @@ knife4j:
# username: admin
# password: 123456
file:
upload:
path: D:/uploads
# 限流器配置
rate:
limiter:
enabled: true
capacity: 100
rate: 10
rate-interval: 1
server:
servlet:
context-path: /prj
@ -96,28 +84,4 @@ sa-token:
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false
security:
xss:
enabled: true
validate-input: true
sanitize-output: true
enable-csp: true
csp-policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; media-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"
excluded-paths:
- "/druid/**"
- "/swagger-resources/**"
- "/v2/api-docs/**"
- "/webjars/**"
static-resource-paths:
- "**/*.css"
- "**/*.js"
- "**/*.jpg"
- "**/*.png"
- "**/*.gif"
- "**/*.ico"
- "**/*.woff"
- "**/*.woff2"
- "**/*.ttf"
- "**/*.eot"
is-log: false