加解密配置

This commit is contained in:
cwchen 2025-09-28 13:22:31 +08:00
parent cf9bc4d8c2
commit a5a948034a
11 changed files with 1009 additions and 49 deletions

View File

@ -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";
}

View File

@ -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<String, String[]> cachedParameterMap;
@ -336,9 +359,11 @@ public class RequestCoverFilter implements Filter {
return getParameterMap().get(name);
}
/**
*/
/**
* 解析查询字符串支持普通格式和嵌套格式
*/
*//*
private Map<String, String[]> parseQueryString(String queryString) {
Map<String, String[]> 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<String, String[]> 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<String, String[]> parseSimpleQueryString(String queryString) {
Map<String, String[]> 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<String, String[]> 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);
}
}
}
}*/

View File

@ -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);
}
}
*/

View File

@ -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<RequestCoverFilter> requestCoverFilterRegistration() {
FilterRegistrationBean<RequestCoverFilter> 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<ResponseEncryptFilter> responseEncryptFilterRegistration() {
FilterRegistrationBean<ResponseEncryptFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new ResponseEncryptFilter());
registration.setFilter(responseEncryptFilter);
registration.addUrlPatterns("/*");
registration.setOrder(-5); // 设置过滤器顺序
registration.setName("responseEncryptFilter");

View File

@ -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()

View File

@ -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<String, CacheEntry> 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<SystemConfigVo> 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");

View File

@ -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<String> 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<SystemConfigVo> 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<SystemConfigVo> 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);

View File

@ -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<String, String[]> 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<String, String[]> getParameterMap() {
if (cachedParameterMap == null) {
cachedParameterMap = parseQueryString(queryString);
}
return cachedParameterMap;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(getParameterMap().keySet());
}
@Override
public String[] getParameterValues(String name) {
return getParameterMap().get(name);
}
/**
* 解析查询字符串支持普通格式和嵌套格式
*/
private Map<String, String[]> parseQueryString(String queryString) {
Map<String, String[]> 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<String, String[]> 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<String, String[]> parseSimpleQueryString(String queryString) {
Map<String, String[]> 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<String, String[]> 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<String, String[]> getParameterMap() {
return parameterMap;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(parameterMap.keySet());
}
@Override
public String[] getParameterValues(String name) {
return parameterMap.get(name);
}
// 重写文件相关方法确保文件上传功能正常工作
@Override
public Collection<Part> 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<SystemConfigVo> 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<SystemConfigVo> 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;
}
}

View File

@ -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<SystemConfigVo> 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<SystemConfigVo> 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;
}
}

View File

@ -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;

View File

@ -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"))));
}