first commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package com.mhd.user.domain.security.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* @author ZhouGY
|
||||
* @title Encrypt
|
||||
* @description: 加密注解
|
||||
* @date 2024/1/25 15:37
|
||||
**/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Encrypt {
|
||||
/**
|
||||
* 入参是否解密,默认解密
|
||||
*/
|
||||
boolean in() default true;
|
||||
|
||||
/**
|
||||
* 返回是否加密,默认加密
|
||||
*/
|
||||
boolean out() default true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.mhd.user.domain.security.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 自定义操作日志记录注解
|
||||
*
|
||||
* @author mhd
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface OpenApiLog {
|
||||
|
||||
/**
|
||||
* 模块
|
||||
*/
|
||||
public String title() default "";
|
||||
|
||||
/**
|
||||
* 是否保存请求的参数
|
||||
*/
|
||||
public boolean isSaveRequestData() default true;
|
||||
|
||||
/**
|
||||
* 是否保存响应的参数
|
||||
*/
|
||||
public boolean isSaveResponseData() default true;
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package com.mhd.user.domain.security.aspect;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.mhd.common.core.domain.entity.DecryptRequestDO;
|
||||
import com.mhd.common.core.domain.entity.R;
|
||||
import com.mhd.common.core.domain.po.SysTenantsPo;
|
||||
import com.mhd.common.core.domain.po.thirdparty.SysDepartInterfaceInfoPo;
|
||||
import com.mhd.common.core.domain.thirdparty.SysDepartInterfaceInfoDTO;
|
||||
import com.mhd.common.core.exception.ServiceException;
|
||||
import com.mhd.common.core.feign.ThirdPartyServiceFeign;
|
||||
import com.mhd.common.core.utils.AESUtil;
|
||||
import com.mhd.common.core.utils.SignatureUtil;
|
||||
import com.mhd.user.domain.security.annotation.Encrypt;
|
||||
import com.mhd.user.infrastructure.feign.ProductServiceFeign;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.rmi.ServerException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author ZhouGY
|
||||
* @title DecryptRequestBodyAdvice
|
||||
* @description post请求的加密参数进行解密,返回一个JSONObject对象
|
||||
* @date 2023/8/17
|
||||
**/
|
||||
@ControllerAdvice(basePackages = "com.mhd.user.interfaces.facade.openApi")
|
||||
@Slf4j
|
||||
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
|
||||
|
||||
@Autowired
|
||||
private ThirdPartyServiceFeign thirdPartyServiceFeign;
|
||||
@Autowired
|
||||
private ProductServiceFeign productServiceFeign;
|
||||
|
||||
/**
|
||||
* 该方法用于判断当前请求,是否要执行beforeBodyRead方法
|
||||
* methodParameter: 方法的参数对象
|
||||
* type: 方法的参数类型
|
||||
* aClass: 将会使用到的Http消息转换器类类型
|
||||
* 注意:此判断方法,会在beforeBodyRead 和 afterBodyRead方法前都触发一次。
|
||||
*
|
||||
* @return 返回true则会执行beforeBodyRead
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
|
||||
//获取配置的组织ID
|
||||
R<SysTenantsPo> sysTenantsPoR = productServiceFeign.getTenantsInfoByOpen();
|
||||
if (R.SUCCESS != sysTenantsPoR.getCode()) {
|
||||
throw new ServiceException("未获取到组织配置信息");
|
||||
}
|
||||
SysTenantsPo sysTenantsPo = sysTenantsPoR.getData();
|
||||
//从三方配置表中取值
|
||||
SysDepartInterfaceInfoDTO sysDepartInterfaceInfoDTO = new SysDepartInterfaceInfoDTO();
|
||||
sysDepartInterfaceInfoDTO.setTopOrganizationId(sysTenantsPo.getTopOrganizationId());
|
||||
sysDepartInterfaceInfoDTO.setInterfaceType("openApi");
|
||||
sysDepartInterfaceInfoDTO.setInterfaceTypeKey("encrypt");
|
||||
List<SysDepartInterfaceInfoPo> sysDepartInterfaceInfoPos = thirdPartyServiceFeign.queryListByFeign(sysDepartInterfaceInfoDTO);
|
||||
if (sysDepartInterfaceInfoPos.isEmpty()){
|
||||
return false;
|
||||
}
|
||||
SysDepartInterfaceInfoPo sysDepartInterfaceInfoPo = sysDepartInterfaceInfoPos.get(0);
|
||||
//是否开启加密
|
||||
if(!"true".equals(sysDepartInterfaceInfoPo.getInterfaceTypeValue())) {
|
||||
return false;
|
||||
}
|
||||
log.info("【进入-RequestBody-supports】");
|
||||
boolean encode = false;
|
||||
// 判断该方法是否含有@Encrypt注解
|
||||
if (Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(Encrypt.class)) {
|
||||
//获取注解配置的包含和去除字段
|
||||
Encrypt serializedField = methodParameter.getMethodAnnotation(Encrypt.class);
|
||||
if (serializedField != null && serializedField.in()) {
|
||||
//入参是否需要解密
|
||||
encode = true;
|
||||
}
|
||||
}
|
||||
return encode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在Http消息转换器执转换,之前执行
|
||||
* inputMessage: 客户端的请求数据
|
||||
* methodParameter: 方法的参数对象
|
||||
* type: 方法的参数类型
|
||||
* aClass: 将会使用到的Http消息转换器类类型
|
||||
*
|
||||
* @return 返回 一个自定义的HttpInputMessage
|
||||
*/
|
||||
@Override
|
||||
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
|
||||
log.info("【进入-RequestBody-beforeBodyRead】");
|
||||
// 解密-使用解密后的数据,构造新的读取流
|
||||
return new DecryptHttpInputMessage(inputMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在Http消息转换器执转换,之后执行
|
||||
* o: 转换后的对象
|
||||
* httpInputMessage: 客户端的请求数据
|
||||
* methodParameter: handler方法的参数类型
|
||||
* type: handler方法的参数类型
|
||||
* aClass: 使用的Http消息转换器类类型
|
||||
*
|
||||
* @return 返回一个新的对象
|
||||
*/
|
||||
@Override
|
||||
public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
|
||||
log.info("【进入-RequestBody-afterBodyRead】");
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数与afterBodyRead相同,不过这个方法处理的是,body为空的情况
|
||||
*/
|
||||
@Override
|
||||
public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
|
||||
log.info("【进入-RequestBody-handleEmptyBody】");
|
||||
return o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密-使用解密后的数据,构造新的读取流
|
||||
*/
|
||||
class DecryptHttpInputMessage implements HttpInputMessage {
|
||||
private HttpInputMessage inputMessage;
|
||||
|
||||
public DecryptHttpInputMessage(HttpInputMessage inputMessage) {
|
||||
this.inputMessage = inputMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBody() throws IOException {
|
||||
//获取配置的组织ID
|
||||
R<SysTenantsPo> sysTenantsPoR = productServiceFeign.getTenantsInfoByOpen();
|
||||
if (R.SUCCESS != sysTenantsPoR.getCode()) {
|
||||
throw new RuntimeException("未获取到组织配置信息");
|
||||
}
|
||||
SysTenantsPo sysTenantsPo = sysTenantsPoR.getData();
|
||||
//从三方配置表中取值
|
||||
SysDepartInterfaceInfoDTO sysDepartInterfaceInfoDTO = new SysDepartInterfaceInfoDTO();
|
||||
sysDepartInterfaceInfoDTO.setTopOrganizationId(sysTenantsPo.getTopOrganizationId());
|
||||
sysDepartInterfaceInfoDTO.setInterfaceType("openApi");
|
||||
List<SysDepartInterfaceInfoPo> sysDepartInterfaceInfoPos = thirdPartyServiceFeign.queryListByFeign(sysDepartInterfaceInfoDTO);
|
||||
Map<String, SysDepartInterfaceInfoPo> stringSysDepartInterfaceInfoPoMap = sysDepartInterfaceInfoPos.stream().collect(Collectors.toMap(SysDepartInterfaceInfoPo::getInterfaceTypeKey, Function.identity()));
|
||||
// 1.参数解密处理
|
||||
String body = StringUtils.defaultString(IOUtils.toString(inputMessage.getBody(), "UTF-8"));
|
||||
log.info("解密前 请求参数:" + body);
|
||||
DecryptRequestDO decryptRequestDO = JSONUtil.toBean(JSONUtil.parseObj(body), DecryptRequestDO.class);
|
||||
//2.验证参数
|
||||
String secret = stringSysDepartInterfaceInfoPoMap.get(SignatureUtil.SECRET_KEY).getInterfaceTypeValue();
|
||||
boolean flag = SignatureUtil.validateSign(decryptRequestDO, secret);
|
||||
if (!flag){
|
||||
throw new RuntimeException("未通过签名验证,请检查签名信息是否有误");
|
||||
}
|
||||
// 4.用服务端私钥进行解密
|
||||
String iv = stringSysDepartInterfaceInfoPoMap.get(SignatureUtil.IV_KEY).getInterfaceTypeValue();
|
||||
String result = AESUtil.decrypt(decryptRequestDO.getEncryptContent(),secret,iv);
|
||||
log.info("解密后 请求参数:{}", result);
|
||||
|
||||
return IOUtils.toInputStream(result, "UTF-8");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return inputMessage.getHeaders();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package com.mhd.user.domain.security.aspect;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.mhd.common.core.domain.dto.open.SystemOpenApiLogDTO;
|
||||
import com.mhd.common.core.domain.entity.R;
|
||||
import com.mhd.common.core.domain.po.SysTenantsPo;
|
||||
import com.mhd.common.core.exception.ServiceException;
|
||||
import com.mhd.common.core.utils.ServletUtils;
|
||||
import com.mhd.common.core.utils.StringUtils;
|
||||
import com.mhd.common.core.utils.ip.IpUtils;
|
||||
import com.mhd.user.application.service.OpenApiLogApplicationService;
|
||||
import com.mhd.user.domain.security.annotation.OpenApiLog;
|
||||
import com.mhd.user.infrastructure.feign.ProductServiceFeign;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 操作日志记录处理
|
||||
*
|
||||
* @author mhd
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
public class OpenApiLogAspect {
|
||||
|
||||
@Autowired
|
||||
private ProductServiceFeign productServiceFeign;
|
||||
|
||||
@Autowired
|
||||
private OpenApiLogApplicationService openApiLogApplicationService;
|
||||
|
||||
/**
|
||||
* 处理完请求后执行
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
|
||||
public void doAfterReturning(JoinPoint joinPoint, OpenApiLog controllerLog, Object jsonResult) {
|
||||
handleLog(joinPoint, controllerLog, jsonResult);
|
||||
}
|
||||
|
||||
protected void handleLog(final JoinPoint joinPoint, OpenApiLog controllerLog, Object jsonResult) {
|
||||
try {
|
||||
// *========数据库日志=========*//
|
||||
SystemOpenApiLogDTO systemOpenApiLogDTO = new SystemOpenApiLogDTO();
|
||||
systemOpenApiLogDTO.setServiceCode("userApi");
|
||||
systemOpenApiLogDTO.setServiceName("用户服务");
|
||||
// 请求的地址
|
||||
String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
|
||||
systemOpenApiLogDTO.setOperIp(ip);
|
||||
systemOpenApiLogDTO.setOperUrl(ServletUtils.getRequest().getRequestURI());
|
||||
//根据当前登录获取客户信息 TODO
|
||||
//获取配置的组织ID
|
||||
R<SysTenantsPo> sysTenantsPoR = productServiceFeign.getTenantsInfoByOpen();
|
||||
if (R.SUCCESS != sysTenantsPoR.getCode()) {
|
||||
throw new ServiceException("未获取到组织配置信息");
|
||||
}
|
||||
SysTenantsPo sysTenantsPo = sysTenantsPoR.getData();
|
||||
systemOpenApiLogDTO.setTopOrganizationId(sysTenantsPo.getTopOrganizationId());
|
||||
systemOpenApiLogDTO.setOrganizationId(sysTenantsPo.getOrganizationId());
|
||||
systemOpenApiLogDTO.setOrganizationName(sysTenantsPo.getOrganizationName());
|
||||
systemOpenApiLogDTO.setCreateTime(new Date());
|
||||
|
||||
// 设置方法名称
|
||||
String className = joinPoint.getTarget().getClass().getName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
systemOpenApiLogDTO.setMethod(className + "." + methodName + "()");
|
||||
// 设置请求方式
|
||||
systemOpenApiLogDTO.setRequestMethod(ServletUtils.getRequest().getMethod());
|
||||
// 处理设置注解上的参数
|
||||
getControllerMethodDescription(joinPoint, controllerLog, systemOpenApiLogDTO, jsonResult);
|
||||
// 保存数据库
|
||||
openApiLogApplicationService.saveLog(systemOpenApiLogDTO);
|
||||
} catch (Exception exp) {
|
||||
// 记录本地异常日志
|
||||
log.error("==前置通知异常==");
|
||||
log.error("异常信息:{}", exp.getMessage());
|
||||
exp.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注解中对方法的描述信息 用于Controller层注解
|
||||
*
|
||||
* @param log 日志
|
||||
* @param systemOpenApiLogDTO 操作日志
|
||||
* @throws Exception
|
||||
*/
|
||||
public void getControllerMethodDescription(JoinPoint joinPoint, OpenApiLog log, SystemOpenApiLogDTO systemOpenApiLogDTO, Object jsonResult) throws Exception {
|
||||
// 设置标题
|
||||
systemOpenApiLogDTO.setTitle(log.title());
|
||||
// 是否需要保存request,参数和值
|
||||
if (log.isSaveRequestData()) {
|
||||
// 获取参数的信息,传入到数据库中。
|
||||
setRequestValue(joinPoint, systemOpenApiLogDTO);
|
||||
}
|
||||
// 是否需要保存response,参数和值
|
||||
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
|
||||
systemOpenApiLogDTO.setJsonResult(JSON.toJSONString(jsonResult));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求的参数,放到log中
|
||||
*
|
||||
* @param openApiLogDO 操作日志
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
private void setRequestValue(JoinPoint joinPoint, SystemOpenApiLogDTO openApiLogDO) throws Exception {
|
||||
String requestMethod = openApiLogDO.getRequestMethod();
|
||||
if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
|
||||
String params = argsArrayToString(joinPoint.getArgs());
|
||||
openApiLogDO.setOperParam(params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数拼装
|
||||
*/
|
||||
private String argsArrayToString(Object[] paramsArray) {
|
||||
String params = "";
|
||||
if (paramsArray != null && paramsArray.length > 0) {
|
||||
for (Object o : paramsArray) {
|
||||
if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
|
||||
try {
|
||||
Object jsonObj = JSON.toJSON(o);
|
||||
params += jsonObj.toString() + " ";
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要过滤的对象。
|
||||
*
|
||||
* @param o 对象信息。
|
||||
* @return 如果是需要过滤的对象,则返回true;否则返回false。
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public boolean isFilterObject(final Object o) {
|
||||
Class<?> clazz = o.getClass();
|
||||
if (clazz.isArray()) {
|
||||
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
|
||||
} else if (Collection.class.isAssignableFrom(clazz)) {
|
||||
Collection collection = (Collection) o;
|
||||
for (Object value : collection) {
|
||||
return value instanceof MultipartFile;
|
||||
}
|
||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||
Map map = (Map) o;
|
||||
for (Object value : map.entrySet()) {
|
||||
Map.Entry entry = (Map.Entry) value;
|
||||
return entry.getValue() instanceof MultipartFile;
|
||||
}
|
||||
}
|
||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||
|| o instanceof BindingResult;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user