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