springboot自定义组件--自定义防重放防篡改组件实现对外提供第三方api安全策略

该博客介绍了如何在API接口中实现安全防护,包括采用HTTPS进行数据加密传输,以及通过签名验证和防重放攻击机制来确保请求的完整性和一次性。签名验证涉及客户端和服务器端使用相同的秘钥对参数加密和比较签名,防止参数篡改。防重放攻击则提出了基于timestamp和nonce的方案,确保请求仅能使用一次,有效防止恶意重放攻击。同时,文章提供了具体的Java代码实现,包括注解、参数构造器、校验规则和配置参数等,展示了如何在实际开发中应用这些安全策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 引言

在日常开发中我们会向第三提供api接口,所以必须暴露到外网,并提供了具体请求地址和请求参数,为了防止被第别有用心之人获取到真实请求参数后再次发起请求获取信息,需要采取很多安全机制;
安全策略

  • 1.首先: 需要采用https方式对第三方提供接口,数据的加密传输会更安全,即便是被破解,也需要耗费更多时间
  • 2.其次:需要有安全的后台验证机制【本文重点】,达到防参数篡改+防二次请求,防止重放攻击必须要保证请求仅一次有效;

防参数篡改

客户端使用约定好的秘钥对传输参数进行加密,得到签名值signature,并且将签名值也放入请求参数中,发送请求给服务端

服务端接收客户端的请求,然后使用约定好的秘钥对请求的参数(除了signature以外)再次进行签名,得到签名值autograph。

服务端对比signature和autograph的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定为非法请求

防二次请求

  • 基于timestamp的方案
  • 基于nonce的方案
  • 基于timestamp和nonce的方案(推荐)
    在这里插入图片描述

二 代码实现

定义校验注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface XlcpAntiReplay {
    /**
     * 是否检查签名
     * @return
     */
    boolean checkSignature() default true;

    /**
     * 反重放校验
     * @return
     */
    boolean antiReplay() default true;
}

定义参数构造器及相应的校验规则(构造者模式)

@Slf4j
public class XlcpSignatureBuilder {
    /**
     * 请求标识
     */
    private String nonce;
    /**
     * 请求时间
     */
    private Long timestamp;
    /**
     * 请求URL
     */
    private String url;
    /**
     * 请求URL
     */
    private String token;
    /**
     * 请求参数
     */
    private Object[] arguments;
    /**
     * 请求参数
     */
    private String params;
    /**
     * 请求URL
     */
    private String signature;

    /**
     * 盐值
     */
    private String salt;

    private XlcpSignatureBuilder nonce(String nonce) {
        Assert.notNull(nonce, "请求标识不能为空");
        this.nonce = nonce;
        return this;
    }

    private XlcpSignatureBuilder timestamp(Long timestamp) {
        this.timestamp = timestamp;
        return this;
    }

    private XlcpSignatureBuilder url(String url) {
        Assert.notNull(url, "请求URL不能为空");
        this.url = url;
        return this;
    }

    private XlcpSignatureBuilder token(String token) {
        this.token = token;
        return this;
    }

    private XlcpSignatureBuilder arguments(Object[] arguments) {
        this.arguments = arguments;
        return this;
    }

    private XlcpSignatureBuilder params(Map<String, String[]> parameterMap) {
        this.params = JSONUtil.toJsonStr(parameterMap);
        return this;
    }

    private XlcpSignatureBuilder signature(String signature) {
        Assert.notNull(signature, "签名摘要不能为空");
        this.signature = signature;
        return this;
    }

    private XlcpSignatureBuilder salt(String salt) {
        Assert.notNull(salt, "盐值不能为空");
        this.salt = salt;
        return this;
    }

    public static XlcpSignatureBuilder build(HttpServletRequest request, XlcpReplayProperties properties){
        XlcpReplayProperties.HeaderKey headerKey = properties.getHeaderKey();
        XlcpReplayProperties.SignatureAlgorithm signatureAlgorithm = properties.getSignatureAlgorithm();
        XlcpSignatureBuilder signatureBuilder = new XlcpSignatureBuilder();
        signatureBuilder.nonce(request.getHeader(headerKey.getNonce()))
                .timestamp(Convert.toLong(request.getHeader(headerKey.getTimestamp())))
                .url(request.getHeader(headerKey.getUrl()))
                .token(request.getHeader(headerKey.getToken()))
                .params(request.getParameterMap())
                .signature(request.getHeader(headerKey.getSignature()))
                .salt(signatureAlgorithm.getSalt());
        return signatureBuilder;
    }

    public void validate(){
        String digest =
                MdFiveUtils.digest(
                        this.salt,this.nonce, this.url, this.timestamp, this.token, this.params, this.arguments);

        if (!StrUtil.equals(this.signature, digest)) {
            if (log.isDebugEnabled()) {
                log.debug("数据签名验证未通过, 传入签名:[ {} ], 生成签名:[ {} ]", signature, digest);
            }
            throw new DataSignatureException("data signature verification failed");
        }
    }


}


@Slf4j
public class XlcpAntiReplayBuilder implements AutoCloseable{
    private static final Long VERSION_INCREMENT_STEP = 1L;
    /**
     * 请求方法名称
     */
    private String methodName;
    /**
     * 请求路径 (请求头)
     */
    private String url;

    /**
     * 目标url
     */
    private String targetUrl;

    /**
     * 请求标识
     */
    private String nonce;

    /**
     * 请求时间戳
     */
    private Long timestamp;

    /**
     * redis版本
     */
    private Long version;


    private XlcpAntiReplayBuilder methodName(String methodName){
        Assert.notNull(methodName,"请求方法不能为空!");
        this.methodName=methodName;
        return this;
    }

    private XlcpAntiReplayBuilder url(String url){
        Assert.notNull(url,"URL不能为空");
        this.url = startAt(url, "/");

        return this;
    }

    private XlcpAntiReplayBuilder targetUrl(String targetUrl){
        Assert.notNull(url,"targetUrl不能为空");
        this.targetUrl = startAt(url, "/");
        return this;
    }

    private XlcpAntiReplayBuilder nonce(String nonce){
        Assert.notNull(nonce,"请求标识不能为空");
        this.nonce=nonce;
        return this;
    }

    private XlcpAntiReplayBuilder timestamp(Long timestamp){
        Assert.notNull("请求时间戳不能为空");
        this.timestamp=timestamp;
        return this;
    }

    private String startAt(String str, CharSequence prefix){
        return StrUtil.startWith(str,prefix)?str:prefix+str;
    }

    public static XlcpAntiReplayBuilder build(HttpServletRequest request, ProceedingJoinPoint point, String contextPath, XlcpReplayProperties props){
        XlcpAntiReplayBuilder xlcpAntiReplayBuilder = new XlcpAntiReplayBuilder();
        xlcpAntiReplayBuilder.methodName(SpringUtils.getMethodName(point))
                .nonce(request.getHeader(props.getHeaderKey().getNonce()))
                .url(request.getHeader(props.getHeaderKey().getUrl()))
                .targetUrl(StrUtil.replace(request.getRequestURI(),contextPath,""))
                .timestamp(Convert.toLong(request.getHeader(props.getHeaderKey().getTimestamp())));
        return xlcpAntiReplayBuilder;
    }

    public void validate(){
        if (!StrUtil.equals(this.targetUrl,this.url)){
            throw new AntiReplayException("请求url与实际url不相符");
        }

        XlcpReplayProperties props = SpringContextHolder.getBean(XlcpReplayProperties.class);

        if (DateTimeUtils.betweenNowSeconds(this.timestamp)>props.getRequest().getExpireTime()){
            throw new AntiReplayException("请求已过期");
        }
        String key = genKey(props);

        this.version = RedisUtil.increment(key);

        RedisUtil.expire(key,props.getCache().getLockHoldTime(), TimeUnit.SECONDS);

        if (version>VERSION_INCREMENT_STEP){
            throw new AntiReplayException("当前请求正在处理中,请不要重复提交");
        }
    }

    private String genKey(XlcpReplayProperties props){
        return props.getCache().getCacheKeyPrefix()+this.methodName+ StrPool.COLON+this.nonce;
    }

    @Override
    public void close() throws Exception {
        log.debug("删除缓存");
        String key = genKey(SpringContextHolder.getBean(XlcpReplayProperties.class));
        if (RedisUtil.exists(key)){
            RedisUtil.remove(key);
        }
    }
}

定义配置参数

@ConfigurationProperties(prefix = XlcpReplayProperties.PRE_FIX)
@Data
public class XlcpReplayProperties {
    /**
     * Request Header信息对象
     */
    private HeaderKey headerKey = new HeaderKey();
    /**
     * 请求配置
     */
    private Request request = new Request();
    /**
     * 缓存配置
     */
    private Cache cache = new Cache();

    private SignatureAlgorithm signatureAlgorithm = new SignatureAlgorithm();
    /**
     * 配置前缀
     */
    public static final String PRE_FIX = "config.anti.replay";


    @Data
    public class HeaderKey{
        /**
         * 请求id防止重放
         */
        private String nonce ="nonce";

        /**
         * 请求时间 避免缓存时间过后重放
         */
        private String timestamp = "timestamp";

        /**
         * 请求url
         */
        private String url = "url";

        /**
         * Token
         */
        private String token = "token";

        /**
         * 签名
         */
        private String signature = "signature";
    }

    @Data
    public class Request{
        /**
         *  请求有效期
         */
        private Long expireTime = 60L;
    }

    @Data
    public class Cache {
        /**
         * 缓存Key前缀
         */
        private String cacheKeyPrefix = "XLCP:SECURITY:ANTI-REPLAY:REQUEST_ID_";
        /**
         * 锁持续时间(避免异常造成锁不释放)
         */
        private long lockHoldTime = 300L;
    }

    @Data
    public class SignatureAlgorithm{
        /**
         * 加密盐值
         */
        private String salt = "XLCP_ANTI_REPLAY";
    }

}

定义AOP切面

@Aspect
@RequiredArgsConstructor
public class XlcpAntiReplayAspect {
    private final XlcpReplayProperties properties;

    private final Environment environment;

    public static final String CONTEX_TPATH_PRE = "server.servlet.context-path";

    @Around("@annotation(xlcpAntiReplay)")
    @SneakyThrows
    public Object around(ProceedingJoinPoint point, XlcpAntiReplay xlcpAntiReplay){
        HttpServletRequest request = WebUtils.getRequest();
        // 签名校验
        if (xlcpAntiReplay.checkSignature()){
            XlcpSignatureBuilder
                    .build(request,properties)
                    .validate();
        }
        // 反重放校验
       if (xlcpAntiReplay.antiReplay()){
           String servletContextPath = environment.getProperty(CONTEX_TPATH_PRE);
           try(XlcpAntiReplayBuilder xlcpAntiReplayBuilder = XlcpAntiReplayBuilder
                   .build(request,point,servletContextPath,properties)) {
               xlcpAntiReplayBuilder.validate();
           }
           return point.proceed();
       }
       return point.proceed();
    }
}

使用到的工具类

public class DateTimeUtils {

    public DateTimeUtils() {
    }

    public static long betweenNowSeconds(Long timestamp) {
        return betweenNowSeconds(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()));
    }

    public static long betweenNowSeconds(LocalDateTime time) {
        return ChronoUnit.SECONDS.between(time, LocalDateTime.now());
    }
}

public class MdFiveUtils {

    private static final String EMPTY_JSON = "{}";

    public static String digest(DigestWorker worker) {
        return digest(null, worker.nonce, worker.url, worker.timestamp, worker.token, worker.params, worker.arguments);
    }

    public static String digest(
            final String salt,
            final String nonce,
            final String url,
            final Long timestamp,
            final String token,
            final Object... arguments) {
        return digest(salt, nonce, url, timestamp, token, null, arguments);
    }

    public static String digest(
            final String salt,
            final String nonce,
            final String url,
            final Long timestamp,
            final String token,
            final String params,
            final Object... arguments) {

        Assert.notBlank(nonce, "nonce不能为空");
        Assert.notBlank(url, "url不能为空");

        StringBuilder sb = new StringBuilder(salt + nonce + url);

        if (!Objects.isNull(timestamp)) {
            sb.append(timestamp);
        }

        if (!Objects.isNull(token)) {
            sb.append(token);
        }

        if (StrUtil.isNotBlank(params) && !StrUtil.equals(params, EMPTY_JSON)) {
            sb.append(params);
        }
        sb.append(argumentsSort(arguments));

        return DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8));
    }

    private static String argumentsSort(final Object... arguments) {
        if (arguments != null && arguments.length > 0) {
            List<Object> list =
                    Arrays.stream(arguments)
                            .sorted(Comparator.comparing(Object::hashCode))
                            .collect(Collectors.toList());

            char[] chars = JSONUtil.toJsonStr(list).toCharArray();
            Arrays.sort(chars);
            return Arrays.toString(chars);
        }

        return "";
    }

    public static DigestWorker builder() {
        return new DigestWorker();
    }

    @Data
    public static class DigestWorker {
        private String nonce;
        private String url;
        private Long timestamp;
        private String token;
        private String params;
        private Object[] arguments;

        public DigestWorker nonce(String nonce) {
            this.nonce = nonce;
            return this;
        }

        public DigestWorker url(String url) {
            this.url = url;
            return this;
        }

        public DigestWorker timestamp(Long timestamp) {
            this.timestamp = timestamp;
            return this;
        }

        public DigestWorker token(String token) {
            this.token = token;
            return this;
        }

        public DigestWorker params(String params) {
            this.params = params;
            return this;
        }

        public DigestWorker arguments(Object... arguments) {
            this.arguments = arguments;
            return this;
        }

        public String execute() {
            return MdFiveUtils.digest(this);
        }
    }
}
public class SpringUtils {

  @SneakyThrows
  public static Method getMethod(ProceedingJoinPoint point) {
    Signature signature = point.getSignature();
    MethodSignature ms = (MethodSignature) signature;
    return point.getTarget().getClass().getMethod(ms.getName(), ms.getParameterTypes());
  }

  public static String getMethodName(ProceedingJoinPoint point) {
    return getClass(point).getName() + '.' + getMethod(point).getName();
  }

  public static Class<?> getClass(ProceedingJoinPoint point) {
    return point.getTarget().getClass();
  }

  public static HttpServletRequest getHttpServletRequest() {
    return getServletRequestAttributes().getRequest();
  }

  public static ServletRequestAttributes getServletRequestAttributes() {
    return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  }
}

添加springboot自动配置管理

@Configuration
@EnableConfigurationProperties(XlcpReplayProperties.class)
@RequiredArgsConstructor
public class XlcpAntiReplayAutoConfiguration {

    private final Environment environment;

    @Bean
    public XlcpAntiReplayAspect xlcpAntiReplayAspect(XlcpReplayProperties xlcpReplayProperties){
        return new XlcpAntiReplayAspect(xlcpReplayProperties,environment);
    }
}

在这里插入图片描述

三 测试案列

注解使用

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/sendTest")
    @Inner(value = false)
    @XlcpAntiReplay
    public R sendTest(String userName){
        return R.ok(userName);
    }

    @PostMapping("/testPost")
    @Inner(value = false)
    @XlcpAntiReplay
    public R testPost(@RequestBody SysUser sysUser){
        return R.ok(sysUser);
    }
}

第三方使用

@Slf4j
public class XlcpAntiReplayTest {
    @Test
    public void testAntiReplay(){
        HttpRequest request = HttpUtil.createGet("localhost:4000/test/sendTest");
        // 请求标识
        String nonce = UUID.randomUUID().toString();
        request.header("nonce", nonce);
        long timestamp = System.currentTimeMillis();
        request.header("timestamp", Convert.toStr(timestamp));
        String url = request.getUrl();
        request.header("url",url);
        // 系统配置的盐
        String salt = "XLCP_ANTI_REPLAY";
        HashMap<String, Object> paramMap = new HashMap<>();
        paramMap.put("userName","lktest");
        request.form(paramMap);
        String signature = MdFiveUtils.digest(salt, nonce, url, timestamp, null, null);

        request.header("signature",signature);


        HttpResponse response = request.execute();
        log.info("响应结果:{}",response);

    }
    @Test
    public void testPost(){
        HttpRequest request = HttpUtil.createPost("localhost:4000/test/testPost");

        // 请求标识
        String nonce = UUID.randomUUID().toString();
        request.header("nonce", nonce);
        long timestamp = System.currentTimeMillis();
        request.header("timestamp", Convert.toStr(timestamp));
        String url = request.getUrl();
        request.header("url",url);
        // 系统配置的盐
        String salt = "XLCP_ANTI_REPLAY";

        String signature = MdFiveUtils.digest(salt, nonce, url, timestamp, null, null);

        request.header("signature",signature);

        HashMap<String, Object> map = new HashMap<>();
        map.put("userName","lktest");

        request.body(JSONUtil.toJsonStr(map));

        HttpResponse response = request.execute();

        log.info("响应结果:{}",response);
    }
}
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值