Spring AOP 实战
7583字约25分钟
2024-06-25
前面总结了 Spring AOP 相关理论知识,我们这里开始进行实战,实战分类两个内容,一个系统请求日志记录,另一个是接口权限校验。前者侧重于 @AfterReturning
(后置返回通知) 内容的处理,后者侧重于 @Before
(前置通知)内容的处理
系统请求日志记录
在系统开发中,主要是后台管理系统(C
端我们记录埋点数据,内容更详细,方便统计用户行为),我们可能需要将用户使用系统的操作给记录下来,有以下好处:
出现问题方便排查,不用去慢慢搂日志看
可以作为审核日志,回放用户操作内容
项目代码整体结构图,为了实战内容尽可能的小而全,会涉及一些其他代码,着重看一下切面类即可
切面类
注意:
1、定义切面类,需要加上切面标识注解
@Aspect
,加上@Component
交由Spring
管理2、
request.getRemoteAddr()
获取请求用户的IP
,如果nginx
的配置没有将真实请求IP
地址一起转发,那么将获取不到真实IP
地址
/**
* @ClassName LogRecordAspect
* @Desciption 日志记录拦截切面
* @Author MaRui
* @Date 2023/2/27 16:45
* @Version 1.0
*/
@Aspect
@Component
public class LogRecordAspect {
@Autowired
private OperateLogService operateLogService;
private static final Logger log = LoggerFactory.getLogger(LogRecordAspect.class);
/**
* 配置织入点
*/
@Pointcut("@annotation(com.marui.log.record.common.annotation.LogRecord)")
public void logRecordPointCut(){
}
/**
* 请求处理完成后执行
* @param joinPoint
* @param result
*/
@AfterReturning(pointcut = "logRecordPointCut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
handleLog(joinPoint, null, result);
}
/**
* 拦截异常操作
* @param joinPoint
* @param e
*/
@AfterThrowing(value = "logRecordPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e, null);
}
/**
* 操作日志处理
* @param joinPoint
* @param e
* @param jsonResult
*/
private void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
try {
OperateLog operateLog = new OperateLog();
// 获取日志注解
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
LogRecord logRecordAnnotation = method.getAnnotation(LogRecord.class);
operateLog.setTitle(logRecordAnnotation.title());
log.info("====>日志记录,操作标题:{}", logRecordAnnotation.title());
operateLog.setOperateType(logRecordAnnotation.operateType().getType());
log.info("====>日志记录,操作类型:{}", logRecordAnnotation.operateType().getType());
String className = signature.getDeclaringType().getSimpleName();
String menthodName = signature.getName();
operateLog.setMethod(className + "." + menthodName + "()");
log.info("====>日志记录,操作方法名:{}", className + "." + menthodName + "()");
// 一般情况下,这里都是通过 token 什么的获取到用户信息(token 工具类)。这里从 header 中模拟获取用户编码
operateLog.setOperateCode(getOperateCode());
log.info("====>日志记录,获取操作人编码:{}", getOperateCode());
operateLog.setStatus(LogRecordConstant.RequestStatus.NORMAL);
if (e != null) {
operateLog.setStatus(LogRecordConstant.RequestStatus.UN_NORMAL);
// 异常记录信息过长,记录会导致异常,如果超长,截取 2000 字符
operateLog.setRequestError(StringUtils.substring(e.getMessage(), 0 , 2000));
log.info("====>日志记录,请求错误信息:{}", e.getMessage());
}
log.info("====>日志记录,请求状态:{}", LogRecordConstant.RequestStatus.NORMAL);
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
operateLog.setRequestMethod(request.getMethod());
log.info("====>日志记录,请求方式:{}", request.getMethod());
operateLog.setRequestUri(request.getRequestURI());
log.info("====>日志记录,请求URI:{}", request.getRequestURI());
operateLog.setRequestIp(request.getRemoteAddr());
log.info("====>日志记录,请求IP:{}", request.getRemoteAddr());
operateLog.setRequestParam(argsArrayToString(joinPoint.getArgs(), methodSignature.getParameterNames()));
log.info("====>日志记录,请求参数:{}", argsArrayToString(joinPoint.getArgs(), methodSignature.getParameterNames()));
String jsonResultStr = JSONObject.toJSONString(jsonResult);
// 结果记录信息太长,最多保存2000字符
operateLog.setRequestResult(jsonResultStr.substring(0, Math.min(2000, jsonResultStr.length())));
log.info("====>日志记录,请求结果:{}", JSONObject.toJSONString(jsonResult));
operateLogService.addLog(operateLog);
} catch (Exception exp) {
log.error("== 后置返回通知异常 ==,异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
}
/**
* 模拟获取用户编码
* @return
*/
private String getOperateCode() {
return "888";
}
/**
* 拼接参数名称和值
* @param paramsArray
* @param paramsNameArray
* @return
*/
private String argsArrayToString(Object[] paramsArray, Object[] paramsNameArray) {
StringBuilder requestParameter = new StringBuilder();
for (int i = 0; i < paramsNameArray.length; i++) {
if (!isFilterObject(paramsArray[i])) {
requestParameter.append("&")
.append(paramsNameArray[i])
.append("=")
.append(paramsArray[i]);
}
}
return requestParameter.replace(0, 1, "?").toString();
}
/**
* 判断是否需要过滤对象
* @param o
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
private boolean isFilterObject(final Object o) {
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse;
}
}
定义日志记录业务操作接口
/**
* @ClassName OperateLogService
* @Desciption 日志操作业务逻辑接口层
* @Author MaRui
* @Date 2023/7/15 13:11
* @Version 1.0
*/
public interface OperateLogService {
/**
* 添加操作日志
* @param operateLog
*/
void addLog(OperateLog operateLog);
}
日志记录业务实现接口
/**
* @ClassName OperateLogServiceImpl
* @Desciption 日志操作业务逻辑实现层
* @Author MaRui
* @Date 2023/7/15 13:12
* @Version 1.0
*/
@Slf4j
@Service
public class OperateLogServiceImpl implements OperateLogService {
@Async
@Override
public void addLog(OperateLog operateLog) {
// 这里打印一下日志记录的内容,权当模拟插入数据库的内容
log.info("插入日志记录:{}", JSONObject.toJSONString(operateLog));
}
}
addLog()
方法上增加了@Async
注解,这是一个异步操作,旨在数据库操作逻辑异步执行,不影响接口响应耗时。需要注意的是,使用@Async
注解需要在启动类上增加@EnableAsync
注解,并且在同一类中,一个方法调用另外一个有@Async
注解的方法,注解是不会生效的
依赖
pom.xml
文件中的主要依赖为 spring-boot-starter-aop
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.33</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
表结构
根据业务上的需要,定义需要记录内容的字段,这里给出的栗子是 MySQL
的 DDL(Data Definition Language,数据定义语言)
表结构
CREATE TABLE `operate_log` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键自增',
`title` varchar(25) DEFAULT NULL COMMENT '标题',
`operate_type` tinyint(1) DEFAULT NULL COMMENT '操作类型:0 其他、1 新增、2 删除、3 修改、4 查询',
`method` varchar(25) DEFAULT NULL COMMENT '方法名称',
`operate_code` varchar(25) DEFAULT NULL COMMENT '操作人编码',
`status` tinyint(1) DEFAULT NULL COMMENT '请求状态:0 正常、1 异常',
`request_method` varchar(10) DEFAULT NULL COMMENT '请求方式',
`request_uri` varchar(50) DEFAULT NULL COMMENT '请求uri',
`request_ip` varchar(25) DEFAULT NULL COMMENT '请求ip',
`request_param` text DEFAULT NULL COMMENT '请求参数',
`request_result` text DEFAULT NULL COMMENT '请求结果',
`request_error` text DEFAULT NULL COMMENT '请求错误信息',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB COMMENT='操作日志记录表';
静态常量 & 枚举值 & 实体类
ResultDTO
是封装的通用结果类,详解请移步 封装通用结果类
定义接口操作类型枚举,用于赋值注解中 operateType
属性。个人比较偏喜欢在接口中定义静态常量,但是自定义注解中,只能包含 String(字符串)
、enum(枚举)
、annotation(注解)
、primitive(基本数据类型)
,所以想要用 Integer
类型是不行的,所以在这里采用了枚举来定义
接口操作类型枚举
/**
* @ClassName OperateType
* @Desciption 接口操作类型枚举
* @Author MaRui
* @Date 2023/7/15 09:21
* @Version 1.0
*/
public enum OperateType {
/*** 其它 */
OTHER(0),
/*** 新增 */
INSERT(1),
/*** 删除 */
DELETE(2),
/*** 修改 */
UPDATE(3),
/*** 查询 */
SELECT(4);
private Integer type;
OperateType(Integer type) {
this.type = type;
}
public Integer getType() {
return this.type;
}
}
接口请求状态静态常量值
/**
* @ClassName LogRecordConstant
* @Desciption 日志记录相关静态常量
* @Author MaRui
* @Date 2023/7/15 12:38
* @Version 1.0
*/
public interface LogRecordConstant {
/**
* 请求状态,0 正常、1 异常
*/
interface RequestStatus {
Integer NORMAL = 0;
Integer UN_NORMAL = 1;
}
}
接口请求实体类
/**
* @ClassName OrderReqDto
* @Desciption 订单请求体
* @Author MaRui
* @Date 2023/2/27 16:26
* @Version 1.0
*/
@Data
public class OrderReqDto implements Serializable {
private static final long serialVersionUID = 1L;
/** 订单编号 */
private String orderNo;
/** 订单类型 */
private Integer type;
}
数据库实体类(这里只做演示,并不包含数据库操作,如若使用,添加 ORM 映射框架相应注解)
/**
* @ClassName OperateLog
* @Desciption 操作日志实体类
* @Author MaRui
* @Date 2023/7/15 12:55
* @Version 1.0
*/
@Data
public class OperateLog {
/** 主键自增 */
private Long id;
/** 标题 */
private String title;
/** 操作类型:0 其他、1 新增、2 删除、3 修改、4 查询 */
private Integer operateType;
/** 方法名称 */
private String method;
/** 操作人编码 */
private String operateCode;
/** 请求状态:0 正常、1 异常 */
private Integer status;
/** 请求方式 */
private String requestMethod;
/** 请求uri */
private String requestUri;
/** 请求ip */
private String requestIp;
/** 请求参数 */
private String requestParam;
/** 请求结果 */
private String requestResult;
/** 请求错误信息 */
private String requestError;
/** 创建时间 */
private Date createTime;
}
自定义注解
在自定义注解中,定义了两个属性,title
为必填内容,描述此接口功能,operateType
非必填,描述接口操作类型
自定义注解
/**
* @ClassName LogRecord
* @Desciption 日志操作记录注解
* @Author MaRui
* @Date 2023/2/27 16:30
* @Version 1.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecord {
String title();
OperateType operateType() default OperateType.OTHER;
}
测试接口
根据情况为接口添加自定义注解,比如像查询请求,记录的意义可能就没那么大
测试接口
/**
* @ClassName OrderController
* @Desciption 订单控制层
* @Author MaRui
* @Date 2023/2/27 16:24
* @Version 1.0
*/
@Slf4j
@RestController
@RequestMapping(value = "/order")
public class OrderController {
@LogRecord(title = "新增订单信息", operateType = OperateType.INSERT)
@PostMapping("/add")
public ResultDTO add(@RequestBody OrderReqDto orderReqDto) {
log.info("====>/add 请求执行");
return ResultDTO.ok(orderReqDto);
}
/**
* 不建议就查询接口也记录数据,我们只针对数据有变化的接口做操作日志记录
* @param orderNo
* @return
*/
@LogRecord(title = "查询订单信息", operateType = OperateType.SELECT)
@GetMapping("/getOne")
public ResultDTO getOne(@RequestParam String orderNo, @RequestParam Integer type) {
log.info("====>/getOne 请求执行");
// int i = 1/0;
return ResultDTO.ok(orderNo);
}
}
效果
请求订单信息查询接口:http://127.0.0.1:8080/order/getOne?orderNo=1234&type=1
订单信息查询请求日志内容
2023-07-15 16:17:51.833 INFO 6912 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-07-15 16:17:51.834 INFO 6912 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-07-15 16:17:51.834 INFO 6912 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2023-07-15 16:17:51.881 INFO 6912 --- [nio-8080-exec-1] c.m.l.record.controller.OrderController : ====>/getOne 请求执行
2023-07-15 16:17:51.883 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作标题:查询订单信息
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作类型:4
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作方法名:OrderController.getOne()
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,获取操作人编码:888
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求状态:0
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求方式:GET
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求URI:/order/getOne
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求IP:127.0.0.1
2023-07-15 16:17:51.885 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求参数:?orderNo=1234&type=1
2023-07-15 16:17:52.052 INFO 6912 --- [nio-8080-exec-1] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求结果:{"code":"000000","data":"1234","msg":"Success"}
2023-07-15 16:17:52.066 INFO 6912 --- [ task-1] c.m.l.r.s.impl.OperateLogServiceImpl : 插入日志记录:{"method":"OrderController.getOne()","operateCode":"888","operateType":4,"requestIp":"127.0.0.1","requestMethod":"GET","requestParam":"?orderNo=1234&type=1","requestResult":"{\"code\":\"000000\",\"data\":\"1234\",\"msg\":\"Success\"}","requestUri":"/order/getOne","status":0,"title":"查询订单信息"}
请求新增订单接口:http://127.0.0.1:8080/order/add
新增订单请求日志内容
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.record.controller.OrderController : ====>/add 请求执行
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作标题:新增订单信息
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作类型:1
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作方法名:OrderController.add()
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,获取操作人编码:888
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求状态:0
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求方式:POST
2023-07-15 16:18:06.713 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求URI:/order/add
2023-07-15 16:18:06.715 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求IP:127.0.0.1
2023-07-15 16:18:06.715 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求参数:?orderReqDto=OrderReqDto(orderNo=202307151050, type=1)
2023-07-15 16:18:06.717 INFO 6912 --- [nio-8080-exec-2] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求结果:{"code":"000000","data":{"orderNo":"202307151050","type":1},"msg":"Success"}
2023-07-15 16:18:06.718 INFO 6912 --- [ task-2] c.m.l.r.s.impl.OperateLogServiceImpl : 插入日志记录:{"method":"OrderController.add()","operateCode":"888","operateType":1,"requestIp":"127.0.0.1","requestMethod":"POST","requestParam":"?orderReqDto=OrderReqDto(orderNo=202307151050, type=1)","requestResult":"{\"code\":\"000000\",\"data\":{\"orderNo\":\"202307151050\",\"type\":1},\"msg\":\"Success\"}","requestUri":"/order/add","status":0,"title":"新增订单信息"}
请求增加了异常代码的订单信息查询接口:http://127.0.0.1:8080/order/getOne?orderNo=1234&type=1
异常订单信息查询请求日志内容
2023-07-15 16:14:33.880 INFO 19016 --- [nio-8080-exec-3] c.m.l.record.controller.OrderController : ====>/getOne 请求执行
2023-07-15 16:14:33.880 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作标题:查询订单信息
2023-07-15 16:14:33.880 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作类型:4
2023-07-15 16:14:33.880 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,操作方法名:OrderController.getOne()
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,获取操作人编码:888
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求错误信息:/ by zero
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求状态:0
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求方式:GET
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求URI:/order/getOne
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求IP:127.0.0.1
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求参数:?orderNo=1234&type=1
2023-07-15 16:14:33.881 INFO 19016 --- [nio-8080-exec-3] c.m.l.r.common.aspect.LogRecordAspect : ====>日志记录,请求结果:null
2023-07-15 16:14:33.881 INFO 19016 --- [ task-3] c.m.l.r.s.impl.OperateLogServiceImpl : 插入日志记录:{"method":"OrderController.getOne()","operateCode":"888","operateType":4,"requestError":"/ by zero","requestIp":"127.0.0.1","requestMethod":"GET","requestParam":"?orderNo=1234&type=1","requestResult":"null","requestUri":"/order/getOne","status":1,"title":"查询订单信息"}
2023-07-15 16:14:33.882 ERROR 19016 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at com.marui.log.record.controller.OrderController.getOne(OrderController.java:39) ~[classes/:na]
at com.marui.log.record.controller.OrderController$$FastClassBySpringCGLIB$$b0594142.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.9.jar:5.3.9]
接口权限校验
在系统开发中,我们可能需要将系统的能力开放给第三方来使用,并不想随便来个人就可以用。虽然可以写一个公共方法,在每个接口中手动调用公共方法进行权限校验,但是这种方式显然不是特别好。我们可以采用 AOP
+ 自定义注解方式统一对接口做校验
看了系统请求日志记录的实战内容后,接口权限校验内容其实是可以不看的。为了尽可能的小而全,接口权限校验代码中我将 全局异常处理实战、单元测试、AES
、MD5
加解密的内容也放进来了,所以代码量稍多,如果看也只建议看一下切面类的代码即可
项目代码整体结构图
能力开放对接文档
为了更好的帮助各位理解接口权限校验需求内容,我下面直接写一份第三方要对接我们接口所需要做的事
http
消息头中携带以下鉴权参数
名称 | 必填 | 数据类型 | 描述 |
---|---|---|---|
channel | true | String | MaRui (具体使用的渠道找我方分配) |
timestamp | true | String | 请求时间戳,格式为:YYYYMMddhhmmss |
signature | true | String | 请求参数签名,算法如下:MD5(timestamp + salt + phoneNumber) ,注意此签名为一次性使用,每次请求需要生成新的签名 |
生成
signature
时使用的手机号为明文,生成完之后,对手机号进行AES
加密处理。MD5
、AES
加密相关的代码在下面提供
MD5 加密参考代码
/** MD5盐值 */
String salt = "GWONIiFDpT3iDvOqI5bjA3aW";
String signature = DigestUtils.md5Hex(timestamp + salt + phoneNumber).toLowerCase();
AES 加解密相关常量值
/**
* @ClassName AESConstant
* @Desciption AES 加解密相关常量值。
* @Author MaRui
* @Date 2023/7/15 17:34
* @Version 1.0
*/
public interface AESConstant {
/** 秘钥 */
String APP_SECRT = "zipwTlo20pkPWM/t";
/** 偏移量 */
String OFFSET = "jCxmigCn6Bn14w2j";
}
AES 加解密工具类
/**
* @ClassName AESUtil
* @Desciption AES 加解密工具类
* @Author MaRui
* @Date 2023/7/15 17:26
* @Version 1.0
*/
public class AESUtil {
private static final String CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
private static final String AES = "AES";
public static final String CODE_TYPE = "UTF-8";
/**
* AES 加密操作
*
* @param content 待加密内容
* @param key 加密密钥
* @param offset 偏移量
* @return 返回Base64转码后的加密数据
*/
public static String encrypt(String content, String key, String offset) {
if (content == null || "".equals(content)) {
return content;
}
try {
/*
* 新建一个密码编译器的实例,由三部分构成,用"/"分隔,分别代表如下
* 1. 加密的类型(如AES,DES,RC2等)
* 2. 模式(AES中包含ECB,CBC,CFB,CTR,CTS等)
* 3. 补码方式(包含nopadding/PKCS5Padding等等)
* 依据这三个参数可以创建很多种加密方式
*/
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
//偏移量
IvParameterSpec zeroIv = new IvParameterSpec(offset.getBytes(CODE_TYPE));
byte[] byteContent = content.getBytes(CODE_TYPE);
// 使用加密秘钥
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CODE_TYPE), AES);
// 初始化为加密模式的密码器
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, zeroIv);
// 加密
byte[] result = cipher.doFinal(byteContent);
// 通过 Base64 转码返回
return Base64.encodeBase64String(result);
} catch (Exception ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* AES 解密操作
*
* @param content 待加密内容
* @param key 加密密钥
* @param offset 偏移量
* @return 返回解密数据
*/
public static String decrypt(String content, String key, String offset) {
if (content == null || "".equals(content)) {
return content;
}
try {
//实例化
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
IvParameterSpec zeroIv = new IvParameterSpec(offset.getBytes(CODE_TYPE));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CODE_TYPE), AES);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, zeroIv);
byte[] result = cipher.doFinal(Base64.decodeBase64(content));
return new String(result, CODE_TYPE);
} catch (Exception ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
}
切面类
/**
* @ClassName PermissionAspect
* @Desciption 权限校验拦截切面
* @Author MaRui
* @Date 2023/7/15 16:58
* @Version 1.0
*/
@Aspect
@Component
public class PermissionAspect {
private static final Logger log = LoggerFactory.getLogger(PermissionAspect.class);
/** 渠道对应权限 map */
private static final ConcurrentHashMap<String, List<String>> permissionConfigMap = new ConcurrentHashMap<>();
// 签名有效期,前后五分钟有效
private static final Integer SIGNATURE_VALID_TIME = 5 * 60;
private final Cache<String, String> signatureCache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
/**
* 配置织入点
*/
@Pointcut("@annotation(com.marui.open.common.annotation.PermissionCheck)")
public void permissionPointCut(){
}
@Before("permissionPointCut()")
public void doBefore(JoinPoint joinPoint) {
permissionCheck(joinPoint.getArgs());
}
private void permissionCheck(Object[] args) {
// 获取加密的手机号
String phoneNumber = null;
for (Object arg : args) {
if (phoneNumber == null && arg instanceof PermissionDto) {
phoneNumber = ((PermissionDto) arg).getPhone();
phoneNumber = AESUtil.decrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET);
}
}
// 校验手机号非空
ConditionUtil.checkStrNotNull(phoneNumber, ResCode.PHONE_NUMBER_EMPTY);
// 从 header 中获取值
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 从 Header 中获取 channel
String channel = request.getHeader("channel");
// 从 Header 中获取 timestamp
String timestamp = request.getHeader("timestamp");
// 从 Header 中获取 signature
String signature = request.getHeader("signature");
// 校验时间非空
ConditionUtil.checkStrNotNull(timestamp, ResCode.TIMESTAMP_EMPTY);
// 校验时间戳有效性
Date date = null;
try {
date = parseDate(timestamp, DateUtil.YYYYMMDDHHMMSS);
} catch (ParseException e) {
e.printStackTrace();
throw new AppException(ResCode.TIMESTAMP_FORMAT_ERROR.getValue(), ResCode.TIMESTAMP_FORMAT_ERROR.getCode());
}
long differenceTime = Math.abs(System.currentTimeMillis() - date.getTime());
ConditionUtil.checkArgument(differenceTime < SIGNATURE_VALID_TIME * 1000, ResCode.TIMESTAMP_INVALID);
// 校验签名非空
ConditionUtil.checkStrNotNull(signature, ResCode.SIGNATURE_EMPTY);
// 签名重复使用校验
ConditionUtil.checkStrNull(signatureCache.getIfPresent(signature), ResCode.SIGNATURE_REAPET);
signatureCache.put(signature, signature);
String sign = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
// 校验签名正确性
ConditionUtil.checkArgument(sign.equals(signature), ResCode.SIGNATURE_ERROR);
// 校验渠道非空
ConditionUtil.checkStrNotNull(channel, ResCode.CHANNEL_EMPTY);
// 校验接口权限
String requestURI = request.getRequestURI();
List<String> permissionConfigList = getPermissionConfig(channel);
ConditionUtil.checkCollectionNotEmpty(permissionConfigList, ResCode.CHANNEL_NO_URI_PERMISSION);
ConditionUtil.checkArgument(permissionConfigList.contains(requestURI), ResCode.CHANNEL_NO_URI_PERMISSION);
}
/**
* 获取渠道对应的接口权限
* @param channel
* @return
*/
private List<String> getPermissionConfig(String channel) {
List<String> result = permissionConfigMap.get(channel);
if (!CollectionUtils.isEmpty(result)) {
return result;
}
if (CollectionUtils.isEmpty(permissionConfigMap)) {
List<String> permissionList = Arrays.asList("/order/create");
permissionConfigMap.put("MaRui", permissionList);
}
return permissionConfigMap.get(channel);
}
}
依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.33</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
静态常量 & 枚举 & 实体类
ResultDTO
是封装的通用结果类,详解请移步 封装通用结果类
AES 加解密相关常量值
/**
* @ClassName AESConstant
* @Desciption AES 加解密相关常量值。
* @Author MaRui
* @Date 2023/7/15 17:34
* @Version 1.0
*/
public interface AESConstant {
/** 秘钥 */
String APP_SECRT = "zipwTlo20pkPWM/t";
/** 偏移量 */
String OFFSET = "jCxmigCn6Bn14w2j";
}
MD5 相关常量
/**
* @ClassName MD5Constant
* @Desciption MD5 相关常量
* @Author MaRui
* @Date 2023/7/15 18:27
* @Version 1.0
*/
public interface MD5Constant {
/** MD5盐值 */
String salt = "GWONIiFDpT3iDvOqI5bjA3aW";
}
异常枚举值
/**
* @Enum ResCode
* @Desciption 异常枚举值
* @Author MaRui
* @Date 2023/2/27 14:17
* @Version 1.0
*/
public enum ResCode {
SUCCESS("000000","Success"),
PARAM_VALID("0000","参数校验"),
ARGUMENT_EMPTY("010001","参数不可以为空"),
PHONE_NUMBER_EMPTY("020001","手机号不可为空"),
CHANNEL_EMPTY("020002","channel 为空"),
TIMESTAMP_EMPTY("020003","timestamp 为空"),
TIMESTAMP_FORMAT_ERROR("020004","timestamp 格式错误"),
TIMESTAMP_INVALID("020005","timestamp 失效"),
SIGNATURE_EMPTY("020006","signature 为空"),
SIGNATURE_ERROR("020007","signature 错误"),
SIGNATURE_REAPET("020008","signature 重复使用"),
CHANNEL_NO_URI_PERMISSION("020009","当前渠道未开通此接口权限");
private final String code;
private final String value;
ResCode(String code, String value) {
this.code = code;
this.value = value;
}
public String getCode() {
return this.code;
}
public String getValue() {
return this.value;
}
/**
* 根据code获取value
*
* @param code
* @return
*/
public static String getValue(String code) {
for (ResCode r : ResCode.values()) {
if (r.getCode().equals(code)) {
return r.value;
}
}
return null;
}
}
订单请求实体类
/**
* @ClassName OrderReqDto
* @Desciption 订单请求实体类
* @Author MaRui
* @Date 2023/7/15 17:08
* @Version 1.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderReqDto implements PermissionDto {
/** 手机号,AES 加密 */
private String phoneNumber;
/** 商品名称 */
private String goodsName;
/** 价格 */
private BigDecimal price;
@Override
public String getPhone() {
return this.phoneNumber;
}
}
订单返回实体类
/**
* @ClassName OrderResDto
* @Desciption 订单返回实体类
* @Author MaRui
* @Date 2023/7/15 17:13
* @Version 1.0
*/
@Data
public class OrderResDto {
/** 订单号 */
private String orderNo;
/** 商品名称 */
private String goodsName;
/** 订单创建时间 */
private Date createTime;
/** 价格 */
private BigDecimal price;
}
权限 DTO
/**
* @ClassName PermissionDto
* @Desciption 权限 DTO
* @Author MaRui
* @Date 2023/7/15 17:16
* @Version 1.0
*/
public interface PermissionDto {
String getPhone();
}
工具类
AES 加解密工具类
/**
* @ClassName AESUtil
* @Desciption AES 加解密工具类
* @Author MaRui
* @Date 2023/7/15 17:26
* @Version 1.0
*/
public class AESUtil {
private static final String CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
private static final String AES = "AES";
public static final String CODE_TYPE = "UTF-8";
/**
* AES 加密操作
*
* @param content 待加密内容
* @param key 加密密钥
* @param offset 偏移量
* @return 返回Base64转码后的加密数据
*/
public static String encrypt(String content, String key, String offset) {
if (content == null || "".equals(content)) {
return content;
}
try {
/*
* 新建一个密码编译器的实例,由三部分构成,用"/"分隔,分别代表如下
* 1. 加密的类型(如AES,DES,RC2等)
* 2. 模式(AES中包含ECB,CBC,CFB,CTR,CTS等)
* 3. 补码方式(包含nopadding/PKCS5Padding等等)
* 依据这三个参数可以创建很多种加密方式
*/
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
//偏移量
IvParameterSpec zeroIv = new IvParameterSpec(offset.getBytes(CODE_TYPE));
byte[] byteContent = content.getBytes(CODE_TYPE);
// 使用加密秘钥
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CODE_TYPE), AES);
// 初始化为加密模式的密码器
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, zeroIv);
// 加密
byte[] result = cipher.doFinal(byteContent);
// 通过 Base64 转码返回
return Base64.encodeBase64String(result);
} catch (Exception ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
/**
* AES 解密操作
*
* @param content 待加密内容
* @param key 加密密钥
* @param offset 偏移量
* @return 返回解密数据
*/
public static String decrypt(String content, String key, String offset) {
if (content == null || "".equals(content)) {
return content;
}
try {
//实例化
Cipher cipher = Cipher.getInstance(CBC_PKCS5_PADDING);
IvParameterSpec zeroIv = new IvParameterSpec(offset.getBytes(CODE_TYPE));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(CODE_TYPE), AES);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, zeroIv);
byte[] result = cipher.doFinal(Base64.decodeBase64(content));
return new String(result, CODE_TYPE);
} catch (Exception ex) {
Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
}
条件判断抛出异常工具
/**
* @ClassName ConditionUtil
* @Desciption 条件判断抛出异常工具
* @Author MaRui
* @Date 2023/2/27 14:15
* @Version 1.0
*/
public class ConditionUtil {
/**
* 校验参数非空:如果对象为 null 抛出自定义异常
* @param object
* @param msg
*/
public static void checkNotNull(Object object, String msg) {
if (null == object) {
throw new AppException(ResCode.PARAM_VALID.getValue() + ": " + msg, ResCode.PARAM_VALID.getCode());
}
}
/**
* 校验参数非空:如果对象为 null 抛出自定义异常
* @param object
* @param resCode
*/
public static void checkNotNull(Object object, ResCode resCode) {
if (null == object) {
throw new AppException(resCode.getValue(), resCode.getCode());
}
}
/**
* 校验参数非空:如果对象为 null 抛出自定义异常
* @param str
* @param resCode
*/
public static void checkStrNotNull(String str, ResCode resCode) {
if (StringUtils.isBlank(str)) {
throw new AppException(resCode.getValue(), resCode.getCode());
}
}
/**
* 校验参数空:如果对象不为 null 抛出自定义异常
* @param str
* @param resCode
*/
public static void checkStrNull(String str, ResCode resCode) {
if (StringUtils.isNotBlank(str)) {
throw new AppException(resCode.getValue(), resCode.getCode());
}
}
/**
* 由调用方校验,将校验结果判断是否抛出自定义异常
* @param bo
* @param msg
*/
public static void checkArgument(Boolean bo, String msg) {
if (!bo) {
throw new AppException(ResCode.PARAM_VALID.getValue() + ": " + msg, ResCode.PARAM_VALID.getCode());
}
}
/**
* 由调用方校验,将校验结果判断是否抛出自定义异常
* @param bo
* @param resCode
*/
public static void checkArgument(Boolean bo, ResCode resCode) {
if (!bo) {
throw new AppException(resCode.getValue(), resCode.getCode());
}
}
/**
* 集合不为空:如果集合为空抛出自定义异常
* @param collection
* @param msg
*/
public static void checkCollectionNotEmpty(Collection<?> collection, String msg) {
if (CollectionUtils.isEmpty(collection)) {
throw new AppException(ResCode.PARAM_VALID.getValue() + ": " + msg, ResCode.PARAM_VALID.getCode());
}
}
/**
* 集合不为空:如果集合为空抛出自定义异常
* @param collection
* @param resCode
*/
public static void checkCollectionNotEmpty(Collection<?> collection, ResCode resCode) {
if (CollectionUtils.isEmpty(collection)) {
throw new AppException(resCode.getValue(), resCode.getCode());
}
}
}
时间工具类
/**
* @ClassName DateUtil
* @Desciption 时间工具类
* @Author MaRui
* @Date 2023/7/15 18:49
* @Version 1.0
*/
public class DateUtil {
public static String YYYYMMDDHHMMSS = "yyyyMMddHHmmss";
public static final String parseDateToStr(final Date date, final String format) {
return new SimpleDateFormat(format).format(date);
}
}
全局异常捕获
全局统一异常处理器
/**
* @ClassName AppExceptionHandler
* @Desciption 全局统一异常处理器
* @Author MaRui
* @Date 2023/2/24 11:01
* @Version 1.0
*/
@RestControllerAdvice
public class AppExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理自定义异常
*/
@ExceptionHandler(AppException.class)
public ResultDTO handleException(AppException e) {
logger.error(e.getMessage(), e);
return ResultDTO.error(e.getCode(), e.getMsg());
}
@ExceptionHandler(Exception.class)
public ResultDTO handleException(Exception e) {
logger.error(e.getMessage(), e);
return ResultDTO.error("500", "未知错误,请联系系统管理员");
}
}
自定义异常类
/**
* @ClassName AppException
* @Desciption 自定义异常类
* @Author MaRui
* @Date 2023/2/24 10:59
* @Version 1.0
*/
public class AppException extends RuntimeException {
private String msg;
private String code = "500";
public AppException(String msg) {
super(msg);
this.msg = msg;
}
public AppException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public AppException(String msg, String code) {
super(msg);
this.msg = msg;
this.code = code;
}
public AppException(String msg, String code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public String toString() {
return "AppException{" + "msg='" + msg + '\'' + ", code='" + code + '\'' + '}';
}
public String toJson() {
return "{" + "msg='" + msg + '\'' + ", code='" + code + '\'' + '}';
}
}
自定义注解
权限校验注解
/**
* @ClassName PermissionAnnotation
* @Desciption 权限校验注解
* @Author MaRui
* @Date 2023/7/15 16:57
* @Version 1.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PermissionCheck {
}
测试接口
测试接口
/**
* @ClassName OrderController
* @Desciption 订单控制层
* @Author MaRui
* @Date 2023/7/15 17:12
* @Version 1.0
*/
@RestController
@RequestMapping(value = "/order")
public class OrderController {
/**
* 创建订单
* @param orderReqDto
* @return
*/
@PermissionCheck
@PostMapping("/create")
public ResultDTO<OrderResDto> createOrder(@RequestBody OrderReqDto orderReqDto) {
OrderResDto orderResDto = new OrderResDto();
orderResDto.setOrderNo("DD202307150001");
orderResDto.setGoodsName(orderResDto.getGoodsName());
orderResDto.setCreateTime(new Date());
orderResDto.setPrice(orderReqDto.getPrice());
return ResultDTO.ok(orderResDto);
}
}
单元测试
订单接口单元测试
/**
* @ClassName OrderControllerTest
* @Desciption 订单接口单元测试
* @Author MaRui
* @Date 2023/7/15 19:28
* @Version 1.0
*/
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class OrderControllerTest {
/**
* 订单创建失败:手机号为空
*/
@Test
public void testCreateOrder1() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.PHONE_NUMBER_EMPTY.getCode(), result.getCode());
assertEquals(ResCode.PHONE_NUMBER_EMPTY.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:timeStamp 为空
*/
@Test
public void testCreateOrder2() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = "";
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.TIMESTAMP_EMPTY.getCode(), result.getCode());
assertEquals(ResCode.TIMESTAMP_EMPTY.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:timeStamp 失效
*/
@Test
public void testCreateOrder3() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", String.valueOf(Long.valueOf(timestamp) - 550l));
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.TIMESTAMP_INVALID.getCode(), result.getCode());
assertEquals(ResCode.TIMESTAMP_INVALID.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:timeStamp 失效
*/
@Test
public void testCreateOrder4() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", String.valueOf(Long.valueOf(timestamp) + 550l));
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.TIMESTAMP_INVALID.getCode(), result.getCode());
assertEquals(ResCode.TIMESTAMP_INVALID.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:签名为空
*/
@Test
public void testCreateOrder5() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui");
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.SIGNATURE_EMPTY.getCode(), result.getCode());
assertEquals(ResCode.SIGNATURE_EMPTY.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:签名重复使用
*/
@Test
public void testCreateOrder6() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result1 = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>第一次请求结果:{}", result1);
ResultDTO result2 = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>第二次请求结果:{}", result2);
assertEquals(ResCode.SIGNATURE_REAPET.getCode(), result2.getCode());
assertEquals(ResCode.SIGNATURE_REAPET.getValue(), result2.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:签名错误
*/
@Test
public void testCreateOrder7() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature + "error");
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.SIGNATURE_ERROR.getCode(), result.getCode());
assertEquals(ResCode.SIGNATURE_ERROR.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:渠道为空
*/
@Test
public void testCreateOrder8() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.CHANNEL_EMPTY.getCode(), result.getCode());
assertEquals(ResCode.CHANNEL_EMPTY.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建失败:签名错误
*/
@Test
public void testCreateOrder9() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui2");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.CHANNEL_NO_URI_PERMISSION.getCode(), result.getCode());
assertEquals(ResCode.CHANNEL_NO_URI_PERMISSION.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
/**
* 订单创建成功
*/
@Test
public void testCreateOrder10() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
String phoneNumber = "13594877777";
// 请求的 URL 地址
String reqUrl = "http://127.0.0.1:8080/order/create";
// 设置 headers 的 contentType 为 application/json
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String timestamp = DateUtil.parseDateToStr(new Date(), DateUtil.YYYYMMDDHHMMSS);
headers.set("timestamp", timestamp);
headers.set("channel", "MaRui");
String signature = DigestUtils.md5Hex(timestamp + MD5Constant.salt + phoneNumber).toLowerCase();
headers.set("signature", signature);
// 请求体
OrderReqDto orderReqDto = OrderReqDto.builder()
.goodsName("坚果")
.price(new BigDecimal("100"))
.phoneNumber(AESUtil.encrypt(phoneNumber, AESConstant.APP_SECRT, AESConstant.OFFSET)).build();
HttpEntity<OrderReqDto> httpEntity = new HttpEntity<>(orderReqDto, headers);
ResultDTO result = restTemplate.postForObject(reqUrl, httpEntity, ResultDTO.class);
log.info("==========>请求结果:{}", result);
assertEquals(ResCode.SUCCESS.getCode(), result.getCode());
assertEquals(ResCode.SUCCESS.getValue(), result.getMsg());
TimeUnit.SECONDS.sleep(2L);
}
}