diff --git a/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java b/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java index cb76caa..2b8089c 100644 --- a/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java +++ b/bonus-common/src/main/java/com/bonus/common/constant/CacheConstants.java @@ -52,4 +52,31 @@ public class CacheConstants * 登录IP黑名单 cache key */ public static final String SYS_LOGIN_BLACKIPLIST ="blackIPList"; + + + /** + * 系统配置 + * */ + public static final String SYSTEM_CONFIG_VOS ="systemConfigVos"; + + /** + * 请求加密 + * */ + public static final String ENCRYPT_REQUEST ="encryptRequest"; + + /** + * 响应加密 + * */ + public static final String ENCRYPT_RESPONSE ="encryptResponse"; + + /** + * 数据完整性校验 + * */ + public static final String CHECK_INTEGRITY ="checkIntegrity"; + + /** + * 防重放请求 + * */ + public static final String REPLAY_ATTACK ="replayAttack"; + } diff --git a/bonus-common/src/main/java/com/bonus/common/filter/RequestCoverFilter.java b/bonus-common/src/main/java/com/bonus/common/filter/RequestCoverFilter.java index b4a3e49..299b0d5 100644 --- a/bonus-common/src/main/java/com/bonus/common/filter/RequestCoverFilter.java +++ b/bonus-common/src/main/java/com/bonus/common/filter/RequestCoverFilter.java @@ -1,3 +1,4 @@ +/* package com.bonus.common.filter; import com.bonus.common.exception.CaptchaException; @@ -20,29 +21,37 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.*; +*/ /** * 请求内容存储 处理请求内容 内容放在gatewayContext中 * 解决数据流被重复读取无数据的 问题 * 对formData 数据进行解密 * * @author bonus - */ + *//* + @Configuration @Slf4j public class RequestCoverFilter implements Filter { - /** + */ +/** * 数据加密标志 - */ + *//* + public static final String ENCRYPT = "encryptRequest"; - /** + */ +/** * 数据完整性校验标志 - */ + *//* + public static final String INTEGRALITY = "checkIntegrity"; - /** + */ +/** * 完整性校验哈希值 - */ + *//* + public static final String HMAC_HEADER_NAME = "Params-Hash"; @Override @@ -79,9 +88,11 @@ public class RequestCoverFilter implements Filter { // 清理逻辑 } - /** + */ +/** * 处理 multipart/form-data 请求 - */ + *//* + private void handleMultipartRequest(HttpServletRequest request, ServletResponse response, FilterChain chain, boolean integrality, boolean encrypt) throws IOException, ServletException { @@ -95,9 +106,11 @@ public class RequestCoverFilter implements Filter { } } - /** + */ +/** * 处理请求体里的参数 - */ + *//* + private void handleBodyRequest(HttpServletRequest request, ServletResponse response, FilterChain chain, boolean integrality, boolean encrypt) throws IOException, ServletException { @@ -152,9 +165,11 @@ public class RequestCoverFilter implements Filter { } } - /** + */ +/** * 处理URL参数 - */ + *//* + private void handleUrlParams(HttpServletRequest request, ServletResponse response, FilterChain chain, boolean integrality, boolean encrypt) throws IOException, ServletException { @@ -168,9 +183,11 @@ public class RequestCoverFilter implements Filter { } } - /** + */ +/** * 更新查询参数,解密和验证数据完整性 - */ + *//* + private HttpServletRequest updateRequestParam(HttpServletRequest request, boolean integrality, boolean encrypt) { @@ -228,9 +245,11 @@ public class RequestCoverFilter implements Filter { return null; } - /** + */ +/** * 数据完整性校验 - */ + *//* + private void integrityVerification(String providedHmac, String query) { if (providedHmac == null) { log.error("完整性校验哈希值为空"); @@ -251,9 +270,11 @@ public class RequestCoverFilter implements Filter { System.err.println(calculatedHash); } - /** + */ +/** * 请求体包装类 - */ + *//* + private static class BodyCachingRequestWrapper extends HttpServletRequestWrapper { private final String body; @@ -294,9 +315,11 @@ public class RequestCoverFilter implements Filter { } } - /** + */ +/** * 查询字符串包装类 - */ + *//* + private static class QueryStringRequestWrapper extends HttpServletRequestWrapper { private final String queryString; private Map cachedParameterMap; @@ -336,9 +359,11 @@ public class RequestCoverFilter implements Filter { return getParameterMap().get(name); } - /** + */ +/** * 解析查询字符串,支持普通格式和嵌套格式 - */ + *//* + private Map parseQueryString(String queryString) { Map parameterMap = new HashMap<>(); if (queryString == null || queryString.trim().isEmpty()) { @@ -384,9 +409,11 @@ public class RequestCoverFilter implements Filter { return parameterMap; } - /** + */ +/** * 添加参数到Map,支持多值参数 - */ + *//* + private void addParameter(Map parameterMap, String key, String value) { if (parameterMap.containsKey(key)) { String[] existingValues = parameterMap.get(key); @@ -399,9 +426,11 @@ public class RequestCoverFilter implements Filter { // log.info("添加参数: {} = {}", key, value); } - /** + */ +/** * 简单解析查询字符串(备用方案) - */ + *//* + private Map parseSimpleQueryString(String queryString) { Map parameterMap = new HashMap<>(); if (queryString != null) { @@ -419,10 +448,12 @@ public class RequestCoverFilter implements Filter { } } - /** + */ +/** * Multipart 请求包装类 * 只处理 params 字段的解密(params 是 JSON 字符串),忽略文件字段 - */ + *//* + private static class MultipartRequestWrapper extends HttpServletRequestWrapper { private final Map parameterMap; private final boolean integrality; @@ -440,9 +471,11 @@ public class RequestCoverFilter implements Filter { processParams(); } - /** + */ +/** * 处理 params 参数(JSON 字符串格式) - */ + *//* + private void processParams() { String[] paramsValues = parameterMap.get("params"); if (paramsValues != null && paramsValues.length > 0) { @@ -482,9 +515,11 @@ public class RequestCoverFilter implements Filter { } } - /** + */ +/** * 数据完整性校验 - */ + *//* + private void integrityVerification(String providedHmac, String data) { if (providedHmac == null) { log.error("完整性校验哈希值为空"); @@ -531,4 +566,4 @@ public class RequestCoverFilter implements Filter { return ((HttpServletRequest) getRequest()).getPart(name); } } -} \ No newline at end of file +}*/ diff --git a/bonus-common/src/main/java/com/bonus/common/filter/ResponseEncryptFilter.java b/bonus-common/src/main/java/com/bonus/common/filter/ResponseEncryptFilter.java index a4b3ff0..5895b38 100644 --- a/bonus-common/src/main/java/com/bonus/common/filter/ResponseEncryptFilter.java +++ b/bonus-common/src/main/java/com/bonus/common/filter/ResponseEncryptFilter.java @@ -1,3 +1,4 @@ +/* package com.bonus.common.filter; import com.bonus.common.utils.encryption.Sm4Utils; @@ -10,18 +11,22 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; +*/ /** * 对返回的 data数据进行加密 * * @author 黑子 - */ + *//* + @Configuration @Slf4j public class ResponseEncryptFilter implements Filter { - /** + */ +/** * 加密标识 - */ + *//* + private static final String ENCRYPT_RESPONSE = "encryptResponse"; @Override @@ -87,15 +92,18 @@ public class ResponseEncryptFilter implements Filter { // 清理逻辑 } - /** + */ +/** * 判断是否需要对响应数据进行加密 * * @param request 请求对象 * @return 如果需要加密返回true,否则返回false - */ + *//* + private boolean shouldEncrypt(HttpServletRequest request) { String encryptHeader = request.getHeader(ENCRYPT_RESPONSE); return "true".equalsIgnoreCase(encryptHeader); } } +*/ diff --git a/bonus-framework/src/main/java/com/bonus/framework/config/FilterConfig.java b/bonus-framework/src/main/java/com/bonus/framework/config/FilterConfig.java index a2e5b35..22e3282 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/config/FilterConfig.java +++ b/bonus-framework/src/main/java/com/bonus/framework/config/FilterConfig.java @@ -2,12 +2,12 @@ package com.bonus.framework.config; import java.util.HashMap; import java.util.Map; +import javax.annotation.Resource; import javax.servlet.DispatcherType; -import com.bonus.common.filter.RequestCoverFilter; -import com.bonus.common.filter.ResponseEncryptFilter; import com.bonus.framework.filter.IpWhitelistFilter; -import com.bonus.framework.filter.ReplayAttackFilter; +import com.bonus.framework.filter.RequestCoverFilter; +import com.bonus.framework.filter.ResponseEncryptFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -32,6 +32,12 @@ public class FilterConfig @Value("${xss.urlPatterns}") private String urlPatterns; + @Resource(name = "RequestCoverFilter") + private RequestCoverFilter requestCoverFilter; + + @Resource(name = "ResponseEncryptFilter") + private ResponseEncryptFilter responseEncryptFilter; + @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean @ConditionalOnProperty(value = "xss.enabled", havingValue = "true") @@ -64,7 +70,7 @@ public class FilterConfig @Bean public FilterRegistrationBean requestCoverFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setFilter(new RequestCoverFilter()); + registration.setFilter(requestCoverFilter); registration.addUrlPatterns("/*"); // registration.setOrder(Ordered.HIGHEST_PRECEDENCE); registration.setOrder(2); @@ -78,7 +84,7 @@ public class FilterConfig @Bean public FilterRegistrationBean responseEncryptFilterRegistration() { FilterRegistrationBean registration = new FilterRegistrationBean<>(); - registration.setFilter(new ResponseEncryptFilter()); + registration.setFilter(responseEncryptFilter); registration.addUrlPatterns("/*"); registration.setOrder(-5); // 设置过滤器顺序 registration.setName("responseEncryptFilter"); diff --git a/bonus-framework/src/main/java/com/bonus/framework/config/SecurityConfig.java b/bonus-framework/src/main/java/com/bonus/framework/config/SecurityConfig.java index 29e6189..85f2d67 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/config/SecurityConfig.java +++ b/bonus-framework/src/main/java/com/bonus/framework/config/SecurityConfig.java @@ -112,7 +112,7 @@ public class SecurityConfig .authorizeHttpRequests((requests) -> { permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); // 对于登录login 注册register 验证码captchaImage 允许匿名访问 - requests.antMatchers("/login", "/register", "/captchaImage","/session/check").permitAll() + requests.antMatchers("/login", "/register", "/captchaImage","/sys/config/getConfig","/session/check").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() diff --git a/bonus-framework/src/main/java/com/bonus/framework/filter/IpWhitelistFilter.java b/bonus-framework/src/main/java/com/bonus/framework/filter/IpWhitelistFilter.java index 4439582..2bb7b8b 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/filter/IpWhitelistFilter.java +++ b/bonus-framework/src/main/java/com/bonus/framework/filter/IpWhitelistFilter.java @@ -1,7 +1,12 @@ package com.bonus.framework.filter; +import com.alibaba.fastjson2.JSON; +import com.bonus.common.constant.CacheConstants; +import com.bonus.common.core.redis.RedisCache; import com.bonus.common.utils.IpWhitelistUtils; +import com.bonus.system.domain.vo.SystemConfigVo; import com.bonus.system.service.ISysIpWhitelistService; +import com.bonus.system.service.ISystemConfigService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; @@ -14,6 +19,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -24,12 +30,24 @@ public class IpWhitelistFilter implements Filter { @Autowired private ISysIpWhitelistService whitelistService; + @Autowired + private ISystemConfigService configService; + + @Autowired + private RedisCache redisUtil; + + + private final AntPathMatcher pathMatcher = new AntPathMatcher(); private final ConcurrentHashMap ipCache = new ConcurrentHashMap<>(); private static final long CACHE_REFRESH_INTERVAL = TimeUnit.MINUTES.toMillis(1); private static final long ENTRY_TTL = TimeUnit.MINUTES.toMillis(1); - private static final long refreshTime = 1000 * 60; + private static final long refreshTime = 1000 * 60 * 5; + private static final long refreshTime2 = 1000 * 60 * 29; + private static final long TIMESTAMP_TOLERANCE = 15 * 60 * 1000; // 15分钟 + // 请求签名在Redis中的过期时间(秒) + private static final int SIGNATURE_EXPIRE_SECONDS = (int) (TIMESTAMP_TOLERANCE * 2 / 1000); private static final String[] EXCLUDE_PATHS = { // 排除路径 }; @@ -113,6 +131,16 @@ public class IpWhitelistFilter implements Filter { // log.info("IP白名单缓存刷新完成,清理了 {} 个缓存条目", sizeBefore); } + @Scheduled(fixedRate = refreshTime2) + public void refreshSystemConfigCache() { + log.info("开始定时刷新系统配置缓存..."); + List systemConfigVos = configService.listConfig(); + Boolean stored = redisUtil.setNxCacheObject(CacheConstants.SYSTEM_CONFIG_VOS, + JSON.toJSONString(systemConfigVos), + (long) SIGNATURE_EXPIRE_SECONDS, + TimeUnit.SECONDS); + } + private void sendForbiddenResponse(HttpServletResponse response, String message) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); diff --git a/bonus-framework/src/main/java/com/bonus/framework/filter/ReplayAttackFilter.java b/bonus-framework/src/main/java/com/bonus/framework/filter/ReplayAttackFilter.java index 0071f49..38fc1c3 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/filter/ReplayAttackFilter.java +++ b/bonus-framework/src/main/java/com/bonus/framework/filter/ReplayAttackFilter.java @@ -1,5 +1,6 @@ package com.bonus.framework.filter; +import javax.annotation.Resource; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -8,15 +9,22 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.bonus.common.constant.CacheConstants; import com.bonus.common.core.domain.entity.SysUser; import com.bonus.common.core.domain.model.LoginUser; import com.bonus.common.core.redis.RedisCache; import com.bonus.common.utils.encryption.Sm4Utils; import com.bonus.framework.web.service.TokenService; +import com.bonus.system.domain.vo.SystemConfigVo; +import com.bonus.system.service.ISystemConfigService; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @@ -28,8 +36,9 @@ import org.springframework.stereotype.Component; * @version:1.0 * @description:防重放攻击过滤器 */ -@Configuration +//@Configuration @Slf4j +@Component public class ReplayAttackFilter implements Filter { static List ignoreUrlPatterns = new ArrayList<>(); static { @@ -39,13 +48,20 @@ public class ReplayAttackFilter implements Filter { ignoreUrlPatterns.add("/smartArchives/getInfo"); ignoreUrlPatterns.add("/smartArchives/getRouters"); ignoreUrlPatterns.add("/smartArchives/session/check"); + ignoreUrlPatterns.add("/smartArchives/sys/config/getConfig"); + } + + private final RedisCache redisUtil; + + public ReplayAttackFilter(RedisCache redisUtil) { + this.redisUtil = redisUtil; } @Autowired - private RedisCache redisUtil; + private TokenService tokenService; @Autowired - private TokenService tokenService; + private ISystemConfigService configService; // 时间戳允许的误差范围(毫秒) private static final long TIMESTAMP_TOLERANCE = 5 * 60 * 1000; // 5分钟 @@ -54,12 +70,56 @@ public class ReplayAttackFilter implements Filter { // Redis键前缀 private static final String SIGNATURE_KEY_PREFIX = "replay:signature:"; + + private static final long TIMESTAMP_TOLERANCE2 = 15 * 60 * 1000; // 15分钟 + // 请求签名在Redis中的过期时间(秒) + private static final int SIGNATURE_EXPIRE_SECONDS2 = (int) (TIMESTAMP_TOLERANCE2 * 2 / 1000); + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; + + Object cacheObject = redisUtil.getCacheObject(CacheConstants.SYSTEM_CONFIG_VOS); + if(Objects.isNull(cacheObject)){ + List systemConfigVos = configService.listConfig(); + Boolean stored = redisUtil.setNxCacheObject(CacheConstants.SYSTEM_CONFIG_VOS, + JSON.toJSONString(systemConfigVos), + (long) SIGNATURE_EXPIRE_SECONDS, + TimeUnit.SECONDS); + if(CollectionUtils.isNotEmpty(systemConfigVos)){ + SystemConfigVo config = systemConfigVos.stream() + .filter(item -> CacheConstants.REPLAY_ATTACK.equals(item.getConfigCode())) + .findFirst() + .orElse(null); + if(Objects.nonNull(config)){ + String useStatus = config.getUseStatus(); + if(Objects.equals("1",useStatus)){ + chain.doFilter(request, response); + return; + } + } + } + }else{ + String cacheStr = (String) cacheObject; + List systemConfigVos = JSON.parseArray(cacheStr, SystemConfigVo.class); + if(CollectionUtils.isNotEmpty(systemConfigVos)){ + SystemConfigVo config = systemConfigVos.stream() + .filter(item -> CacheConstants.REPLAY_ATTACK.equals(item.getConfigCode())) + .findFirst() + .orElse(null); + if(Objects.nonNull(config)){ + String useStatus = config.getUseStatus(); + if(Objects.equals("1",useStatus)){ + chain.doFilter(request, response); + return; + } + } + } + } + // 跳过OPTIONS预检请求 if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { chain.doFilter(request, response); diff --git a/bonus-framework/src/main/java/com/bonus/framework/filter/RequestCoverFilter.java b/bonus-framework/src/main/java/com/bonus/framework/filter/RequestCoverFilter.java new file mode 100644 index 0000000..e8267cc --- /dev/null +++ b/bonus-framework/src/main/java/com/bonus/framework/filter/RequestCoverFilter.java @@ -0,0 +1,609 @@ +package com.bonus.framework.filter; + +import com.alibaba.fastjson2.JSON; +import com.bonus.common.constant.CacheConstants; +import com.bonus.common.core.redis.RedisCache; +import com.bonus.common.exception.CaptchaException; +import com.bonus.common.utils.encryption.Sm3Util; +import com.bonus.common.utils.encryption.Sm4Utils; +import com.bonus.system.domain.vo.SystemConfigVo; +import com.bonus.system.service.ISystemConfigService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StreamUtils; + +import javax.annotation.Resource; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.Part; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * 请求内容存储 处理请求内容 内容放在gatewayContext中 + * 解决数据流被重复读取无数据的 问题 + * 对formData 数据进行解密 + * + * @author bonus + */ + +//@Configuration +@Slf4j +@Component(value = "RequestCoverFilter") +public class RequestCoverFilter implements Filter { + + /** + * 数据加密标志 + */ + public static final String ENCRYPT = "encryptRequest"; + /** + * 数据完整性校验标志 + */ + public static final String INTEGRALITY = "checkIntegrity"; + /** + * 完整性校验哈希值 + */ + public static final String HMAC_HEADER_NAME = "Params-Hash"; + + private final RedisCache redisUtil; + + public RequestCoverFilter(RedisCache redisUtil) { + this.redisUtil = redisUtil; + } + + + @Autowired + private ISystemConfigService configService; + + private static final long TIMESTAMP_TOLERANCE = 15 * 60 * 1000; // 15分钟 + // 请求签名在Redis中的过期时间(秒) + private static final int SIGNATURE_EXPIRE_SECONDS = (int) (TIMESTAMP_TOLERANCE * 2 / 1000); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // 初始化逻辑 + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + + HttpServletRequest httpRequest = (HttpServletRequest) request; + // 获取系统请求加密和完整性校验状态 + boolean encrypt_request = getSystemConfigStatus(CacheConstants.ENCRYPT_REQUEST); + boolean check_integrity = getSystemConfigStatus(CacheConstants.CHECK_INTEGRITY); + // 获取请求头信息 + boolean integrality = "true".equalsIgnoreCase(httpRequest.getHeader(INTEGRALITY)) && check_integrity; + boolean encrypt = "true".equalsIgnoreCase(httpRequest.getHeader(ENCRYPT)) && encrypt_request; + + String contentType = httpRequest.getContentType(); + + // 处理不同类型的请求 + if (contentType == null) { +// log.info("请求头中无Content-Type信息,处理URL参数。"); + handleUrlParams(httpRequest, response, chain, integrality, encrypt); + } else if (contentType.contains(MediaType.APPLICATION_JSON_VALUE)) { + handleBodyRequest(httpRequest, response, chain, integrality, encrypt); + } else if (contentType.contains(MediaType.MULTIPART_FORM_DATA_VALUE)) { + handleMultipartRequest(httpRequest, response, chain, integrality, encrypt); + } else { + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + // 清理逻辑 + } + + /** + * 处理 multipart/form-data 请求 + */ + private void handleMultipartRequest(HttpServletRequest request, ServletResponse response, + FilterChain chain, boolean integrality, boolean encrypt) + throws IOException, ServletException { + try { + // 创建包装的请求对象,处理 multipart 请求 + MultipartRequestWrapper wrappedRequest = new MultipartRequestWrapper(request, integrality, encrypt); + chain.doFilter(wrappedRequest, response); + } catch (Exception e) { + log.error("处理 multipart 请求时发生错误: {}", e.getMessage(), e); + throw new ServletException("请求处理失败", e); + } + } + + /** + * 处理请求体里的参数 + */ + private void handleBodyRequest(HttpServletRequest request, ServletResponse response, + FilterChain chain, boolean integrality, boolean encrypt) + throws IOException, ServletException { + + // 读取请求体内容 + String requestBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); + + if (ObjectUtils.isEmpty(requestBody)) { + chain.doFilter(request, response); + return; + } + + // 去掉多余的引号(如果有) + if (requestBody.startsWith("\"") && requestBody.endsWith("\"")) { + requestBody = requestBody.substring(1, requestBody.length() - 1); + } + + try { + // 解密请求体 + if (encrypt) { + requestBody = Sm4Utils.decrypt(requestBody); + } + + if (ObjectUtils.isEmpty(requestBody)) { + chain.doFilter(request, response); + return; + } + + // 校验数据完整性 + if (integrality) { + String[] parts = requestBody.split("\\|"); + if (parts.length != 2) { + log.error("解密后的请求体格式不正确: {}", requestBody); + throw new CaptchaException("请求参数不正确"); + } + integrityVerification(parts[1], parts[0]); + requestBody = parts[0]; + } + + if (ObjectUtils.isEmpty(requestBody)) { + chain.doFilter(request, response); + return; + } + + // 创建包装的请求对象 + HttpServletRequest wrappedRequest = new BodyCachingRequestWrapper(request, requestBody); + chain.doFilter(wrappedRequest, response); + + } catch (Exception e) { + log.error("处理请求体时发生错误: {}", e.getMessage(), e); + throw new ServletException("请求处理失败", e); + } + } + + /** + * 处理URL参数 + */ + private void handleUrlParams(HttpServletRequest request, ServletResponse response, + FilterChain chain, boolean integrality, boolean encrypt) + throws IOException, ServletException { + + try { + HttpServletRequest updatedRequest = updateRequestParam(request, integrality, encrypt); + chain.doFilter(updatedRequest != null ? updatedRequest : request, response); + } catch (Exception e) { + log.error("处理 GET 请求时发生错误: {}", e.getMessage(), e); + throw new ServletException("请求处理失败", e); + } + } + + /** + * 更新查询参数,解密和验证数据完整性 + */ + private HttpServletRequest updateRequestParam(HttpServletRequest request, + boolean integrality, boolean encrypt) { + + String queryString = request.getQueryString(); + if (!ObjectUtils.isEmpty(queryString)) { + try { + // 解析 params 参数 + String encryptedParams = null; + String[] queryParams = queryString.split("&"); + for (String param : queryParams) { + if (param.startsWith("params=")) { + encryptedParams = param.substring(7); // 去掉 "params=" + break; + } + } + + if (encryptedParams == null) { +// log.warn("未找到加密参数,跳过解密处理"); + return null; + } + + // URL解码 + encryptedParams = java.net.URLDecoder.decode(encryptedParams, StandardCharsets.UTF_8.name()); + + String query = encryptedParams; + + // 解密查询参数 + if (encrypt) { + query = Sm4Utils.decrypt(query); + } + + // 校验数据完整性 + if (integrality) { + String[] parts = query.split("\\|"); + if (parts.length != 2) { + log.error("解密后的参数格式不正确: {}", query); + throw new CaptchaException("请求参数不正确"); + } + integrityVerification(parts[1], parts[0]); + query = parts[0]; // 只保留原始参数部分 + } + + if (ObjectUtils.isEmpty(query)) { + return null; + } + + // 创建新的请求对象 + return new QueryStringRequestWrapper(request, query); + + } catch (Exception e) { + log.error("解密查询参数时发生错误: {}", e.getMessage(), e); + throw new CaptchaException("请求参数不正确"); + } + } + return null; + } + + /** + * 数据完整性校验 + */ + private void integrityVerification(String providedHmac, String query) { + if (providedHmac == null) { + log.error("完整性校验哈希值为空"); + throw new CaptchaException("请求参数不正确"); + } + String calculatedHash = Sm3Util.encrypt(query); +// log.info("计算出的哈希值: {}", calculatedHash); +// log.info("提供的哈希值: {}", providedHmac); + if (!calculatedHash.equals(providedHmac)) { + log.error("参数完整性校验失败"); + throw new CaptchaException("请求参数不正确"); + } + } + + public static void main(String[] args) { + String query = "pageNum=1&pageSize=10"; + String calculatedHash = Sm3Util.encrypt(query); + System.err.println(calculatedHash); + } + + /** + * 请求体包装类 + */ + private static class BodyCachingRequestWrapper extends HttpServletRequestWrapper { + private final String body; + + public BodyCachingRequestWrapper(HttpServletRequest request, String body) { + super(request); + this.body = body; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + return new ServletInputStream() { + @Override + public int read() throws IOException { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return byteArrayInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener listener) { + // 不需要实现 + } + }; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + } + + /** + * 查询字符串包装类 + */ + private static class QueryStringRequestWrapper extends HttpServletRequestWrapper { + private final String queryString; + private Map cachedParameterMap; + + public QueryStringRequestWrapper(HttpServletRequest request, String queryString) { + super(request); + this.queryString = queryString; +// log.info("QueryStringRequestWrapper 接收到的参数: {}", queryString); + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public String getParameter(String name) { + String[] values = getParameterMap().get(name); + return (values != null && values.length > 0) ? values[0] : null; + } + + @Override + public Map getParameterMap() { + if (cachedParameterMap == null) { + cachedParameterMap = parseQueryString(queryString); + } + return cachedParameterMap; + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(getParameterMap().keySet()); + } + + @Override + public String[] getParameterValues(String name) { + return getParameterMap().get(name); + } + + /** + * 解析查询字符串,支持普通格式和嵌套格式 + */ + private Map parseQueryString(String queryString) { + Map parameterMap = new HashMap<>(); + if (queryString == null || queryString.trim().isEmpty()) { + return parameterMap; + } + +// log.info("开始解析查询字符串: {}", queryString); + + try { + // 先URL解码 + String decodedQueryString = java.net.URLDecoder.decode(queryString, StandardCharsets.UTF_8.name()); + String[] pairs = decodedQueryString.split("&"); + + for (String pair : pairs) { + if (pair == null || pair.trim().isEmpty()) { + continue; + } + + String[] keyValue = pair.split("=", 2); + if (keyValue.length >= 1) { + String key = keyValue[0].trim(); + String value = keyValue.length == 2 ? keyValue[1].trim() : ""; + + // 处理嵌套参数格式(如 params[beginTime]) + if (key.startsWith("params[") && key.endsWith("]")) { + String nestedKey = key.substring(7, key.length() - 1); + String paramsKey = "params[" + nestedKey + "]"; + addParameter(parameterMap, paramsKey, value); + } else { + // 处理普通参数(如 pageNum=1, pageSize=10) + addParameter(parameterMap, key, value); + } + } + } + + } catch (Exception e) { + log.warn("解析查询字符串失败: {}", e.getMessage()); + // 失败时尝试简单解析 + return parseSimpleQueryString(queryString); + } + +// log.info("解析后的参数Map: {}", parameterMap); + return parameterMap; + } + + /** + * 添加参数到Map,支持多值参数 + */ + private void addParameter(Map parameterMap, String key, String value) { + if (parameterMap.containsKey(key)) { + String[] existingValues = parameterMap.get(key); + String[] newValues = Arrays.copyOf(existingValues, existingValues.length + 1); + newValues[existingValues.length] = value; + parameterMap.put(key, newValues); + } else { + parameterMap.put(key, new String[]{value}); + } +// log.info("添加参数: {} = {}", key, value); + } + + /** + * 简单解析查询字符串(备用方案) + */ + private Map parseSimpleQueryString(String queryString) { + Map parameterMap = new HashMap<>(); + if (queryString != null) { + String[] pairs = queryString.split("&"); + for (String pair : pairs) { + String[] keyValue = pair.split("=", 2); + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + addParameter(parameterMap, key, value); + } + } + } + return parameterMap; + } + } + + /** + * Multipart 请求包装类 + * 只处理 params 字段的解密(params 是 JSON 字符串),忽略文件字段 + */ + private static class MultipartRequestWrapper extends HttpServletRequestWrapper { + private final Map parameterMap; + private final boolean integrality; + private final boolean encrypt; + + public MultipartRequestWrapper(HttpServletRequest request, boolean integrality, boolean encrypt) { + super(request); + this.integrality = integrality; + this.encrypt = encrypt; + + // 复制原始参数映射 + this.parameterMap = new HashMap<>(request.getParameterMap()); + + // 处理 params 参数 + processParams(); + } + + /** + * 处理 params 参数(JSON 字符串格式) + */ + private void processParams() { + String[] paramsValues = parameterMap.get("params"); + if (paramsValues != null && paramsValues.length > 0) { + String encryptedParams = paramsValues[0]; + try { + String decryptedParams = encryptedParams; + + // 解密参数 + if (encrypt) { + decryptedParams = Sm4Utils.decrypt(decryptedParams); + } + + // 校验数据完整性 + if (integrality) { + String[] parts = decryptedParams.split("\\|"); + if (parts.length != 2) { + log.error("解密后的参数格式不正确: {}", decryptedParams); + throw new CaptchaException("请求参数不正确"); + } + integrityVerification(parts[1], parts[0]); + decryptedParams = parts[0]; // 只保留原始 JSON 字符串部分 + } + + if (!ObjectUtils.isEmpty(decryptedParams)) { + // 由于 params 是 JSON 字符串,我们将其作为原始参数保留 + // 而不是尝试解析为键值对,以保持 JSON 结构完整 + parameterMap.put("params", new String[]{decryptedParams}); + } else { + // 移除空的 params 参数 + parameterMap.remove("params"); + } + + } catch (Exception e) { + log.error("处理 multipart params 参数时发生错误: {}", e.getMessage(), e); + throw new CaptchaException("请求参数不正确"); + } + } + } + + /** + * 数据完整性校验 + */ + private void integrityVerification(String providedHmac, String data) { + if (providedHmac == null) { + log.error("完整性校验哈希值为空"); + throw new CaptchaException("请求参数不正确"); + } + String calculatedHash = Sm3Util.encrypt(data); + log.info("计算出的哈希值: {}", calculatedHash); + log.info("提供的哈希值: {}", providedHmac); + if (!calculatedHash.equals(providedHmac)) { + log.error("参数完整性校验失败"); + throw new CaptchaException("请求参数不正确"); + } + } + + @Override + public String getParameter(String name) { + String[] values = parameterMap.get(name); + return (values != null && values.length > 0) ? values[0] : null; + } + + @Override + public Map getParameterMap() { + return parameterMap; + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(parameterMap.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + return parameterMap.get(name); + } + + // 重写文件相关方法,确保文件上传功能正常工作 + @Override + public Collection getParts() throws IOException, ServletException { + return ((HttpServletRequest) getRequest()).getParts(); + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + return ((HttpServletRequest) getRequest()).getPart(name); + } + } + + /** + * 获取系统配置 + * @return boolean + * @author cwchen + * @date 2025/9/28 10:36 + */ + public boolean getSystemConfigStatus(String key) { + boolean SystemConfigStatus = false; + Object cacheObject = redisUtil.getCacheObject(CacheConstants.SYSTEM_CONFIG_VOS); + if(Objects.isNull(cacheObject)){ + List systemConfigVos = configService.listConfig(); + Boolean stored = redisUtil.setNxCacheObject(CacheConstants.SYSTEM_CONFIG_VOS, + JSON.toJSONString(systemConfigVos), + (long) SIGNATURE_EXPIRE_SECONDS, + TimeUnit.SECONDS); + if(CollectionUtils.isNotEmpty(systemConfigVos)){ + SystemConfigVo config = systemConfigVos.stream() + .filter(item -> key.equals(item.getConfigCode())) + .findFirst() + .orElse(null); + if(Objects.nonNull(config)){ + String useStatus = config.getUseStatus(); + if(Objects.equals("0",useStatus)){ + SystemConfigStatus = true; + } + } + } + }else{ + String cacheStr = (String) cacheObject; + List systemConfigVos = JSON.parseArray(cacheStr, SystemConfigVo.class); + if(CollectionUtils.isNotEmpty(systemConfigVos)){ + SystemConfigVo config = systemConfigVos.stream() + .filter(item -> key.equals(item.getConfigCode())) + .findFirst() + .orElse(null); + if(Objects.nonNull(config)){ + String useStatus = config.getUseStatus(); + if(Objects.equals("0",useStatus)){ + SystemConfigStatus = true; + } + } + } + } + return SystemConfigStatus; + } +} \ No newline at end of file diff --git a/bonus-framework/src/main/java/com/bonus/framework/filter/ResponseEncryptFilter.java b/bonus-framework/src/main/java/com/bonus/framework/filter/ResponseEncryptFilter.java new file mode 100644 index 0000000..5883ab9 --- /dev/null +++ b/bonus-framework/src/main/java/com/bonus/framework/filter/ResponseEncryptFilter.java @@ -0,0 +1,176 @@ +package com.bonus.framework.filter; + +import com.alibaba.fastjson2.JSON; +import com.bonus.common.constant.CacheConstants; +import com.bonus.common.core.redis.RedisCache; +import com.bonus.common.utils.encryption.Sm4Utils; +import com.bonus.system.domain.vo.SystemConfigVo; +import com.bonus.system.service.ISystemConfigService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import javax.annotation.Resource; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 对返回的 data数据进行加密 + * + * @author 黑子 + */ +//@Configuration +@Slf4j +@Component(value = "ResponseEncryptFilter") +public class ResponseEncryptFilter implements Filter { + + /** + * 加密标识 + */ + private static final String ENCRYPT_RESPONSE = "encryptResponse"; + + private final RedisCache redisUtil; + + public ResponseEncryptFilter(RedisCache redisUtil) { + this.redisUtil = redisUtil; + } + + @Autowired + private ISystemConfigService configService; + + private static final long TIMESTAMP_TOLERANCE = 15 * 60 * 1000; // 15分钟 + // 请求签名在Redis中的过期时间(秒) + private static final int SIGNATURE_EXPIRE_SECONDS = (int) (TIMESTAMP_TOLERANCE * 2 / 1000); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // 初始化逻辑 + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + // 是否需要相应加密 + boolean isResponseEncrypt = getSystemConfigStatus(CacheConstants.ENCRYPT_RESPONSE); + // 检查请求头中是否包含加密标志 + if (shouldEncrypt(httpRequest) && isResponseEncrypt) { + // 使用响应包装器来缓存响应内容 + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpResponse); + + try { + chain.doFilter(request, responseWrapper); + + // 获取响应状态码 + int status = responseWrapper.getStatus(); + + // 只处理状态码为200 OK的响应 + if (status == HttpServletResponse.SC_OK) { + // 获取响应内容 + byte[] responseBody = responseWrapper.getContentAsByteArray(); + + if (responseBody != null && responseBody.length > 0) { + String responseData = new String(responseBody, StandardCharsets.UTF_8); + + // 加密响应数据 + String encryptedData = Sm4Utils.encrypt(responseData); + byte[] encryptedContent = encryptedData.getBytes(StandardCharsets.UTF_8); + + // 设置加密后的响应 + responseWrapper.resetBuffer(); + responseWrapper.setContentLength(encryptedContent.length); + responseWrapper.getOutputStream().write(encryptedContent); + + // 添加响应头标识 + responseWrapper.setHeader(ENCRYPT_RESPONSE, "true"); + } + } + + } catch (Exception e) { + log.error("响应加密处理时发生错误: {}", e.getMessage(), e); + throw new ServletException("响应处理失败", e); + } finally { + // 确保响应被提交 + responseWrapper.copyBodyToResponse(); + } + } else { + // 不需要加密,直接继续处理链 + chain.doFilter(request, response); + } + } + + @Override + public void destroy() { + // 清理逻辑 + } + + /** + * 判断是否需要对响应数据进行加密 + * + * @param request 请求对象 + * @return 如果需要加密返回true,否则返回false + */ + private boolean shouldEncrypt(HttpServletRequest request) { + String encryptHeader = request.getHeader(ENCRYPT_RESPONSE); + return "true".equalsIgnoreCase(encryptHeader); + } + + /** + * 获取系统配置 + * @return boolean + * @author cwchen + * @date 2025/9/28 10:36 + */ + public boolean getSystemConfigStatus(String key) { + boolean isResponseEncrypt = false; + Object cacheObject = redisUtil.getCacheObject(CacheConstants.SYSTEM_CONFIG_VOS); + if (Objects.isNull(cacheObject)) { + List systemConfigVos = configService.listConfig(); + Boolean stored = redisUtil.setNxCacheObject(CacheConstants.SYSTEM_CONFIG_VOS, + JSON.toJSONString(systemConfigVos), + (long) SIGNATURE_EXPIRE_SECONDS, + TimeUnit.SECONDS); + if (CollectionUtils.isNotEmpty(systemConfigVos)) { + SystemConfigVo config = systemConfigVos.stream() + .filter(item -> key.equals(item.getConfigCode())) + .findFirst() + .orElse(null); + if (Objects.nonNull(config)) { + String useStatus = config.getUseStatus(); + if (Objects.equals("0", useStatus)) { + isResponseEncrypt = true; + } + } + } + } else { + String cacheStr = (String) cacheObject; + List systemConfigVos = JSON.parseArray(cacheStr, SystemConfigVo.class); + if (CollectionUtils.isNotEmpty(systemConfigVos)) { + SystemConfigVo config = systemConfigVos.stream() + .filter(item -> key.equals(item.getConfigCode())) + .findFirst() + .orElse(null); + if (Objects.nonNull(config)) { + String useStatus = config.getUseStatus(); + if (Objects.equals("0", useStatus)) { + isResponseEncrypt = true; + } + } + } + } + return isResponseEncrypt; + } +} + diff --git a/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java b/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java index 1e9875f..cdab677 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java +++ b/bonus-framework/src/main/java/com/bonus/framework/interceptor/ParamSecureHandler.java @@ -35,6 +35,7 @@ public class ParamSecureHandler implements AsyncHandlerInterceptor { ignoreUrlPatterns.add("/smartArchives/getInfo"); ignoreUrlPatterns.add("/smartArchives/getRouters"); ignoreUrlPatterns.add("/smartArchives/session/check"); + ignoreUrlPatterns.add("/smartArchives/sys/config/getConfig"); } private String rnd = null; diff --git a/bonus-framework/src/main/java/com/bonus/framework/security/handle/LogoutSuccessHandlerImpl.java b/bonus-framework/src/main/java/com/bonus/framework/security/handle/LogoutSuccessHandlerImpl.java index 8f69a27..5cd9055 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/security/handle/LogoutSuccessHandlerImpl.java +++ b/bonus-framework/src/main/java/com/bonus/framework/security/handle/LogoutSuccessHandlerImpl.java @@ -5,6 +5,8 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.bonus.common.constant.CacheConstants; +import com.bonus.common.core.redis.RedisCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.Authentication; @@ -31,6 +33,12 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler @Autowired private TokenService tokenService; + private final RedisCache redisUtil; + + public LogoutSuccessHandlerImpl(RedisCache redisUtil) { + this.redisUtil = redisUtil; + } + /** * 退出处理 @@ -50,6 +58,8 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler tokenService.delLoginUser(loginUser.getToken()); // 记录用户退出日志 AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success"))); + // 删除redis缓存数据 + redisUtil.deleteObject(CacheConstants.SYSTEM_CONFIG_VOS); } ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success")))); }