software copyright
This commit is contained in:
@@ -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-topic:AI识别请求(云平台 → 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
|
||||
// 禁用CSRF(REST 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user