From e5179ac4415d61e31a50bdfd23f28430add23890 Mon Sep 17 00:00:00 2001 From: cwchen <1048842385@qq.com> Date: Thu, 4 Sep 2025 16:59:12 +0800 Subject: [PATCH] =?UTF-8?q?=E9=98=B2=E9=87=8D=E6=94=BE=E6=94=BB=E5=87=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/system/SysLoginController.java | 1 + .../common/core/domain/entity/SysUser.java | 11 + .../bonus/common/core/redis/RedisCache.java | 6 + .../common/filter/RequestCoverFilter.java | 3 +- .../com/bonus/common/utils/GenerateUtil.java | 25 ++ .../bonus/framework/config/FilterConfig.java | 11 + .../framework/filter/ReplayAttackFilter.java | 253 ++++++++++++++++++ .../interceptor/ReadHttpRequestWrapper.java | 2 +- .../resources/mapper/system/SysUserMapper.xml | 3 +- 9 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 bonus-common/src/main/java/com/bonus/common/utils/GenerateUtil.java create mode 100644 bonus-framework/src/main/java/com/bonus/framework/filter/ReplayAttackFilter.java diff --git a/bonus-admin/src/main/java/com/bonus/web/controller/system/SysLoginController.java b/bonus-admin/src/main/java/com/bonus/web/controller/system/SysLoginController.java index c925a80..2486597 100644 --- a/bonus-admin/src/main/java/com/bonus/web/controller/system/SysLoginController.java +++ b/bonus-admin/src/main/java/com/bonus/web/controller/system/SysLoginController.java @@ -84,6 +84,7 @@ public class SysLoginController tokenService.refreshToken(loginUser); } AjaxResult ajax = AjaxResult.success(); + user.setPassword(""); ajax.put("user", user); ajax.put("roles", roles); ajax.put("permissions", permissions); diff --git a/bonus-common/src/main/java/com/bonus/common/core/domain/entity/SysUser.java b/bonus-common/src/main/java/com/bonus/common/core/domain/entity/SysUser.java index d8ce3ba..c5943ce 100644 --- a/bonus-common/src/main/java/com/bonus/common/core/domain/entity/SysUser.java +++ b/bonus-common/src/main/java/com/bonus/common/core/domain/entity/SysUser.java @@ -92,6 +92,9 @@ public class SysUser extends BaseEntity /** 角色ID */ private Long roleId; + /**用户密钥*/ + private String secret; + public SysUser() { @@ -310,6 +313,14 @@ public class SysUser extends BaseEntity this.roleId = roleId; } + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) diff --git a/bonus-common/src/main/java/com/bonus/common/core/redis/RedisCache.java b/bonus-common/src/main/java/com/bonus/common/core/redis/RedisCache.java index 7507869..2be0e2b 100644 --- a/bonus-common/src/main/java/com/bonus/common/core/redis/RedisCache.java +++ b/bonus-common/src/main/java/com/bonus/common/core/redis/RedisCache.java @@ -265,4 +265,10 @@ public class RedisCache { return redisTemplate.keys(pattern); } + + //添加分布式锁 + public Boolean setNxCacheObject(final String key, final T value,long lt,TimeUnit tu) + { + return redisTemplate.opsForValue().setIfAbsent(key,value,lt,tu); + } } 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 39a7a5f..d70dff0 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 @@ -4,6 +4,7 @@ import com.bonus.common.exception.CaptchaException; import com.bonus.common.utils.encryption.Sm3Util; import com.bonus.common.utils.encryption.Sm4Utils; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; @@ -27,7 +28,7 @@ import java.util.*; * @author bonus */ -@Component +@Configuration @Slf4j public class RequestCoverFilter implements Filter { diff --git a/bonus-common/src/main/java/com/bonus/common/utils/GenerateUtil.java b/bonus-common/src/main/java/com/bonus/common/utils/GenerateUtil.java new file mode 100644 index 0000000..e6d1697 --- /dev/null +++ b/bonus-common/src/main/java/com/bonus/common/utils/GenerateUtil.java @@ -0,0 +1,25 @@ +package com.bonus.common.utils; + +import java.security.SecureRandom; +import java.util.Base64; + +/** + * @className:GenerateUtil + * @author:cwchen + * @date:2025-09-04-14:22 + * @version:1.0 + * @description:生成密钥等工具类 + */ +public class GenerateUtil { + + private static String generateRandomSecret(int length) { + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[length]; + random.nextBytes(bytes); + return Base64.getEncoder().encodeToString(bytes); + } + + public static void main(String[] args) { + System.err.println(generateRandomSecret(32));; + } +} 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 d38c09d..ba32ae5 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 @@ -6,6 +6,7 @@ import javax.servlet.DispatcherType; import com.bonus.common.filter.RequestCoverFilter; import com.bonus.common.filter.ResponseEncryptFilter; +import com.bonus.framework.filter.ReplayAttackFilter; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -82,4 +83,14 @@ public class FilterConfig return registration; } + /*@Bean + public FilterRegistrationBean replayAttackFilter() { + FilterRegistrationBean registration = new FilterRegistrationBean<>(); + registration.setFilter(new ReplayAttackFilter()); + registration.addUrlPatterns("/*"); + registration.setOrder(1); + registration.setName("replayAttackFilter"); + return registration; + }*/ + } 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 new file mode 100644 index 0000000..8d11813 --- /dev/null +++ b/bonus-framework/src/main/java/com/bonus/framework/filter/ReplayAttackFilter.java @@ -0,0 +1,253 @@ +package com.bonus.framework.filter; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +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 lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +/** + * @className:ReplayAttackFilter + * @author:cwchen + * @date:2025-09-04-11:19 + * @version:1.0 + * @description:防重放攻击过滤器 + */ +@Configuration +@Slf4j +public class ReplayAttackFilter implements Filter { + static List ignoreUrlPatterns = new ArrayList<>(); + static { + ignoreUrlPatterns.add("/captchaImage"); + ignoreUrlPatterns.add("/login"); + ignoreUrlPatterns.add("/loginOut"); + ignoreUrlPatterns.add("/getInfo"); + ignoreUrlPatterns.add("/getRouters"); + } + + @Autowired + private RedisCache redisUtil; + + @Autowired + private TokenService tokenService; + + // 时间戳允许的误差范围(毫秒) + private static final long TIMESTAMP_TOLERANCE = 5 * 60 * 1000; // 5分钟 + // 请求签名在Redis中的过期时间(秒) + private static final int SIGNATURE_EXPIRE_SECONDS = (int) (TIMESTAMP_TOLERANCE * 2 / 1000); + // Redis键前缀 + private static final String SIGNATURE_KEY_PREFIX = "replay:signature:"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + // 跳过OPTIONS预检请求 + if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { + chain.doFilter(request, response); + return; + } + + String requestURI = httpRequest.getRequestURI(); + boolean shouldIgnore = ignoreUrlPatterns.contains(requestURI); + if (shouldIgnore) { + // 忽略该请求的处理 + System.out.println("忽略请求: " + requestURI); + chain.doFilter(request, response); + return; + } + + // 获取当前登录人 + LoginUser loginUser = tokenService.getLoginUser(httpRequest); + Long userId = Optional.ofNullable(loginUser) + .map(LoginUser::getUserId) + .orElse(null); + // 密钥 + String secret = Optional.ofNullable(loginUser) + .map(LoginUser::getUser) + .map(SysUser::getSecret) + .orElse(null); // 或者提供默认值 + + try { + // 1. 获取必要请求头 + String timestampStr = httpRequest.getHeader("timestamp"); + String signature = httpRequest.getHeader("X-Signature"); + + // 2. 检查必要参数是否存在 + if (timestampStr == null || secret == null || userId == null || signature == null) { + sendErrorResponse(httpResponse, "Missing required headers", HttpServletResponse.SC_BAD_REQUEST); + return; + } + + // 3. 验证时间戳格式和有效性 + long timestamp; + try { + timestamp = Long.parseLong(timestampStr); + } catch (NumberFormatException e) { + sendErrorResponse(httpResponse, "Invalid timestamp format", HttpServletResponse.SC_BAD_REQUEST); + return; + } + + // 4. 检查时间戳是否在允许范围内 + long currentTime = System.currentTimeMillis(); + if (Math.abs(currentTime - timestamp) > TIMESTAMP_TOLERANCE) { + sendErrorResponse(httpResponse, "Timestamp expired or invalid", HttpServletResponse.SC_BAD_REQUEST); + return; + } + + // 5. 获取请求URL + String requestUrl = httpRequest.getRequestURI(); + String queryString = httpRequest.getQueryString(); + if (queryString != null) { + requestUrl += "?" + queryString; + } + + // 6. 生成请求的唯一签名标识 + String requestSignature = generateRequestSignature(String.valueOf(userId), timestamp, requestUrl, httpRequest.getMethod()); + + // 7. 检查签名是否已使用(防重放) + String signatureKey = SIGNATURE_KEY_PREFIX + requestSignature; + if (redisUtil.hasKey(signatureKey)) { + sendErrorResponse(httpResponse, "Request replay detected", HttpServletResponse.SC_BAD_REQUEST); + return; + } + + // 8. 验证请求签名(防止篡改) + if (!validateSignature(httpRequest, String.valueOf(userId),secret, timestamp, requestUrl, signature)) { + sendErrorResponse(httpResponse, "Invalid signature", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + // 9. 将签名存储到Redis(设置过期时间) + Boolean stored = redisUtil.setNxCacheObject(signatureKey, + String.valueOf(currentTime), + (long) SIGNATURE_EXPIRE_SECONDS, + TimeUnit.SECONDS); + + if (stored == null || !stored) { + sendErrorResponse(httpResponse, "Failed to store signature", HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + + // 验证通过,继续处理请求 + chain.doFilter(request, response); + + } catch (Exception e) { + sendErrorResponse(httpResponse, "Server error: " + e.getMessage(), HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + /** + * 生成请求的唯一签名标识(用于Redis键) + */ + private String generateRequestSignature(String userId, long timestamp, String requestUrl, String method) { + String rawString = userId + ":" + timestamp + ":" + method + ":" + requestUrl; + return sha256Hash(rawString); + } + + /** + * 验证请求签名 + */ + private boolean validateSignature(HttpServletRequest request, String userId,String secret, + long timestamp, String requestUrl, String receivedSignature) { + String encryptUserId = Sm4Utils.encrypt(userId); + String encryptSecret = Sm4Utils.encrypt(secret); + + // 构建待签名字符串 + String signString = buildSignString(encryptUserId, timestamp, requestUrl, request.getMethod()); + + // 使用HMAC-SHA256计算签名 + String calculatedSignature = calculateHMAC(signString, encryptSecret); + + return calculatedSignature.equals(receivedSignature); + } + + /** + * 构建待签名字符串 + */ + private String buildSignString(String userId, long timestamp, String requestUrl, String method) { + StringBuilder sb = new StringBuilder(); + sb.append(userId) + .append(timestamp) + .append(method.toUpperCase()) + .append(requestUrl); + + return sb.toString(); + } + + + /** + * 计算HMAC-SHA256签名 + */ + private String calculateHMAC(String data, String key) { + try { + javax.crypto.Mac mac = javax.crypto.Mac.getInstance("HmacSHA256"); + javax.crypto.spec.SecretKeySpec secretKey = + new javax.crypto.spec.SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + mac.init(secretKey); + byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hash); + } catch (Exception e) { + throw new RuntimeException("Failed to calculate HMAC", e); + } + } + + /** + * SHA256哈希 + */ + private String sha256Hash(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(hash); + } catch (Exception e) { + throw new RuntimeException("Failed to calculate SHA256", e); + } + } + + private String bytesToHex(byte[] bytes) { + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + + private void sendErrorResponse(HttpServletResponse response, String message, int status) + throws IOException { + response.setStatus(status); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + response.getWriter().write(String.format( + "{\"error\": \"%s\", \"code\": %d, \"timestamp\": %d}", + message, status, System.currentTimeMillis() + )); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + System.out.println("ReplayAttackFilter initialized"); + } + + @Override + public void destroy() { + System.out.println("ReplayAttackFilter destroyed"); + } +} \ No newline at end of file diff --git a/bonus-framework/src/main/java/com/bonus/framework/interceptor/ReadHttpRequestWrapper.java b/bonus-framework/src/main/java/com/bonus/framework/interceptor/ReadHttpRequestWrapper.java index e2b24e7..665478e 100644 --- a/bonus-framework/src/main/java/com/bonus/framework/interceptor/ReadHttpRequestWrapper.java +++ b/bonus-framework/src/main/java/com/bonus/framework/interceptor/ReadHttpRequestWrapper.java @@ -12,7 +12,7 @@ import java.util.Map; /** * 包装和增强原始的 HttpServletRequest 对象,允许多次读取请求体(body)的数据,特别是在处理请求时使用流读取数据的情况下 - * @author 黑子 + * @author */ public class ReadHttpRequestWrapper extends HttpServletRequestWrapper { diff --git a/bonus-system/src/main/resources/mapper/system/SysUserMapper.xml b/bonus-system/src/main/resources/mapper/system/SysUserMapper.xml index 876b065..4495929 100644 --- a/bonus-system/src/main/resources/mapper/system/SysUserMapper.xml +++ b/bonus-system/src/main/resources/mapper/system/SysUserMapper.xml @@ -24,6 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -50,7 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.pwd_update_date, u.create_by, u.create_time, u.remark, d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status, - r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status + r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status,u.secret from da_ky_sys_user u left join da_ky_sys_dept d on u.dept_id = d.dept_id left join da_ky_sys_user_role ur on u.user_id = ur.user_id