generated from youfool-project/youfool-prj-springboot-template
Compare commits
No commits in common. "72ff1c7b38ebd9449cabbf42719e36888a64fa9c" and "d91c141c6d67dd12c2910c581e2d5a0f7b345717" have entirely different histories.
72ff1c7b38
...
d91c141c6d
194
README.md
194
README.md
|
|
@ -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 加密
|
使用事务时需注意使用`@DSTransactional`代替`@Transactional`来使用注解式事务。
|
||||||
- 🔐 **认证授权**:基于 Sa-Token 的权限管理
|
|
||||||
- 📊 **API 文档**:集成 Knife4j (Swagger) 接口文档
|
|
||||||
- 📈 **监控日志**:集成 Druid 数据源监控和 REST 日志记录
|
|
||||||
- 🚦 **限流控制**:内置漏桶算法限流器
|
|
||||||
- 🗄️ **数据源**:支持动态数据源切换
|
|
||||||
- 📁 **文件管理**:支持文件上传功能
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **框架**: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
10
pom.xml
|
|
@ -12,12 +12,12 @@
|
||||||
<url>https://www.chinaweal.com.cn</url>
|
<url>https://www.chinaweal.com.cn</url>
|
||||||
<description>boot基础的后台模板</description>
|
<description>boot基础的后台模板</description>
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>1.8</java.version>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>1.8</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>1.8</maven.compiler.target>
|
||||||
<maven.compiler.compilerVersion>21</maven.compiler.compilerVersion>
|
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
|
||||||
<failOnMissingWebXml>false</failOnMissingWebXml>
|
<failOnMissingWebXml>false</failOnMissingWebXml>
|
||||||
<spring.boot.version>2.3.5.RELEASE</spring.boot.version>
|
<spring.boot.version>2.3.5.RELEASE</spring.boot.version>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.30</version>
|
<version>1.18.12</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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.RestResult;
|
||||||
import com.chinaweal.youfool.framework.springboot.rest.ResultCode;
|
import com.chinaweal.youfool.framework.springboot.rest.ResultCode;
|
||||||
import com.chinaweal.youfool.framework.springboot.user.entity.UserBase;
|
import com.chinaweal.youfool.framework.springboot.user.entity.UserBase;
|
||||||
import io.lroyia.ccdemo.annotation.RateLimit;
|
|
||||||
import io.lroyia.ccdemo.common.constants.SessionConstants;
|
import io.lroyia.ccdemo.common.constants.SessionConstants;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
@ -42,7 +41,6 @@ public class LoginController extends BaseController {
|
||||||
* @since 2022年4月20日 15:47:35
|
* @since 2022年4月20日 15:47:35
|
||||||
*/
|
*/
|
||||||
@PostMapping("login")
|
@PostMapping("login")
|
||||||
@RateLimit
|
|
||||||
public RestResult<?> doLogin(String username, String password, Boolean encrypt) {
|
public RestResult<?> doLogin(String username, String password, Boolean encrypt) {
|
||||||
AssertUtils.isNotBlank(username, password);
|
AssertUtils.isNotBlank(username, password);
|
||||||
if (encrypt != null && encrypt) {
|
if (encrypt != null && encrypt) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll("\"", """)
|
|
||||||
.replaceAll("'", "'")
|
|
||||||
.replaceAll("/", "/");
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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("\\", "\\\\");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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("<", "<")
|
|
||||||
.replaceAll(">", ">")
|
|
||||||
.replaceAll("\"", """)
|
|
||||||
.replaceAll("'", "'")
|
|
||||||
.replaceAll("/", "/");
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -66,18 +66,6 @@ knife4j:
|
||||||
# username: admin
|
# username: admin
|
||||||
# password: 123456
|
# password: 123456
|
||||||
|
|
||||||
file:
|
|
||||||
upload:
|
|
||||||
path: D:/uploads
|
|
||||||
|
|
||||||
# 限流器配置
|
|
||||||
rate:
|
|
||||||
limiter:
|
|
||||||
enabled: true
|
|
||||||
capacity: 100
|
|
||||||
rate: 10
|
|
||||||
rate-interval: 1
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
servlet:
|
servlet:
|
||||||
context-path: /prj
|
context-path: /prj
|
||||||
|
|
@ -97,27 +85,3 @@ sa-token:
|
||||||
token-style: uuid
|
token-style: uuid
|
||||||
# 是否输出操作日志
|
# 是否输出操作日志
|
||||||
is-log: false
|
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"
|
|
||||||
Loading…
Reference in New Issue