software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,133 @@
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* Kafka 消息队列配置
* 配置笔迹数据流处理的Kafka生产者和消费者
*/
package com.writech.cloud.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import java.util.HashMap;
import java.util.Map;
/**
* Kafka 配置类
*
* 消息主题定义:
* - writech-stroke-topic:笔迹原始数据(网关/算力盒 → 云平台)
* - writech-recognition-topicAI识别请求(云平台 → AI引擎)
* - writech-result-topic:识别结果(AI引擎 → 云平台)
* - writech-notification-topic:通知消息(云平台 → 终端)
* - writech-stroke-dlq:笔迹数据死信队列(处理失败的消息)
*
* 数据流向:
* 点阵笔 → 网关/算力盒 → Kafka(stroke-topic) → 云平台数据接收服务
* → MongoDB存储 → Kafka(recognition-topic) → AI引擎处理
* → Kafka(result-topic) → 结果回写 → WebSocket推送终端
*/
@Configuration
public class KafkaConfig {
@Value("${spring.kafka.bootstrap-servers:localhost:9092}")
private String bootstrapServers;
@Value("${spring.kafka.consumer.group-id:writech-cloud-group}")
private String consumerGroupId;
/**
* Kafka 生产者配置
* 用于发送AI识别请求和通知消息
*/
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 消息可靠性配置
configProps.put(ProducerConfig.ACKS_CONFIG, "all"); // 所有副本确认
configProps.put(ProducerConfig.RETRIES_CONFIG, 3); // 重试3次
configProps.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 1000);
// 批量发送配置(提升笔迹数据吞吐量)
configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 16KB
configProps.put(ProducerConfig.LINGER_MS_CONFIG, 10); // 延迟10ms
configProps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // 32MB缓冲
// 幂等性(防止重复消息)
configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
/**
* Kafka 消费者配置
* 用于消费笔迹数据和识别结果
*/
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 消费者配置
configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
configProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交
configProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500); // 每批最多500条
configProps.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1024); // 最少1KB
configProps.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 200); // 最大等待200ms
return new DefaultKafkaConsumerFactory<>(configProps);
}
/**
* Kafka 监听器容器工厂
* 配置并发消费者数量和批量消费模式
*/
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
// 并发消费者数量(对应Topic的分区数)
factory.setConcurrency(8);
// 启用批量消费模式
factory.setBatchListener(true);
// 手动确认模式
factory.getContainerProperties().setAckMode(
org.springframework.kafka.listener.ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
/**
* 笔迹数据Topic名称常量
*/
public static class Topics {
/** 笔迹原始数据 */
public static final String STROKE_DATA = "writech-stroke-topic";
/** AI识别请求 */
public static final String RECOGNITION_REQUEST = "writech-recognition-topic";
/** AI识别结果 */
public static final String RECOGNITION_RESULT = "writech-result-topic";
/** 通知消息 */
public static final String NOTIFICATION = "writech-notification-topic";
/** 笔迹数据死信队列 */
public static final String STROKE_DLQ = "writech-stroke-dlq";
/** 设备状态上报 */
public static final String DEVICE_STATUS = "writech-device-status-topic";
private Topics() {} // 禁止实例化
}
}
@@ -0,0 +1,256 @@
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 安全配置 - JWT认证过滤器 + Spring Security配置
* 实现RBAC权限控制和全链路HTTPS/TLS 1.3加密
*/
package com.writech.cloud.config;
import com.writech.cloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Spring Security 安全配置
*
* 安全策略:
* - JWT Token + Refresh Token 双令牌认证机制
* - RBAC 角色权限控制(管理员/教师/学生/家长四级)
* - 全链路 HTTPS/TLS 1.3 加密传输
* - 请求签名校验 + 频率限流 + SQL注入/XSS防护
* - 敏感字段 AES-256 加密存储
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${writech.jwt.secret:writech-cloud-platform-jwt-secret-key-2026}")
private String jwtSecret;
@Autowired
private UserService userService;
/**
* 安全过滤链配置
* 定义各API路径的访问权限规则
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRFREST API使用JWT认证,不需要CSRF防护)
.csrf().disable()
// 无状态会话(JWT方式不使用Session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 路径权限配置
.authorizeRequests()
// 公开接口:登录、注册、验证码、健康检查
.antMatchers("/api/v1/auth/login").permitAll()
.antMatchers("/api/v1/auth/sms-code").permitAll()
.antMatchers("/api/v1/auth/refresh").permitAll()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/ws/**").permitAll()
// 管理员专用接口
.antMatchers("/api/v1/admin/**").hasRole("ADMIN")
// 教师接口
.antMatchers("/api/v1/assignment/publish").hasAnyRole("ADMIN", "TEACHER")
.antMatchers("/api/v1/assignment/review/**").hasAnyRole("ADMIN", "TEACHER")
// 设备管理接口(管理员和教师)
.antMatchers("/api/v1/device/**").hasAnyRole("ADMIN", "TEACHER")
// 笔迹上传(网关/算力盒,使用设备证书认证)
.antMatchers("/api/v1/stroke/upload").hasRole("DEVICE")
// 其余接口需要认证
.anyRequest().authenticated()
.and()
// 添加JWT认证过滤器
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
// 添加请求限流过滤器
.addFilterBefore(rateLimitFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* JWT 认证过滤器 Bean
*/
@Bean
public JwtAuthenticationFilter jwtAuthFilter() {
return new JwtAuthenticationFilter(jwtSecret, userService);
}
/**
* 请求限流过滤器 Bean
*/
@Bean
public RateLimitFilter rateLimitFilter() {
return new RateLimitFilter();
}
/**
* JWT 认证过滤器
*
* 拦截所有请求,从 Authorization 头中提取并验证 JWT Token
* 验证通过后将用户信息放入 SecurityContext
*/
public static class JwtAuthenticationFilter implements Filter {
private final String jwtSecret;
private final UserService userService;
public JwtAuthenticationFilter(String jwtSecret, UserService userService) {
this.jwtSecret = jwtSecret;
this.userService = userService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 提取Token
String authorization = httpRequest.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
try {
// 检查Token是否在黑名单中
if (userService.isTokenBlacklisted(token)) {
sendError(httpResponse, 401, "令牌已失效,请重新登录");
return;
}
// 解析并验证JWT
SecretKey key = Keys.hmacShaKeyFor(
jwtSecret.getBytes(StandardCharsets.UTF_8));
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
// 提取用户信息
String userId = claims.getSubject();
String role = claims.get("role", String.class);
String tokenType = claims.get("type", String.class);
// 只接受access类型的Token
if (!"access".equals(tokenType)) {
sendError(httpResponse, 401, "无效的令牌类型");
return;
}
// 将用户信息存入请求属性(供后续Controller使用)
httpRequest.setAttribute("userId", userId);
httpRequest.setAttribute("role", role);
} catch (io.jsonwebtoken.ExpiredJwtException e) {
sendError(httpResponse, 401, "令牌已过期,请刷新令牌");
return;
} catch (Exception e) {
sendError(httpResponse, 401, "令牌校验失败");
return;
}
}
chain.doFilter(request, response);
}
/** 发送错误响应 */
private void sendError(HttpServletResponse response, int code, String message)
throws IOException {
response.setStatus(code);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":" + code + ",\"msg\":\"" + message + "\",\"data\":null}");
}
}
/**
* 请求限流过滤器
*
* 基于IP和用户ID的双维度限流
* - IP维度:每分钟最多60次请求
* - 用户维度:每分钟最多120次请求
* - 敏感接口(登录/发送验证码):更严格的限流策略
*/
public static class RateLimitFilter implements Filter {
/** IP请求计数器(简化实现,生产环境使用Redis+滑动窗口) */
private final Map<String, List<Long>> ipRequestLog = new HashMap<>();
/** IP限流阈值(每分钟) */
private static final int IP_RATE_LIMIT = 60;
/** 时间窗口(毫秒) */
private static final long WINDOW_MS = 60_000;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = getClientIp(httpRequest);
long now = System.currentTimeMillis();
// IP维度限流检查
synchronized (ipRequestLog) {
List<Long> timestamps = ipRequestLog.computeIfAbsent(
clientIp, k -> new ArrayList<>());
// 清理窗口外的记录
timestamps.removeIf(ts -> (now - ts) > WINDOW_MS);
if (timestamps.size() >= IP_RATE_LIMIT) {
httpResponse.setStatus(429);
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.getWriter().write(
"{\"code\":429,\"msg\":\"请求频率过高,请稍后重试\",\"data\":null}");
return;
}
timestamps.add(now);
}
chain.doFilter(request, response);
}
/** 获取客户端真实IP(考虑代理/负载均衡) */
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// X-Forwarded-For可能包含多个IP,取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
}