防重放攻击
This commit is contained in:
parent
6f67f1b6e9
commit
e5179ac441
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -265,4 +265,10 @@ public class RedisCache
|
|||
{
|
||||
return redisTemplate.keys(pattern);
|
||||
}
|
||||
|
||||
//添加分布式锁
|
||||
public <T> Boolean setNxCacheObject(final String key, final T value,long lt,TimeUnit tu)
|
||||
{
|
||||
return redisTemplate.opsForValue().setIfAbsent(key,value,lt,tu);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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));;
|
||||
}
|
||||
}
|
||||
|
|
@ -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> replayAttackFilter() {
|
||||
FilterRegistrationBean<ReplayAttackFilter> registration = new FilterRegistrationBean<>();
|
||||
registration.setFilter(new ReplayAttackFilter());
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setOrder(1);
|
||||
registration.setName("replayAttackFilter");
|
||||
return registration;
|
||||
}*/
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import java.util.Map;
|
|||
|
||||
/**
|
||||
* 包装和增强原始的 HttpServletRequest 对象,允许多次读取请求体(body)的数据,特别是在处理请求时使用流读取数据的情况下
|
||||
* @author 黑子
|
||||
* @author
|
||||
*/
|
||||
public class ReadHttpRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<result property="updateBy" column="update_by" />
|
||||
<result property="updateTime" column="update_time" />
|
||||
<result property="remark" column="remark" />
|
||||
<result property="secret" column="secret" />
|
||||
<association property="dept" javaType="SysDept" resultMap="deptResult" />
|
||||
<collection property="roles" javaType="java.util.List" resultMap="RoleResult" />
|
||||
</resultMap>
|
||||
|
|
@ -50,7 +51,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||
<sql id="selectUserVo">
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue