request参数升序排序 md5加密 防重播 header信息 java API接口调用 切片机制实现

api接口大多都支持访问信息的验证,其中参数的排序,加密都是经常用到的。有时候还需要将验证信息放到header中。
将api调用者的参数的key及头信息(时间戳、随机串,调用者标识)按照ascii码升序排列后 用系统中的salt加密 ,得到的签名与调用者传入的签名进行比较。已实现验证合法性的目的。
下面就给大家介绍下我在项目中是如何使用的。大家多提意见。

  1. 创建切片类

    package com.xxx.openapis.annotations
    
    import java.lang.annotation.*;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Inherited
    public @interface OpenAPI {
    }
    
  2. 创建返回结果类

    package com.xxx.openapis.web
    
    import java.io.Serializable;
    
    public class AjaxResult implements Serializable {
        private final int status;
        private final Object data;
    
        private AjaxResult(int status, Object data) {
            this.status = status;
            this.data = data;
        }
    
        public static AjaxResult error(int errorCode, String errorMessage) {
            return new AjaxResult(errorCode, errorMessage);
        }
    
        public static AjaxResult success() {
            return new AjaxResult(0, null);
        }
    
        public static AjaxResult success(Object data) {
            return new AjaxResult(0, data);
        }
    
        public int getStatus() {
            return status;
        }
    
        public Object getData() {
            return data;
        }
    }
    
  3. 创建一个实现org.springframework.core.Ordered 的抽象切片类

    package com.xxx.openapis.aspects;
    
    import com.xxx.openapis.web.AjaxResult;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.springframework.core.Ordered;
    
    import javax.inject.Inject;
    import javax.servlet.http.HttpServletRequest;
    
    @Aspect
    public abstract class AbstractAPIAspect implements Ordered {
        static final String X_API_TIMESTAMP = "x_api_timestamp";//时间戳
        static final String X_API_NONCE = "x_api_nonce"; //随机串
        static final String X_API_PARTNERCODE = "x_api_partnercode";//调用者标识
        static final String X_API_SIGNATURE = "x_api_signature";//签名
        static final long NONCE_TTL = 20 * 60 * 1000L;//随机串时间范围
        @Inject
        HttpServletRequest request;
    
        @Around("@within(com.xxx.openapis.annotations.OpenAPI) && execution(public com.xxx.web.AjaxResult *(..))")
        public Object aound(ProceedingJoinPoint jp) {
            getLogger().debug("检查API请求");
            AjaxResult error = check();
            if (error != null) {
                getLogger().error("检查API请求,发现问题:{}", error.getData());
                return error;
            }
            try {
                getLogger().debug("检查API请求,没有发现问题继续");
                return jp.proceed();
            } catch (Throwable throwable) {
                getLogger().error("执行出错", throwable);
                return AjaxResult.error(500, "执行出错," + throwable.getMessage());
            }
        }
    
        protected abstract Logger getLogger();
    
        protected abstract AjaxResult check();
    
    }
    
  4. 创建时间戳检查类继承AbstractAspect抽象类

    package com.xxx.openapis.aspects;
    
    import com.xxx.openapis.web.AjaxResult;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    
    import javax.inject.Named;
    
    import static org.springframework.util.StringUtils.hasText;
    
    @Aspect
    @Named
    @Slf4j
    public class TimestampChecker extends AbstractAPIAspect {
    
        static final int ORDER = 1;//执行顺序
    
        @Override
        protected Logger getLogger() {
            return log;
        }
    
        @Override
        protected AjaxResult check() {
    
            String header = request.getHeader(X_API_TIMESTAMP);
            if (!hasText(header)) return AjaxResult.error(401, "未提供请求时间戳");
            long timestamp;
            try {
                timestamp = Long.valueOf(header);
            } catch (NumberFormatException e) {
                return AjaxResult.error(401, "请求时间戳应该是系统时间的毫秒数");
            }
            if (Math.abs(timestamp - System.currentTimeMillis()) > NONCE_TTL)
                return AjaxResult.error(403, "时间超过允许的范围,可能是“重播”攻击");
            return null;
        }
    
        @Override
        public int getOrder() {
            return ORDER;
        }
    }
    
  5. 创建一个随机数检查类继承AbstractAspect抽象类

    package com.xxx.openapis.aspects;
    
    import com.xxx.openapis.web.AjaxResult;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.springframework.data.redis.core.StringRedisTemplate;
    
    import javax.inject.Inject;
    import javax.inject.Named;
    import java.util.concurrent.TimeUnit;
    
    import static org.springframework.util.StringUtils.hasText;
    
    @Aspect
    @Named
    @Slf4j
    public class NonceChecker extends AbstractAPIAspect {
    
        static final int ORDER = TimestampChecker.ORDER + 1;
        private static final String KEY_PREFIX = "DISTRIBUTOR.API.NONCE.";
        @Inject
        private StringRedisTemplate redisTemplate;
    
        @Override
        protected Logger getLogger() {
            return log;
        }
    
        @Override
        protected AjaxResult check() {
            String nonce = request.getHeader(X_API_NONCE);
            if (!hasText(nonce)) return AjaxResult.error(401, "未提供请求随机数");
    
            if (nonceInRedis(nonce)) return AjaxResult.error(403, "已接收相同的请求,怀疑是“重播”攻击。");
            return null;
        }
    
        private boolean nonceInRedis(String nonce) {
            if (redisTemplate == null) {
                getLogger().debug("未启用Redis,忽略此项检查");
                return false;
            }
            if (hasText(redisTemplate.opsForValue().get(KEY_PREFIX + nonce))) return true;
            redisTemplate.opsForValue().set(KEY_PREFIX + nonce, nonce, NONCE_TTL, TimeUnit.MILLISECONDS);
            return false;
        }
    
        @Override
        public int getOrder() {
            return ORDER;
        }
    }
    
  6. 创建用户验证类继承AbstractAPIAspect抽象类

    package com.xxx.openapis.aspects;
    
    import com.xxx.openapis.models.User;
    import com.xxx.openapis.web.AjaxResult;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    
    import javax.inject.Inject;
    import javax.inject.Named;
    
    import static org.springframework.util.StringUtils.hasText;
    
    @Aspect
    @Named
    @Slf4j
    public class UserChecker extends AbstractAPIAspect {
        static final int ORDER = NonceChecker.ORDER + 1;
        @Inject
        private UserService userService;
    
        @Override
        protected Logger getLogger() {
            return log;
        }
    
        @Override
        protected AjaxResult check() {
            String userCode = request.getHeader(X_API_PARTNERCODE);
            if (!hasText(userCode)) {
                return AjaxResult.error(401, "未提用户商编号");
            }
            User user = userService.getUser(userCode);
            if (user == null) return AjaxResult.error(403, "用户编号错误,不存在此用户");
            if (user.isLocked()) return AjaxResult.error(403, "用户编号错误,此用户已冻结");
            request.setAttribute("user", user);
            return null;
        }
    
        @Override
        public int getOrder() {
            return ORDER;
        }
    }
    
  7. 创建签名验证类继承AbstractAPIAspect抽象类

    package com.xxx.openapis.aspects;
    
    import com.xxx.openapis.models.User;
    import com.xxx.openapis.web.AjaxResult;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.aspectj.lang.annotation.Aspect;
    import org.slf4j.Logger;
    import org.springframework.util.StringUtils;
    
    import javax.inject.Named;
    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    import java.util.Arrays;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import static org.springframework.util.StringUtils.hasText;
    
    @Aspect
    @Named
    @Slf4j
    public class SignatureChecker extends AbstractAPIAspect {
    
        private static final int ORDER = VendorChecker.ORDER + 1;
    
        @Override
        protected Logger getLogger() {
            return log;
        }
    
        @Override
        protected AjaxResult check() {
            String callerSign = request.getHeader(X_API_SIGNATURE);
            if (!hasText(callerSign)) return AjaxResult.error(401, "未提供请求签名");
    
            User user= (User) request.getAttribute("user");
            String salt = user.getSalt();
            String sign = sign(request.getHeader(X_API_TIMESTAMP), request.getHeader(X_API_NONCE), request.getHeader(X_API_PARTNERCODE), request.getParameterMap(), salt);
            getLogger().debug("收到的签名{}", callerSign);
            getLogger().debug("计算签名{}", sign);
    
            if (!callerSign.equalsIgnoreCase(sign)) return AjaxResult.error(403, "签名错误");
            return null;
        }
    
        private String sign(final String timestamp, final String nonce, final String partnerCode, final Map<String, String[]> requestParameters, String salt) {
        	  //请求参数进行排序,按照ASCII码升序排列
            String toSign = timestamp
                    + nonce
                    + partnerCode
                    + requestParameters.entrySet()
                    .stream()
                    .map(entry -> {
                        String key = entry.getKey();
                        String value = Arrays.stream(entry.getValue())
                                .map(this::encode)
                                .filter(StringUtils::hasText)
                                .sorted()
                                .collect(Collectors.joining(","));
                        if (hasText(value)) return key + '=' + value;
                        else return null;
                    })
                    .filter(StringUtils::hasText)
                    .sorted()
                    .collect(Collectors.joining("&"))
                    + salt;
            return DigestUtils.md5Hex(toSign);
        }
    
        private String encode(String value) {
            try {
                return URLEncoder.encode(value, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        @Override
        public int getOrder() {
            return ORDER;
        }
    }
    
  8. 创建controller类

    package com.xxx.openapis.conctollers;
    
    import com.xxx.openapis.annotations.OpenAPI;
    
    import com.xxx.openapis.web.AjaxResult;
    
    
    import static com.xinnet.market.web.AjaxResult.error;
    import static com.xinnet.market.web.AjaxResult.success;
    
    @RestController
    @RequestMapping("/api")
    @OpenAPI  //此处重点 加上它相当于告诉系统先走上边的几个检查符合条件后才进控制层
    public class DistributorAPI {
        @PostMapping("/test")
        public AjaxResult updatePayStatus(@RequestParam("test") String test) String test) {		
            try {
                //调用后端业务逻辑
                return success();
            } catch (OrderNotMacherException e) {
                return error(509, e.getMessage());
            }
        }
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值