/** * 自然写互动课堂教学管理云平台软件 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> 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 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; } } }