feat(open-api): 重构开放接口签名验证逻辑

- 新增 SignInterceptor 用于签名验证
- 添加 BodyWrapperFilter 用于获取请求体
- 重构 SecurityUnitUseStatisticsDTO 数据结构
- 更新 OpenController 接口
- 修改 FastJson2Config 支持表单数据
This commit is contained in:
luozhun 2024-11-20 16:18:03 +08:00
parent 47cd8d9963
commit 56b0a9dca4
8 changed files with 331 additions and 60 deletions

View File

@ -2,10 +2,13 @@ package com.changhu.config;
import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.changhu.support.filter.BodyWrapperFilter;
import com.changhu.support.interceptor.JsonBodyInterceptor; import com.changhu.support.interceptor.JsonBodyInterceptor;
import com.changhu.support.interceptor.OpenApiInterceptor; import com.changhu.support.interceptor.SignInterceptor;
import com.changhu.support.interceptor.UserTypeInterceptor; import com.changhu.support.interceptor.UserTypeInterceptor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@ -59,10 +62,19 @@ public class WebConfig implements WebMvcConfigurer {
// 注册clientType 拦截器 用于校验当前用户是否匹配操作客户端 // 注册clientType 拦截器 用于校验当前用户是否匹配操作客户端
registry.addInterceptor(new UserTypeInterceptor()); registry.addInterceptor(new UserTypeInterceptor());
// 注册开放接口 拦截器 用于校验第三方是否携带指定apiKey // 注册开放接口 拦截器 用于校验第三方是否携带指定apiKey
registry.addInterceptor(new OpenApiInterceptor()) registry.addInterceptor(new SignInterceptor())
.addPathPatterns("/open/**"); .addPathPatterns("/open/**");
} }
@Bean
public FilterRegistrationBean<BodyWrapperFilter> loggingFilter() {
FilterRegistrationBean<BodyWrapperFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new BodyWrapperFilter());
registrationBean.addUrlPatterns("/open/*"); // 指定过滤的URL模式
registrationBean.setOrder(1000);
return registrationBean;
}
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")

View File

@ -68,4 +68,5 @@ public class OpenController {
public List<ServiceProjectSecurityUserRosterDTO> serviceProjectUserRoster(@Schema(description = "服务项目id") Long serviceProjectId) { public List<ServiceProjectSecurityUserRosterDTO> serviceProjectUserRoster(@Schema(description = "服务项目id") Long serviceProjectId) {
return openApiService.serviceProjectUserRoster(serviceProjectId); return openApiService.serviceProjectUserRoster(serviceProjectId);
} }
} }

View File

@ -7,6 +7,8 @@ import com.changhu.module.management.pojo.model.LegalPersonInfo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* @author 20252 * @author 20252
* @createTime 2024/11/18 上午10:32 * @createTime 2024/11/18 上午10:32
@ -14,41 +16,10 @@ import lombok.Data;
*/ */
@Data @Data
public class SecurityUnitUseStatisticsDTO { public class SecurityUnitUseStatisticsDTO {
@Schema(description = "服务项目id") @Schema(description = "事业单位id")
private Long serviceProjectId; private Long enterprisesUnitId;
@Schema(description = "公安单位id") @Schema(description = "事业单位名称")
private Long policeUnitId; private String enterprisesUnitName;
@Schema(description = "公安单位名称")
private String policeUnitName;
@Schema(description = "保安单位名称")
private String securityUnitName;
@Schema(description = "服务项目名称")
private String serviceProjectName;
@Schema(description = "保安服务类别")
private ServiceProjectType type;
@Schema(description = "二级类型")
private ServiceProjectTwoType twoType;
@Schema(description = "外包公司名称")
private String outsourceName;
@Schema(description = "保安人员总数")
private Integer securityUserTotal;
@Schema(description = "持证保安数量")
private Integer haveCardSecurityUserCount;
@Schema(description = "是否备案")
private IsOrNot isFiling;
@Schema(description = "证件号(保安服务许可证/备案证)")
private String idNumber;
@Schema(description = "保安单位法人信息")
private LegalPersonInfo securityUnitLegalPersonInfo;
@Schema(description = "项目经理")
private String serviceProjectManager;
@Schema(description = "项目经理联系方式")
private String serviceProjectManagerTelephone;
@Schema(description = "") @Schema(description = "")
private String provinceName; private String provinceName;
@Schema(description = "") @Schema(description = "")
@ -57,5 +28,52 @@ public class SecurityUnitUseStatisticsDTO {
private String districtsName; private String districtsName;
@Schema(description = "街道") @Schema(description = "街道")
private String streetName; private String streetName;
@Schema(description = "事业单位详细地址")
private String address;
@Schema(description = "服务项目列表")
List<_ServiceProjectVo> serviceProjectList;
@Schema(description = "公安单位id")
private Long policeUnitId;
@Schema(description = "公安单位名称")
private String policeUnitName;
@Schema(description = "保安单位id")
private Long securityUnitId;
@Schema(description = "保安单位名称")
private String securityUnitName;
@Schema(description = "保安单位法人信息")
private LegalPersonInfo securityUnitLegalPersonInfo;
@Schema(description = "项目经理")
private String serviceProjectManager;
@Schema(description = "项目经理联系方式")
private String serviceProjectManagerTelephone;
@Data
static class _ServiceProjectVo {
@Schema(description = "服务项目id")
private Long snowFlakeId;
@Schema(description = "服务项目名称")
private String name;
@Schema(description = "保安服务类别")
private ServiceProjectType type;
@Schema(description = "二级类型")
private ServiceProjectTwoType twoType;
@Schema(description = "外包公司名称")
private String outsourceName;
@Schema(description = "是否备案")
private IsOrNot isFiling;
@Schema(description = "证件号(保安服务许可证/备案证)")
private String idNumber;
@Schema(description = "保安人员总数")
private Integer securityUserTotal;
@Schema(description = "持证保安数量")
private Integer haveCardSecurityUserCount;
}
} }

View File

@ -86,7 +86,8 @@ public class FastJson2Config {
//4.解决中文乱码问题相当于在Controller上的@RequestMapping中加了个属性produces = "application/json" //4.解决中文乱码问题相当于在Controller上的@RequestMapping中加了个属性produces = "application/json"
fastConverter.setSupportedMediaTypes(List.of( fastConverter.setSupportedMediaTypes(List.of(
MediaType.APPLICATION_JSON MediaType.APPLICATION_JSON,
MediaType.APPLICATION_FORM_URLENCODED
)); ));
return fastConverter; return fastConverter;
} }

View File

@ -0,0 +1,27 @@
package com.changhu.support.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Objects;
/**
* @author 20252
* @createTime 2024/11/19 下午3:07
* @desc BodyWrapperFilter...
*/
@Slf4j
public class BodyWrapperFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("进入filter:{}", servletRequest.getRemoteAddr());
ServletRequest requestWrapper = null;
if (servletRequest instanceof HttpServletRequest) {
requestWrapper = new CustomHttpServletRequestWrapper((HttpServletRequest) servletRequest);
}
filterChain.doFilter(Objects.requireNonNullElse(requestWrapper, servletRequest), servletResponse);
}
}

View File

@ -0,0 +1,65 @@
package com.changhu.support.filter;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* @author 20252
* @createTime 2024/11/19 下午3:12
* @desc CustomHttpServletRequestWrapper...
*/
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
BufferedReader reader = request.getReader();
try (StringWriter writer = new StringWriter()) {
int read;
char[] buf = new char[1024 * 8];
while ((read = reader.read(buf)) != -1) {
writer.write(buf, 0, read);
}
this.body = writer.getBuffer().toString().getBytes();
}
}
public String getBody() {
return new String(body, StandardCharsets.UTF_8);
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}

View File

@ -0,0 +1,126 @@
package com.changhu.support.interceptor;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.digest.MD5;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.TypeReference;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.changhu.common.db.enums.IsEnable;
import com.changhu.common.exception.MessageException;
import com.changhu.common.utils.IpUtil;
import com.changhu.pojo.entity.AccessKeys;
import com.changhu.support.filter.CustomHttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author 20252
* @createTime 2024/11/19 下午1:58
* @desc SignInterceptor...
*/
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
private static final String ACCESS_KEY = "Access-Key";//调用者身份唯一标识
private static final String TIMESTAMP = "Time-Stamp";//时间戳
private static final String SIGN = "Sign";//签名
@Override
public boolean preHandle(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
String ip = IpUtil.getIp(request);
try {
return checkSign(request);
} catch (MessageException e) {
log.error("开放接口访问失败:{} 访问时间:{} IP:{} 访问接口:{} ", e.getMessage(), LocalDateTime.now(), ip, request.getRequestURI());
throw e;
}
}
return false;
}
private boolean checkSign(HttpServletRequest request) throws MessageException {
String accessKey = request.getHeader(ACCESS_KEY);
String timestamp = request.getHeader(TIMESTAMP);
String sign = request.getHeader(SIGN);
if (StrUtil.isBlank(accessKey) || StrUtil.isBlank(timestamp) || StrUtil.isBlank(sign)) {
throw new MessageException("请求体缺失");
}
AccessKeys accessKeyEntity = Db.lambdaQuery(AccessKeys.class)
.eq(AccessKeys::getAccessKey, accessKey)
.oneOpt()
.orElseThrow(() -> new MessageException("无效的accessKey"));
if (IsEnable.FALSE.equals(accessKeyEntity.getIsEnable())) {
throw new MessageException("无效的accessKey");
}
if (accessKeyEntity.getEffectiveTime() > 0 && System.currentTimeMillis() - Long.parseLong(timestamp) > accessKeyEntity.getEffectiveTime()) {
throw new MessageException("请求已过期");
}
List<String> allowedResources = Optional.ofNullable(accessKeyEntity.getAllowedResources()).orElseThrow(() -> new MessageException("暂无允许访问的资源"));
if (!allowedResources.contains(request.getRequestURI())) {
throw new MessageException("无效的请求资源");
}
Map<String, Object> hashMap = new HashMap<>();
//添加请求url参数
Map<String, String> map = HttpUtil.decodeParamMap(request.getQueryString(), StandardCharsets.UTF_8);
if (!map.isEmpty()) {
hashMap.putAll(map);
}
hashMap.put(ACCESS_KEY, accessKey);
hashMap.put(TIMESTAMP, timestamp);
//添加body参数
CustomHttpServletRequestWrapper c = (CustomHttpServletRequestWrapper) request;
Optional.ofNullable(new TypeReference<Map<String, Object>>() {
}.parseObject(c.getBody())).ifPresent(hashMap::putAll);
String nowSign = generatedSign(hashMap, accessKeyEntity.getSecretKey());
if (!sign.equals(nowSign)) {
throw new MessageException("签名错误");
}
return true;
}
/**
* 获取签名
*
* @param map 参数结果
* @param secretKey 密钥
* @return 签名字符串
*/
private String generatedSign(Map<String, Object> map, String secretKey) {
List<Map.Entry<String, Object>> entries = new ArrayList<>(map.entrySet());
String str = entries.stream()
.filter(en -> null == en.getValue() || StrUtil.isNotBlank(en.getValue().toString()))
.sorted(Comparator.comparing(o -> o.getKey().toLowerCase()))
.map(en -> StrUtil.format("{}={}", en.getKey(), URLUtil.encodeAll(en.getValue() + "")))
.collect(Collectors.joining("&"));
str += ("&Secret-Key=" + secretKey);
return MD5.create().digestHex(str).toUpperCase();
}
public static void main(String[] args) {
String str1 = "Access-Key=w2wzi0wefmmo6s735z2el8tfzitya5gj&addr=%E6%B9%96%E5%8D%97%E7%9C%81%E9%95%BF%E6%B2%99%E5%B8%82&age=14&name=zhangsan&Time-Stamp=1732084303463&Secret-Key=db1b5214-02ee-497f-957c-88323b4351bf";
String str2 = "Access-Key=w2wzi0wefmmo6s735z2el8tfzitya5gj&addr=%E6%B9%96%E5%8D%97%E7%9C%81%E9%95%BF%E6%B2%99%E5%B8%82&age=14&name=zhangsan&Time-Stamp=1732084303463&Secret-Key=db1b5214-02ee-497f-957c-88323b4351bf";
}
}

View File

@ -33,40 +33,61 @@
column="securityUnitLegalPersonInfo" column="securityUnitLegalPersonInfo"
typeHandler="com.baomidou.mybatisplus.extension.handlers.Fastjson2TypeHandler" typeHandler="com.baomidou.mybatisplus.extension.handlers.Fastjson2TypeHandler"
property="securityUnitLegalPersonInfo"/> property="securityUnitLegalPersonInfo"/>
<result
column="serviceProjectList"
typeHandler="com.baomidou.mybatisplus.extension.handlers.Fastjson2TypeHandler"
property="serviceProjectList"/>
</resultMap> </resultMap>
<select id="securityUnitUseStatistics" resultMap="SecurityUnitUseStatisticsDTOResultMap"> <select id="securityUnitUseStatistics" resultMap="SecurityUnitUseStatisticsDTOResultMap">
WITH security_user_counts AS (
SELECT
service_project_id,
COUNT(1) AS securityUserTotal,
SUM(IF(security_number != '', 1, 0)) AS haveCardSecurityUserCount
FROM
security_user
WHERE delete_flag = 0
GROUP BY service_project_id )
select select
sp.snow_flake_id as 'serviceProjectId', eu.snow_flake_id as 'enterprisesUnitId',
pu.snow_flake_id as 'policeUnitId', eu.name as 'enterprisesUnitName',
pu.name as 'policeUnitName',
su.name as 'securityUnitName',
sp.name as 'serviceProjectName',
sp.type as 'type',
sp.two_type as 'twoType',
sp.outsource_name as 'outsourceName',
count(suu.snow_flake_id) as 'securityUserTotal',
SUM(IF(suu.security_number != '', 1, 0)) as 'haveCardSecurityUserCount',
sp.is_filing as 'isFiling',
sp.id_number,
su.legal_person_info as 'securityUnitLegalPersonInfo',
mpu.name as 'serviceProjectManagerName',
mpu.telephone as 'serviceProjectManagerTelephone',
ad1.name as 'provinceName', ad1.name as 'provinceName',
ad2.name as 'cityName', ad2.name as 'cityName',
ad3.name as 'districtsName', ad3.name as 'districtsName',
ad4.name as 'streetName' ad4.name as 'streetName',
from police_unit pu eu.address as 'address',
pu.snow_flake_id as 'policeUnitId',
pu.name as 'policeUnitName',
su.snow_flake_id as 'securityUnitId',
su.name as 'securityUnitName',
su.legal_person_info as 'securityUnitLegalPersonInfo',
mpu.name as 'serviceProjectManagerName',
mpu.telephone as 'serviceProjectManagerTelephone',
json_arrayagg(
json_object(
'snowFlakeId', sp.snow_flake_id,
'name', sp.name,
'type', sp.type,
'twoType', sp.two_type,
'outsourceName', sp.outsource_name,
'isFiling', sp.is_filing,
'idNumber', sp.id_number,
'securityUserTotal', suc.securityUserTotal,
'haveCardSecurityUserCount', suc.haveCardSecurityUserCount
)) as 'serviceProjectList'
from
police_unit pu
join enterprises_unit eu on pu.snow_flake_id = eu.police_unit_id and eu.delete_flag = 0 join enterprises_unit eu on pu.snow_flake_id = eu.police_unit_id and eu.delete_flag = 0
join service_project sp on eu.snow_flake_id = sp.enterprises_unit_id and sp.delete_flag = 0 join service_project sp on eu.snow_flake_id = sp.enterprises_unit_id and sp.delete_flag = 0
join security_unit su on sp.security_unit_id = su.snow_flake_id and su.delete_flag = 0 join security_unit su on sp.security_unit_id = su.snow_flake_id and su.delete_flag = 0
LEFT join mini_program_user mpu on sp.project_manager_mini_program_user_id = mpu.snow_flake_id LEFT join mini_program_user mpu on sp.project_manager_mini_program_user_id = mpu.snow_flake_id
LEFT join security_user suu on suu.service_project_id = sp.snow_flake_id and suu.delete_flag = 0
left join administrative_division ad1 on eu.province = ad1.code and ad1.delete_flag = 0 left join administrative_division ad1 on eu.province = ad1.code and ad1.delete_flag = 0
left join administrative_division ad2 on eu.city = ad2.code and ad2.delete_flag = 0 left join administrative_division ad2 on eu.city = ad2.code and ad2.delete_flag = 0
left join administrative_division ad3 on eu.districts = ad3.code and ad3.delete_flag = 0 left join administrative_division ad3 on eu.districts = ad3.code and ad3.delete_flag = 0
left join administrative_division ad4 on eu.street = ad4.code and ad4.delete_flag = 0 left join administrative_division ad4 on eu.street = ad4.code and ad4.delete_flag = 0
left JOIN security_user_counts suc ON sp.snow_flake_id = suc.service_project_id
where pu.delete_flag = 0 where
pu.delete_flag = 0
<choose> <choose>
<when test="level==1"> <when test="level==1">
and eu.province = #{code} and eu.province = #{code}
@ -85,8 +106,8 @@
</when> </when>
<otherwise>and eu.snow_flake_id = -1</otherwise> <otherwise>and eu.snow_flake_id = -1</otherwise>
</choose> </choose>
group by sp.snow_flake_id group by eu.snow_flake_id
order by sp.create_time desc order by eu.create_time desc
</select> </select>
<select id="serviceProjectUserRoster" <select id="serviceProjectUserRoster"
resultType="com.changhu.pojo.dto.ServiceProjectSecurityUserRosterDTO"> resultType="com.changhu.pojo.dto.ServiceProjectSecurityUserRosterDTO">