feat(security): 添加签名验证功能并优化开放平台接口
- 新增 BodyWrapperFilter 和 CustomHttpServletRequestWrapper 类,用于获取请求体内容- 实现 SignInterceptor 类,添加签名验证功能 - 更新 OpenApiInterceptor,增加请求头缺失异常处理 - 修改 SecurityUnitUseStatisticsDTO,添加 policeUnitId 字段- 更新 OpenApiMapper.xml,增加 policeUnitId 字段映射- 修改前端 openPlatform 组件,演示签名生成和请求过程 - 引入 js-md5 依赖,用于生成 MD5 签名
This commit is contained in:
parent
fd690f3195
commit
771dac470c
|
@ -16,6 +16,8 @@ import lombok.Data;
|
||||||
public class SecurityUnitUseStatisticsDTO {
|
public class SecurityUnitUseStatisticsDTO {
|
||||||
@Schema(description = "服务项目id")
|
@Schema(description = "服务项目id")
|
||||||
private Long serviceProjectId;
|
private Long serviceProjectId;
|
||||||
|
@Schema(description = "公安单位id")
|
||||||
|
private Long policeUnitId;
|
||||||
@Schema(description = "公安单位名称")
|
@Schema(description = "公安单位名称")
|
||||||
private String policeUnitName;
|
private String policeUnitName;
|
||||||
@Schema(description = "保安单位名称")
|
@Schema(description = "保安单位名称")
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.changhu.support.filter;
|
||||||
|
|
||||||
|
import jakarta.servlet.*;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 {
|
||||||
|
ServletRequest requestWrapper = null;
|
||||||
|
if (servletRequest instanceof HttpServletRequest) {
|
||||||
|
requestWrapper = new CustomHttpServletRequestWrapper((HttpServletRequest) servletRequest);
|
||||||
|
}
|
||||||
|
if (requestWrapper == null) {
|
||||||
|
filterChain.doFilter(servletRequest, servletResponse);
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(requestWrapper, servletResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.changhu.support.interceptor;
|
package com.changhu.support.interceptor;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.changhu.common.annotation.CheckOpenApi;
|
import com.changhu.common.annotation.CheckOpenApi;
|
||||||
import com.changhu.common.exception.MessageException;
|
import com.changhu.common.exception.MessageException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
@ -23,6 +24,9 @@ public class OpenApiInterceptor implements HandlerInterceptor {
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
|
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
|
||||||
String header = request.getHeader("X-API-KEY");
|
String header = request.getHeader("X-API-KEY");
|
||||||
|
if (StrUtil.isBlank(header)) {
|
||||||
|
throw new MessageException("请求头缺失");
|
||||||
|
}
|
||||||
log.info("apiKey:{} {} 请求:{}", header, LocalDateTime.now(), request.getRequestURI());
|
log.info("apiKey:{} {} 请求:{}", header, LocalDateTime.now(), request.getRequestURI());
|
||||||
if (handler instanceof HandlerMethod handlerMethod) {
|
if (handler instanceof HandlerMethod handlerMethod) {
|
||||||
CheckOpenApi methodAnnotation = handlerMethod.getMethodAnnotation(CheckOpenApi.class);
|
CheckOpenApi methodAnnotation = handlerMethod.getMethodAnnotation(CheckOpenApi.class);
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
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.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 {
|
||||||
|
String ip = IpUtil.getIp(request);
|
||||||
|
try {
|
||||||
|
checkSign(request);
|
||||||
|
} catch (MessageException e) {
|
||||||
|
log.error("开放接口访问失败:{} 访问时间:{} IP:{} 访问接口:{} ", e.getMessage(), LocalDateTime.now(), ip, request.getRequestURI());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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("签名错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签名
|
||||||
|
*
|
||||||
|
* @param map 参数结果
|
||||||
|
* @param secretKey 密钥
|
||||||
|
* @return 签名字符串
|
||||||
|
*/
|
||||||
|
private String generatedSign(Map<String, Object> map, String secretKey) {
|
||||||
|
List<Map.Entry<String, Object>> infoIds = new ArrayList<>(map.entrySet());
|
||||||
|
infoIds.sort(Map.Entry.comparingByKey());
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (Map.Entry<String, Object> m : infoIds) {
|
||||||
|
if (null == m.getValue() || StrUtil.isNotBlank(m.getValue().toString())) {
|
||||||
|
sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("secret-key=").append(secretKey);
|
||||||
|
return MD5.create().digestHex(sb.toString()).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=1732067854476&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=1732067854476&secret-key=db1b5214-02ee-497f-957c-88323b4351bf";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -37,6 +37,7 @@
|
||||||
<select id="securityUnitUseStatistics" resultMap="SecurityUnitUseStatisticsDTOResultMap">
|
<select id="securityUnitUseStatistics" resultMap="SecurityUnitUseStatisticsDTOResultMap">
|
||||||
select
|
select
|
||||||
sp.snow_flake_id as 'serviceProjectId',
|
sp.snow_flake_id as 'serviceProjectId',
|
||||||
|
pu.snow_flake_id as 'policeUnitId',
|
||||||
pu.name as 'policeUnitName',
|
pu.name as 'policeUnitName',
|
||||||
su.name as 'securityUnitName',
|
su.name as 'securityUnitName',
|
||||||
sp.name as 'serviceProjectName',
|
sp.name as 'serviceProjectName',
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"@vueuse/core": "^11.0.3",
|
"@vueuse/core": "^11.0.3",
|
||||||
"ant-design-vue": "^4.2.3",
|
"ant-design-vue": "^4.2.3",
|
||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
|
"js-md5": "^0.8.3",
|
||||||
"jsencrypt": "^3.3.2",
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"pinia": "^2.2.2",
|
"pinia": "^2.2.2",
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {TableProMaxProps} from "@/types/components/table";
|
||||||
import {AccessKeyRes, GeneratedAccessKeyParams} from "@/types/views/openPlatform/openPlatform.ts";
|
import {AccessKeyRes, GeneratedAccessKeyParams} from "@/types/views/openPlatform/openPlatform.ts";
|
||||||
import {ComponentExposed} from "vue-component-type-helpers";
|
import {ComponentExposed} from "vue-component-type-helpers";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {useUserStore} from "@/stores/modules/userStore.ts";
|
import {md5} from "js-md5";
|
||||||
|
|
||||||
type TableProps = TableProMaxProps<AccessKeyRes>
|
type TableProps = TableProMaxProps<AccessKeyRes>
|
||||||
|
|
||||||
|
@ -123,29 +123,42 @@ const saveOrUpdateAccessKey = (params: GeneratedAccessKeyParams) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const userStore = useUserStore()
|
|
||||||
|
|
||||||
const a = () => {
|
const a = () => {
|
||||||
axios.get('http://127.0.0.1:8765/open/dataView', {
|
const accessKey = "w2wzi0wefmmo6s735z2el8tfzitya5gj"
|
||||||
headers: {
|
const secretKey = "db1b5214-02ee-497f-957c-88323b4351bf"
|
||||||
'X-API-KEY': '123',
|
const now = Date.now()
|
||||||
'access-key': '123',
|
const paramsMap = new Map();
|
||||||
'time-stamp': '123',
|
paramsMap.set('name', 'zhangsan')
|
||||||
'sign': '123',
|
paramsMap.set('age', 14)
|
||||||
'nonce': '123'
|
paramsMap.set('addr', '湖南省长沙市')
|
||||||
|
paramsMap.set('access-key', accessKey)
|
||||||
|
paramsMap.set('time-stamp', now)
|
||||||
|
|
||||||
|
// 将 Map 转换为数组并排序
|
||||||
|
const entries = Array.from(paramsMap.entries());
|
||||||
|
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
||||||
|
// 拼接成 URL 编码的字符串
|
||||||
|
let encodedParams = entries.map(([key, value]) => `${key}=${encodeURIComponent(value).replace(/%([0-9A-Fa-f]{2})/g, function (_, p1) {
|
||||||
|
return '%' + p1.toUpperCase();
|
||||||
|
})}`).join('&');
|
||||||
|
encodedParams = encodedParams + "&secret-key=" + secretKey
|
||||||
|
console.log(encodedParams);
|
||||||
|
|
||||||
|
const sign = md5(encodedParams).toUpperCase()
|
||||||
|
console.log(sign);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'access-key': accessKey,
|
||||||
|
'time-stamp': now,
|
||||||
|
'sign': sign
|
||||||
}
|
}
|
||||||
}).then(resp => {
|
console.log(headers);
|
||||||
console.log(resp);
|
|
||||||
|
api.get('/open/dataView', null, {
|
||||||
|
headers
|
||||||
})
|
})
|
||||||
// api.get('/open/dataView', null, {
|
|
||||||
// headers: {
|
|
||||||
// 'X-API-KEY': '123',
|
|
||||||
// 'access-key': '123',
|
|
||||||
// 'time-stamp': '123',
|
|
||||||
// 'sign': '123',
|
|
||||||
// 'nonce': '123'
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedResources = ref<SelectNodeVo<string>[]>([])
|
const allowedResources = ref<SelectNodeVo<string>[]>([])
|
||||||
|
|
Loading…
Reference in New Issue