Files
2026-03-22 15:24:40 +08:00

257 lines
9.9 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自然写互动课堂教学管理云平台软件 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;
}
}
}