diff --git a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/config/replay/AntiReplayProperties.java b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/config/replay/AntiReplayProperties.java new file mode 100644 index 0000000..be2156f --- /dev/null +++ b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/config/replay/AntiReplayProperties.java @@ -0,0 +1,34 @@ +package cn.zyjblogs.config.replay; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 防重放攻击 + * + * @author zhuyijun + */ +@Data +@ConfigurationProperties(prefix = "zyjblogs.anti.replay") +public class AntiReplayProperties { + public static final String REDIS_PREFIX = "anti:replay:"; + /** + * 是否启用防重放验证 + */ + private Boolean enabled = true; + + + /** + * 请求ID 防止重放 + */ + private String nonce = "requestId"; + /** + * 请求时间 避免缓存时间过后重放 + */ + private String timestamp = "timestamp"; + + private String sign = "sign"; + + private Long expireTime = 120L; + +} diff --git a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/exception/AntiReplayException.java b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/exception/AntiReplayException.java new file mode 100644 index 0000000..6f489ac --- /dev/null +++ b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/exception/AntiReplayException.java @@ -0,0 +1,16 @@ +package cn.zyjblogs.exception; + +import cn.zyjblogs.starter.common.exception.AbstractFrameworkException; + +/** + * @author zhuyijun + */ +public class AntiReplayException extends AbstractFrameworkException { + public AntiReplayException(String message) { + super(message); + } + + public AntiReplayException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/AuthFilter.java b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/AuthFilter.java index da6a1c3..40a890c 100644 --- a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/AuthFilter.java +++ b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/AuthFilter.java @@ -1,5 +1,8 @@ package cn.zyjblogs.filter; +import cn.zyjblogs.config.replay.AntiReplayProperties; +import cn.zyjblogs.crypto.sm3.SM3; +import cn.zyjblogs.exception.AntiReplayException; import cn.zyjblogs.starter.common.autoconfigure.rsa.RsaKeyProperties; import cn.zyjblogs.starter.common.entity.constant.CommonRedisKeyConstant; import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant; @@ -11,6 +14,7 @@ import cn.zyjblogs.starter.common.entity.response.ResponseObject; import cn.zyjblogs.starter.common.entity.response.ResponseResult; import cn.zyjblogs.starter.common.utils.jwt.JwtParsers; import cn.zyjblogs.starter.common.utils.rsa.RsaUtils; +import cn.zyjblogs.starter.common.utils.string.StringUtils; import cn.zyjblogs.starter.redis.utils.RedisTemplateHandler; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +23,7 @@ import io.jsonwebtoken.ExpiredJwtException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; @@ -30,15 +35,15 @@ import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.util.CollectionUtils; -import cn.zyjblogs.starter.common.utils.string.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import javax.validation.constraints.NotNull; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.PublicKey; import java.util.List; -import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * Copyright (C), 2021, 北京同创永益科技发展有限公司 @@ -50,13 +55,14 @@ import java.util.Objects; */ @Component @Slf4j -@EnableConfigurationProperties(WhiteListProperties.class) +@EnableConfigurationProperties({WhiteListProperties.class, AntiReplayProperties.class}) @RequiredArgsConstructor public class AuthFilter implements GlobalFilter { private final WhiteListProperties whiteListProperties; private AntPathMatcher antPathMatcher = new AntPathMatcher(); private final RsaKeyProperties rsaKeyProperties; - private final RedisTemplateHandler redisTemplateHandler; + private final RedisTemplateHandler redisTemplateHandler; + private final AntiReplayProperties antiReplayProperties; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { @@ -71,7 +77,8 @@ public class AuthFilter implements GlobalFilter { String path = request.getURI().getPath(); //白名单请求直接放行 if (isWhileList(path)) { - return chain.filter(exchange); + //重放攻击防护 + return antiReplayFilter(exchange, chain); } if (token == null || StringUtils.isEmpty(token)) { return getErrorMono(response, HttpCode.UNAUTHORIZED, "无访问权限"); @@ -83,10 +90,60 @@ public class AuthFilter implements GlobalFilter { log.info("token过期"); return getErrorMono(response, HttpCode.UNAUTHORIZED, "token失效"); } - if ("/user/login".equals(path)) { + //重放攻击防护 + return antiReplayFilter(build, chain); + } + + public Mono antiReplayFilter(ServerWebExchange exchange, GatewayFilterChain chain) { + if (Boolean.FALSE.equals(antiReplayProperties.getEnabled())) { return chain.filter(exchange); } - return chain.filter(build); + ServerHttpRequest request = exchange.getRequest(); + HttpHeaders headers = request.getHeaders(); + String nonce = headers.getFirst(antiReplayProperties.getNonce()); + String timestamp = headers.getFirst(antiReplayProperties.getTimestamp()); + String sign = headers.getFirst(antiReplayProperties.getSign()); + if (StringUtils.isEmpty(nonce) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(sign)) { + return getErrorMono(exchange.getResponse(), HttpCode.BAD_REQUEST, "请求头参数错误!"); + } + try { + validateNonceAndTimestamp(nonce, timestamp, sign); + } catch (Exception e) { + return getErrorMono(exchange.getResponse(), HttpCode.BAD_REQUEST, e.getMessage()); + } + return chain.filter(exchange); + } + + + private void validateNonceAndTimestamp(String nonce, String timestamp, String sign) { + // 判断Nonce和Timestamp是否为空 + if (nonce == null || timestamp == null) { + throw new AntiReplayException("请求头参数错误"); + } + Boolean flag = redisTemplateHandler.hasKey(AntiReplayProperties.REDIS_PREFIX + nonce); + // 验证Nonce是否已经使用过 + if (Boolean.TRUE.equals(flag)) { + throw new AntiReplayException("请重复请求!"); + } + redisTemplateHandler.set(AntiReplayProperties.REDIS_PREFIX + nonce, timestamp); + redisTemplateHandler.expire(AntiReplayProperties.REDIS_PREFIX + nonce, antiReplayProperties.getExpireTime(), TimeUnit.SECONDS); + long l = System.currentTimeMillis(); + log.info("{}", l); + // 验证Timestamp是否在合理时间范围内 + long timeStampValue; + try { + timeStampValue = Long.parseLong(timestamp); + } catch (NumberFormatException e) { + throw new AntiReplayException(antiReplayProperties.getTimestamp() + "参数错误!"); + } + if (Math.abs(timeStampValue - l) / 1000 > antiReplayProperties.getExpireTime()) { + throw new AntiReplayException(antiReplayProperties.getTimestamp() + "请求过期!"); + } + System.out.println("sign: " + SM3.digest(nonce + timestamp)); + if (!SM3.verify(nonce + timestamp, sign)) { + throw new AntiReplayException("请求头校验不通过"); + } + // 请求传过来的时间戳与服务器当前时间戳差值大于120,则当前请求的timestamp无效 } /** @@ -155,6 +212,11 @@ public class AuthFilter implements GlobalFilter { private Mono getErrorMono(ServerHttpResponse response, HttpCode responseCode, String msg) { response.getHeaders().setContentType(MediaType.APPLICATION_JSON); response.setStatusCode(org.springframework.http.HttpStatus.OK); + return getVoidMono(response, responseCode, msg, log); + } + + @NotNull + static Mono getVoidMono(ServerHttpResponse response, HttpCode responseCode, String msg, Logger log) { ResponseObject responseObject = ResponseResult.error(responseCode, msg); ObjectMapper mapper = new ObjectMapper(); byte[] bytes = new byte[0]; @@ -165,6 +227,5 @@ public class AuthFilter implements GlobalFilter { } DataBuffer wrap = response.bufferFactory().wrap(bytes); return response.writeWith(Mono.just(wrap)); - } } diff --git a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/CORSConfig.java b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/CORSConfig.java index 0bc8219..143da27 100644 --- a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/CORSConfig.java +++ b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/CORSConfig.java @@ -21,7 +21,7 @@ public class CORSConfig { config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.setAllowCredentials(Boolean.TRUE); - config.addExposedHeader("Authorization"); +// config.addExposedHeader("Authorization"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); diff --git a/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/SignatureValidator.java b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/SignatureValidator.java new file mode 100644 index 0000000..3f02c59 --- /dev/null +++ b/server/zyjblogs-gateway/src/main/java/cn/zyjblogs/filter/SignatureValidator.java @@ -0,0 +1,101 @@ +package cn.zyjblogs.filter; + +import cn.zyjblogs.config.replay.AntiReplayProperties; +import cn.zyjblogs.crypto.sm3.SM3; +import cn.zyjblogs.starter.common.utils.string.StringUtils; +import com.alibaba.nacos.common.utils.ConvertUtils; +import lombok.extern.log4j.Log4j2; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; + +import java.net.URI; +import java.util.Map; + +/** + * @author zhuyijun + */ +@Log4j2 +public class SignatureValidator { + + public static SignatureWorker builder() { + return new SignatureWorker(); + } + + public static class SignatureWorker { + + /** + * 请求标识 + */ + private String nonce; + /** + * 请求时间 + */ + private Long timestamp; + + private String url; + /** + * 请求参数 + */ + private Map params; + /** + * 请求的签名 + */ + private String sign; + + + public SignatureWorker nonce(String nonce) { + this.nonce = nonce; + return this; + } + + public SignatureWorker timestamp(Long timestamp) { + this.timestamp = timestamp; + return this; + } + + public SignatureWorker params(Map parameterMap) { + this.params = parameterMap; + return this; + } + + public SignatureWorker sign(String sign) { + this.sign = sign; + return this; + } + + + public SignatureWorker url(URI uri) { + StringBuilder path = new StringBuilder(uri.getPath()); + String query = uri.getQuery(); + if(StringUtils.isNotEmpty(query)){ + path.append("?").append(query); + } + this.url = path.toString(); + return this; + } + + public SignatureWorker data(AntiReplayProperties antiReplayProperties, ServerHttpRequest request) { + HttpHeaders headers = request.getHeaders(); + String nonce = headers.getFirst(antiReplayProperties.getNonce()); + String timestamp = headers.getFirst(antiReplayProperties.getTimestamp()); + String sign = headers.getFirst(antiReplayProperties.getSign()); + return this.nonce(nonce) + .timestamp(ConvertUtils.toLong(timestamp)) + .url(request.getURI()) + .sign(sign); + } + + + public void execute() { + StringBuilder stringBuilder = new StringBuilder(); + String digest = stringBuilder.append(this.nonce).append(this.timestamp).append(this.url).append(this.params).toString(); + if (!SM3.verify(digest,this.sign)) { + if (log.isDebugEnabled()) { + log.debug("数据签名验证未通过, 传入签名:[ {} ], 生成签名:[ {} ]", sign, digest); + } + throw new IllegalArgumentException("数据签名验证未通过"); + } + } + + } +} diff --git a/server/zyjblogs-gateway/src/main/resources/bootstrap-test.yml b/server/zyjblogs-gateway/src/main/resources/bootstrap-test.yml index bb3759c..d0e884a 100644 --- a/server/zyjblogs-gateway/src/main/resources/bootstrap-test.yml +++ b/server/zyjblogs-gateway/src/main/resources/bootstrap-test.yml @@ -1,8 +1,7 @@ zyjblogs: config: nacos: - host: ${HATECH_CONFIG_NACOS_HOST:127.0.0.1} - port: ${HATECH_CONFIG_NACOS_PORT:8848} - username: ${HATECH_CONFIG_NACOS_USERNAME:nacos} - password: ${HATECH_CONFIG_NACOS_PASSWORD:nacos} - + host: ${ZYJBLOGS_CONFIG_NACOS_HOST:zyjblogs.cn} + port: ${ZYJBLOGS_CONFIG_NACOS_PORT:9999} + username: ${ZYJBLOGS_CONFIG_NACOS_USERNAME:nacos} + password: ${ZYJBLOGS_CONFIG_NACOS_PASSWORD:1317453947ju} \ No newline at end of file diff --git a/server/zyjblogs-oauth/src/main/resources/bootstrap-test.yml b/server/zyjblogs-oauth/src/main/resources/bootstrap-test.yml index ca0d427..d0e884a 100644 --- a/server/zyjblogs-oauth/src/main/resources/bootstrap-test.yml +++ b/server/zyjblogs-oauth/src/main/resources/bootstrap-test.yml @@ -1,8 +1,7 @@ zyjblogs: config: nacos: - host: ${ZYJBLOGS_CONFIG_NACOS_HOST:127.0.0.1} - port: ${ZYJBLOGS_CONFIG_NACOS_PORT:8848} + host: ${ZYJBLOGS_CONFIG_NACOS_HOST:zyjblogs.cn} + port: ${ZYJBLOGS_CONFIG_NACOS_PORT:9999} username: ${ZYJBLOGS_CONFIG_NACOS_USERNAME:nacos} - password: ${ZYJBLOGS_CONFIG_NACOS_PASSWORD:nacos} - + password: ${ZYJBLOGS_CONFIG_NACOS_PASSWORD:1317453947ju} \ No newline at end of file diff --git a/starter/zyjblogs-mybatisplus-spring-boot-starter/src/main/java/cn/zyjblogs/starter/mybatisplus/utils/IdUtils.java b/starter/zyjblogs-mybatisplus-spring-boot-starter/src/main/java/cn/zyjblogs/starter/mybatisplus/utils/IdUtils.java index 12290d9..a803b4b 100644 --- a/starter/zyjblogs-mybatisplus-spring-boot-starter/src/main/java/cn/zyjblogs/starter/mybatisplus/utils/IdUtils.java +++ b/starter/zyjblogs-mybatisplus-spring-boot-starter/src/main/java/cn/zyjblogs/starter/mybatisplus/utils/IdUtils.java @@ -38,4 +38,8 @@ public class IdUtils { public static String nextUUID(Object entity) { return IdWorker.get32UUID(); } + + public static String nextUUID() { + return IdWorker.get32UUID(); + } }