generated from youfool-project/youfool-prj-springboot-template
Compare commits
2 Commits
d91c141c6d
...
72ff1c7b38
| Author | SHA1 | Date |
|---|---|---|
|
|
72ff1c7b38 | |
|
|
e603ad7127 |
194
README.md
194
README.md
|
|
@ -1,8 +1,194 @@
|
||||||
# youfool-framework-springboot 基础项目模板
|
# CCDemo - Spring Boot 基础项目模板
|
||||||
|
|
||||||
## 数据源
|
基于 youfool-framework-springboot 3.3.1 的 Spring Boot 项目模板,集成常用功能和最佳实践。
|
||||||
|
|
||||||
当前版本(3.0版本以上)的基础框架使用了`苞米豆`的[动态数据源](https://github.com/baomidou/dynamic-datasource-spring-boot-starter) 。
|
## 项目特性
|
||||||
|
|
||||||
使用事务时需注意使用`@DSTransactional`代替`@Transactional`来使用注解式事务。
|
- 🛡️ **安全防护**:集成 XSS 防护、SM3 加密、RSA 加密
|
||||||
|
- 🔐 **认证授权**:基于 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>1.8</java.version>
|
<java.version>21</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>1.8</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>1.8</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
|
<maven.compiler.compilerVersion>21</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.12</version>
|
<version>1.18.30</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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 {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
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,6 +8,7 @@ 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;
|
||||||
|
|
@ -41,6 +42,7 @@ 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) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
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("\\", "\\\\");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
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,6 +66,18 @@ 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
|
||||||
|
|
@ -84,4 +96,28 @@ sa-token:
|
||||||
# token风格
|
# 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