Jelajahi Sumber

完成api参数签名功能

wangzaijun 1 tahun lalu
induk
melakukan
e88ff9e68e
20 mengubah file dengan 206 tambahan dan 54 penghapusan
  1. 5 0
      pom.xml
  2. 1 1
      readme.md
  3. 15 11
      src/main/java/com/smppw/analysis/application/dto/BaseReq.java
  4. 1 1
      src/main/java/com/smppw/analysis/application/dto/style/BaseStyleReq.java
  5. 1 1
      src/main/java/com/smppw/analysis/domain/dao/rank/RankToBDao.java
  6. 1 1
      src/main/java/com/smppw/analysis/domain/dao/rank/RankToCDao.java
  7. 1 0
      src/main/java/com/smppw/analysis/domain/dto/info/FundSimilarParams.java
  8. 1 1
      src/main/java/com/smppw/analysis/domain/dto/info/PubliclyCurrencyFundHeadInfoVO.java
  9. 1 0
      src/main/java/com/smppw/analysis/domain/dto/performance/RankParams.java
  10. 1 1
      src/main/java/com/smppw/analysis/domain/manager/info/AbstractFundHeadInfo.java
  11. 16 11
      src/main/java/com/smppw/analysis/domain/manager/info/handler/PubliclyFundHeadInfo.java
  12. 1 1
      src/main/java/com/smppw/analysis/domain/manager/performance/handler/RankHandler.java
  13. 2 2
      src/main/java/com/smppw/analysis/domain/service/BaseInfoService.java
  14. 9 7
      src/main/java/com/smppw/analysis/domain/service/impl/BaseInfoServiceImpl.java
  15. 82 2
      src/main/java/com/smppw/analysis/infrastructure/components/ApiSignInterceptor.java
  16. 23 0
      src/main/java/com/smppw/analysis/infrastructure/config/AnalysisConfig.java
  17. 26 8
      src/main/java/com/smppw/analysis/infrastructure/config/AnalysisProperty.java
  18. 6 2
      src/main/resources/application-tob.yaml
  19. 6 2
      src/main/resources/application-toc.yaml
  20. 7 2
      src/main/resources/application.yaml

+ 5 - 0
pom.xml

@@ -64,6 +64,11 @@
             <version>${hutool-version}</version>
         </dependency>
         <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-crypto</artifactId>
+            <version>${hutool-version}</version>
+        </dependency>
+        <dependency>
             <groupId>com.github.ben-manes.caffeine</groupId>
             <artifactId>caffeine</artifactId>
             <version>2.8.8</version>

+ 1 - 1
readme.md

@@ -88,7 +88,7 @@ springboot3+redis+mybatis的一个标的详情页通用服务
 - [ ] 2、所有标的基本信息获取逻辑重构
 - [x] 3、统一返回和异常处理
 - [x] 4、完善的日志记录
-- [ ] 5、接口参数签名校验
+- [x] 5、接口参数签名校验
 - [ ] 6、接口文档
 - [ ] 7、风格评测接口重构
 

+ 15 - 11
src/main/java/com/smppw/analysis/application/dto/BaseReq.java

@@ -6,23 +6,27 @@ import lombok.Setter;
 /**
  * @author wangzaijun
  * @date 2023/8/7 16:24
- * @description 基本请求参数,用来做接口签名验证的,验证通过后把这些参数洗掉
+ * @description 基本请求参数
  */
 @Setter
 @Getter
 public abstract class BaseReq<R> {
     /**
-     * 参数签名,防止参数被篡改
+     * 授权实体,应用id
      */
-    private String sign;
-    /**
-     * 随机字符串,防止重放攻击
-     */
-    private String nonce;
-    /**
-     * 时间戳,防止重放攻击
-     */
-    private long timestamp;
+    private String appKey;
+//    /**
+//     * 参数签名,防止参数被篡改
+//     */
+//    private String sign;
+//    /**
+//     * 随机字符串,防止重放攻击
+//     */
+//    private String nonce;
+//    /**
+//     * 时间戳,防止重放攻击
+//     */
+//    private long timestamp;
 
     public abstract R convert();
 }

+ 1 - 1
src/main/java/com/smppw/analysis/application/dto/style/BaseStyleReq.java

@@ -9,7 +9,7 @@ import lombok.Setter;
 
 @Setter
 @Getter
-public abstract class BaseStyleReq<R extends BaseParams> extends BaseReq {
+public abstract class BaseStyleReq<R extends BaseParams> extends BaseReq<R> {
     /**
      * 标的id,包括基金、机构行业经理
      */

+ 1 - 1
src/main/java/com/smppw/analysis/domain/dao/rank/RankToBDao.java

@@ -3,7 +3,7 @@ package com.smppw.analysis.domain.dao.rank;
 import com.smppw.analysis.domain.mapper.core.RankToBMapper;
 import org.springframework.stereotype.Component;
 
-@Component("tob")
+@Component("ppw-tob")
 public class RankToBDao extends AbstractRankDao {
     private final RankToBMapper mapper;
 

+ 1 - 1
src/main/java/com/smppw/analysis/domain/dao/rank/RankToCDao.java

@@ -3,7 +3,7 @@ package com.smppw.analysis.domain.dao.rank;
 import com.smppw.analysis.domain.mapper.core.RankToCMapper;
 import org.springframework.stereotype.Component;
 
-@Component("toc")
+@Component("ppw-toc")
 public class RankToCDao extends AbstractRankDao {
     private final RankToCMapper mapper;
 

+ 1 - 0
src/main/java/com/smppw/analysis/domain/dto/info/FundSimilarParams.java

@@ -13,6 +13,7 @@ import lombok.Setter;
 @Getter
 public class FundSimilarParams {
     public static final String DEFAULT_THRESHOLD = "0.85";
+    private String appKey;
     /**
      * 基金id
      */

+ 1 - 1
src/main/java/com/smppw/analysis/domain/dto/info/PubliclyCurrencyFundHeadInfoVO.java

@@ -16,7 +16,7 @@ public class PubliclyCurrencyFundHeadInfoVO extends PubliclyFundHeadInfoVO {
     /**
      * 是否货币型公募基金
      */
-    private Integer isCurrency;
+    private Integer isCurrency = 0;
     /**
      * 7日年化收益
      */

+ 1 - 0
src/main/java/com/smppw/analysis/domain/dto/performance/RankParams.java

@@ -12,6 +12,7 @@ import lombok.Setter;
 @Setter
 @Getter
 public class RankParams extends BaseParams {
+    private String appKey;
     /**
      * 选择的指标
      */

+ 1 - 1
src/main/java/com/smppw/analysis/domain/manager/info/AbstractFundHeadInfo.java

@@ -157,7 +157,7 @@ public abstract class AbstractFundHeadInfo<R extends FundHeadInfoVO> extends Abs
      */
     private void handleStrategySummaryName(R info) {
         Integer thirdStrategyId = info.getThirdStrategyId();
-        if (!Objects.equals(-1, thirdStrategyId)) {
+        if (thirdStrategyId == null || Objects.equals(-1, thirdStrategyId)) {
             info.setThirdStrategyId(null);
             if (StrUtil.isBlank(info.getSubstrategy())) {
                 info.setStrategySummaryName(info.getStrategy());

+ 16 - 11
src/main/java/com/smppw/analysis/domain/manager/info/handler/PubliclyFundHeadInfo.java

@@ -28,16 +28,8 @@ public class PubliclyFundHeadInfo extends AbstractFundHeadInfo<PubliclyFundHeadI
 
     @Override
     public PubliclyFundHeadInfoVO handleExtInfo(FundInfoDO fundInfo, FundHeadInfoVO headInfo, Map<String, String> secNameMap) {
-        PubliclyCurrencyFundHeadInfoVO result = BeanUtil.copyProperties(headInfo, PubliclyCurrencyFundHeadInfoVO.class);
-        if (fundInfo.getStrategy() != null) {
-            result.setStrategy(secNameMap.get(fundInfo.getStrategy().toString()));
-            result.setStrategyId(fundInfo.getStrategy());
-        }
-        if (fundInfo.getSubstrategy() != null) {
-            result.setSubstrategy(secNameMap.get(fundInfo.getSubstrategy().toString()));
-            result.setSubstrategyId(fundInfo.getSubstrategy());
-        }
         if (Objects.equals(Strategy.Cash.getStrategyId(), fundInfo.getStrategy())) {
+            PubliclyCurrencyFundHeadInfoVO result = BeanUtil.copyProperties(headInfo, PubliclyCurrencyFundHeadInfoVO.class);
             result.setIsCurrency(1);
             List<MonetaryFundProfitDO> tempList = this.fundInformationDao.queryMonetaryFund(fundInfo.getRefId());
             if (CollectionUtil.isNotEmpty(tempList)) {
@@ -52,9 +44,22 @@ public class PubliclyFundHeadInfo extends AbstractFundHeadInfo<PubliclyFundHeadI
                 }
                 result.setRetPerUnit(monetaryFundProfitDO.getProfitPerMillion());
             }
-        } else {
-            result.setIsCurrency(0);
+            this.handleStrategy(fundInfo, result, secNameMap);
+            return result;
         }
+        PubliclyFundHeadInfoVO result = BeanUtil.copyProperties(headInfo, PubliclyFundHeadInfoVO.class);
+        this.handleStrategy(fundInfo, result, secNameMap);
         return result;
     }
+
+    private void handleStrategy(FundInfoDO fundInfo, PubliclyFundHeadInfoVO result, Map<String, String> secNameMap) {
+        if (fundInfo.getStrategy() != null) {
+            result.setStrategy(secNameMap.get(fundInfo.getStrategy().toString()));
+            result.setStrategyId(fundInfo.getStrategy());
+        }
+        if (fundInfo.getSubstrategy() != null) {
+            result.setSubstrategy(secNameMap.get(fundInfo.getSubstrategy().toString()));
+            result.setSubstrategyId(fundInfo.getSubstrategy());
+        }
+    }
 }

+ 1 - 1
src/main/java/com/smppw/analysis/domain/manager/performance/handler/RankHandler.java

@@ -33,7 +33,7 @@ public class RankHandler extends AbstractPerformance<RankParams, Map<String, Obj
         List<String> indexIds = params.getSecIds();
         CollUtil.addAllIfNotContains(secIds, indexIds);
         IStrategy strategy = StrategyHandleUtils.getStrategy(params.getStrategy());
-        String rankDate = this.baseInfoService.getLatestRankRat();
+        String rankDate = this.baseInfoService.getLatestRankRat(params.getAppKey());
         List<Map<String, Object>> tempList = this.baseInfoService.getFundRank(rankDate, fundId, indexIds, params.getIndicator());
         Map<String, String> mapper = this.baseInfoService.querySecName(secIds);
         Map<String, String> productMapper = MapUtil.<String, String>builder(MapUtil.newHashMap(true)).putAll(mapper)

+ 2 - 2
src/main/java/com/smppw/analysis/domain/service/BaseInfoService.java

@@ -27,7 +27,7 @@ public interface BaseInfoService {
      *
      * @return /
      */
-    String getLatestRankRat();
+    String getLatestRankRat(String appKey);
 
     /**
      * 获取标的排名期,与指标排名比较,取最小的排名期
@@ -35,7 +35,7 @@ public interface BaseInfoService {
      * @param refId /
      * @return /
      */
-    String getLatestRankRat(String refId);
+    String getLatestRankRat(String appKey, String refId);
 
     /**
      * 获取标的类型

+ 9 - 7
src/main/java/com/smppw/analysis/domain/service/impl/BaseInfoServiceImpl.java

@@ -60,7 +60,7 @@ public class BaseInfoServiceImpl implements BaseInfoService, ApplicationContextA
     private static final Map<String, Boolean> INDEX_EXIST = MapUtil.newConcurrentHashMap();
     private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
-    private final RankDao rankDao;
+    private final RankFactory rankFactory;
     private final CacheGateway<Object> cacheGateway;
     private final IndexesProfileDao indexesProfileDao;
     private final RongzhiIndexNavDao rongzhiIndexNavDao;
@@ -84,7 +84,7 @@ public class BaseInfoServiceImpl implements BaseInfoService, ApplicationContextA
                                FundInformationDao fundInformationDao, FundAnnounceDao fundAnnounceDao,
                                FundArchivesDao fundArchivesDao, MfChargeRateMapper mfChargeRateMapper,
                                PersonnelInformationMapper personnelInformationMapper, FundAssetSizeMapper fundAssetSizeMapper) {
-        this.rankDao = rankFactory.getInstance(property.getDataSource());
+        this.rankFactory = rankFactory;
         this.cacheGateway = factory.getCacheGateway(property.getCacheType());
         this.indexesProfileDao = indexesProfileDao;
         this.rongzhiIndexNavDao = rongzhiIndexNavDao;
@@ -108,13 +108,15 @@ public class BaseInfoServiceImpl implements BaseInfoService, ApplicationContextA
     }
 
     @Override
-    public String getLatestRankRat() {
-        return this.rankDao.getRankDate();
+    public String getLatestRankRat(String appKey) {
+        RankDao instance = rankFactory.getInstance(appKey);
+        return instance.getRankDate();
     }
 
     @Override
-    public String getLatestRankRat(String refId) {
-        return this.rankDao.getRankDate(refId);
+    public String getLatestRankRat(String appKey, String refId) {
+        RankDao instance = rankFactory.getInstance(appKey);
+        return instance.getRankDate(refId);
     }
 
     @Override
@@ -280,7 +282,7 @@ public class BaseInfoServiceImpl implements BaseInfoService, ApplicationContextA
             params.setThreshold(FundSimilarParams.DEFAULT_THRESHOLD);
         }
         IStrategy strategy = StrategyHandleUtils.getStrategy(params.getStrategy());
-        String rankDate = this.getLatestRankRat();
+        String rankDate = this.getLatestRankRat(params.getAppKey());
         List<FundSimilarDo> tempList = ListUtil.list(true);
         if (params.getCalcType() == 2) {
             Map<String, Object> req = MapUtil.<String, Object>builder().put("strategy", strategy.getStrategyId()).put("startDate", params.getStartDate())

+ 82 - 2
src/main/java/com/smppw/analysis/infrastructure/components/ApiSignInterceptor.java

@@ -1,18 +1,98 @@
 package com.smppw.analysis.infrastructure.components;
 
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SignUtil;
+import cn.hutool.json.JSONUtil;
+import com.smppw.analysis.infrastructure.config.AnalysisProperty;
+import com.smppw.common.pojo.ResultVo;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.lang.NonNull;
 import org.springframework.web.servlet.HandlerInterceptor;
 
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.Map;
+
 /**
  * @author wangzaijun
  * @date 2023/8/7 15:58
  * @description api验证签名拦截器
  */
 public class ApiSignInterceptor implements HandlerInterceptor {
+    private static final long MAX_TIMEOUT = 50000L;
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    private final AnalysisProperty property;
+
+    public ApiSignInterceptor(AnalysisProperty property) {
+        this.property = property;
+    }
+
     @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
-        // todo 参数签名验证
+    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
+        if (logger.isInfoEnabled()) {
+            logger.info(StrUtil.format("{} 接口正在被 {} 请求!", request.getRequestURI(), request.getRemoteAddr()));
+        }
+        if (!this.property.getEnableSign()) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("所有接口不需要签名验证!");
+            }
+            return HandlerInterceptor.super.preHandle(request, response, handler);
+        }
+        String sign = request.getHeader("sign");
+        if (StrUtil.isBlank(sign)) {
+            this.writeFailJson(response, "sign 参数为空");
+            return false;
+        }
+        String timestamp = request.getHeader("timestamp");
+        if (StrUtil.isBlank(timestamp)) {
+            this.writeFailJson(response, "timestamp 参数为空");
+            return false;
+        }
+        String appKey = request.getParameter("appKey");
+        AnalysisProperty.AppSign appSign = this.property.getAppSigns().stream().filter(e -> appKey.equals(e.getAppKey())).findFirst().orElse(null);
+        if (appSign == null) {
+            this.writeFailJson(response, "appKey非法");
+            return false;
+        }
+        if (!appSign.getEnabled()) {
+            logger.warn(StrUtil.format("{} 应用未开启接口签名验证!", appKey));
+            return HandlerInterceptor.super.preHandle(request, response, handler);
+        }
+        boolean flag = this.checkTimestamp(request, response, timestamp, appSign.getTimeout());
+        if (!flag) {
+            return false;
+        }
+        String appSecret = appSign.getAppSecret();
+        Map<String, String[]> parameterMap = request.getParameterMap();
+        String paramsSign = SignUtil.signParamsMd5(parameterMap, appSecret, timestamp);
+        if (!sign.equalsIgnoreCase(paramsSign)) {
+            this.writeFailJson(response, "签名验证失败");
+            return false;
+        }
         return HandlerInterceptor.super.preHandle(request, response, handler);
     }
+
+    private boolean checkTimestamp(HttpServletRequest request, HttpServletResponse response, String timestamp, Duration timeout) throws Exception {
+        long millis = System.currentTimeMillis();
+        long time = Long.parseLong(timestamp);
+        long seconds = Math.min(timeout.getSeconds(), MAX_TIMEOUT);
+        if (millis - time > seconds) {
+            logger.error(StrUtil.format("接口有重放攻击,调用方:{}-{}"), request.getRemoteUser(), request.getRemoteAddr());
+            this.writeFailJson(response, "请求失败!");
+            return false;
+        }
+        return true;
+    }
+
+    private synchronized void writeFailJson(HttpServletResponse response, String msg) throws Exception {
+        ResultVo<?> resultVo = ResultVo.fail(400, msg);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(Charset.defaultCharset().displayName());
+        response.getWriter().write(JSONUtil.toJsonStr(resultVo));
+        response.getWriter().flush();
+    }
 }

+ 23 - 0
src/main/java/com/smppw/analysis/infrastructure/config/AnalysisConfig.java

@@ -0,0 +1,23 @@
+package com.smppw.analysis.infrastructure.config;
+
+import com.smppw.analysis.infrastructure.components.ApiSignInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.lang.NonNull;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class AnalysisConfig implements WebMvcConfigurer {
+    private final AnalysisProperty property;
+
+    public AnalysisConfig(AnalysisProperty property) {
+        this.property = property;
+    }
+
+    @Override
+    public void addInterceptors(@NonNull InterceptorRegistry registry) {
+        if (this.property.getEnableSign()) {
+            registry.addInterceptor(new ApiSignInterceptor(this.property)).addPathPatterns();
+        }
+    }
+}

+ 26 - 8
src/main/java/com/smppw/analysis/infrastructure/config/AnalysisProperty.java

@@ -1,8 +1,13 @@
 package com.smppw.analysis.infrastructure.config;
 
+import lombok.Getter;
+import lombok.Setter;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
 
+import java.time.Duration;
+import java.util.List;
+
 /**
  * @author wangzaijun
  * @date 2023/8/4 17:35
@@ -20,13 +25,14 @@ public class AnalysisProperty {
      */
     private String cacheType = "memory";
     /**
-     * 签名校验是否可用
+     * 参数是否签名验证
      */
-    private Boolean enableSign = Boolean.FALSE;
+    private Boolean enableSign = Boolean.TRUE;
     /**
-     * 数据源
+     * 签名配置
+     * appKey 不能随意更改,要和RankDao的实现类一致
      */
-    private String dataSource = "";
+    private List<AppSign> appSigns;
 
     public String getPyUrl() {
         return pyUrl;
@@ -52,11 +58,23 @@ public class AnalysisProperty {
         this.enableSign = enableSign;
     }
 
-    public String getDataSource() {
-        return dataSource;
+    public List<AppSign> getAppSigns() {
+        return appSigns;
+    }
+
+    public void setAppSigns(List<AppSign> appSigns) {
+        this.appSigns = appSigns;
     }
 
-    public void setDataSource(String dataSource) {
-        this.dataSource = dataSource;
+    @Setter
+    @Getter
+    public static class AppSign {
+        private Boolean enabled = Boolean.FALSE;
+        private String appKey;
+        private String appSecret;
+        /**
+         * 默认超时5分钟超时
+         */
+        private Duration timeout = Duration.ofMinutes(5);
     }
 }

+ 6 - 2
src/main/resources/application-tob.yaml

@@ -67,8 +67,12 @@ smppw:
     analysis:
       py-url: https://pymaster-test.simuwang.com/  # python服务调用地址
       cache-type: redis # 缓存类型,支持redis和memory;如果是redis必须配置redis
-      enable-sign: false # 参数签名校验功能是否可用
-      data-source: tob # 数据源、toc和tob
+      app-signs:
+        - app-key: ppw-toc
+          app-secret: xxxxxx
+          enabled: true
+        - app-key: ppw-tob
+      enable-sign: false
 
 # 打印一下sql日志
 logging:

+ 6 - 2
src/main/resources/application-toc.yaml

@@ -67,8 +67,12 @@ smppw:
     analysis:
       py-url: https://pymaster-test.simuwang.com/  # python服务调用地址
       cache-type: redis # 缓存类型,支持redis和memory;如果是redis必须配置redis
-      enable-sign: false # 参数签名校验功能是否可用
-      data-source: toc # 数据源、toc和tob
+      app-signs:
+        - app-key: ppw-toc
+          app-secret: xxxxxx
+          enabled: true
+        - app-key: ppw-tob
+      enable-sign: false
 
 # 打印一下sql日志
 logging:

+ 7 - 2
src/main/resources/application.yaml

@@ -70,8 +70,13 @@ smppw:
     analysis:
       py-url: https://pymaster-test.simuwang.com/  # python服务调用地址
       cache-type: redis # 缓存类型,支持redis和memory;如果是redis必须配置redis
-      enable-sign: false # 参数签名校验功能是否可用
-      data-source: toc # 数据源、toc和tob
+      enable-sign: false
+      app-signs:
+        - app-key: ppw-toc
+          app-secret: xxxxxx
+          enabled: true
+          timeout: 360000 # 6分钟
+        - app-key: ppw-tob
 
 # 打印一下sql日志
 logging: