【JAVA】实现API 接口参数签名

使用sa-token+SpringBoot+拦截器实现API 接口参数签名

在涉及跨系统接口调用时,我们容易碰到以下安全问题:
1.请求身份被伪造。
2.请求参数被篡改。
3.请求被抓包,然后重放攻击。

1.引入 sa-token
sa-token官方文档:https://sa-token.cc/doc.html#/

<dependency>
    <groupId>cn.dev33groupId>
    <artifactId>sa-token-spring-boot-starterartifactId>
    <version>1.35.0.RCversion>
dependency>

2.配置密钥

请求发起端和接收端需要配置一个相同的秘钥,在 application.yml 中配置

# 开发接口密钥配置
sa-token:
  sign:
    # API 接口签名秘钥
    secret-key: 8ba6126f-3921-4eca-8f1b-451aa38a563b

3.重写HttpServletRequestWrapper类

方便获取请求头的参数,包括@RequestBody注解接受的参数

package com.xhs.interceptor;

import org.apache.commons.io.IOUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @desc: 保存请求体参数的内容
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 19:06
 * @version: JDK 1.8
 */
public class RequestWrapper extends HttpServletRequestWrapper {

    /**
     * 保存请求体参数
     */
    private final String body;
    /**
     * 保存其他类型的参数
     */
    private final Map<String, String[]> parameterMap;

    /**
     * 获取参数
     *
     * @param request request
     * @throws IOException IOException
     */
    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 获取请求体参数
        body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
        // 获取其他类型的参数
        parameterMap = new HashMap<>(request.getParameterMap());
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        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 String getParameter(String name) {
        String[] values = parameterMap.get(name);
        if (values != null && values.length > 0) {
            return values[0];
        }
        return null;
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return parameterMap;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return Collections.enumeration(parameterMap.keySet());
    }

    @Override
    public String[] getParameterValues(String name) {
        return parameterMap.get(name);
    }

    /**
     * 获取请求体参数
     *
     * @return String
     */
    public String getBody() {
        return body;
    }
}

4.创建签名校验的拦截器 SignInterceptor

校验请求的参数是否有效

package com.xhs.interceptor;

import cn.dev33.satoken.sign.SaSignUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * @desc: 签名校验的拦截器
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 17:56
 * @version: JDK 1.8
 */
@Slf4j
public class SignInterceptor implements HandlerInterceptor {

    /**
     * 创建一个签名校验的拦截器
     */
    public SignInterceptor() {

    }

    /**
     * 每次请求之前触发的方法
     *
     * @param request  request
     * @param response response
     * @param handler  handler
     * @return boolean
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果是OPTIONS请求,让其响应一个 200状态码,说明可以正常访问
        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            // 放行OPTIONS请求
            return true;
        }
        // 保存传递过来的参数
        Map<String, String> map = new HashMap<>(16);

        // 在拦截器中获取处理方法的参数,并检查是否带有 @RequestBody 注解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
        for (MethodParameter parameter : methodParameters) {
            if (parameter.hasParameterAnnotation(RequestBody.class)) {
                // 参数带有 @RequestBody 注解
                // 获取请求体参数
                RequestWrapper requestWrapper = new RequestWrapper(request);
                String requestBody = requestWrapper.getBody();
                if (StringUtils.hasLength(requestBody)) {
                    JSONObject jsonObject = JSONObject.parseObject(requestBody);
                    map = JSON.parseObject(jsonObject.toJSONString(), HashMap.class);
                    log.info("请求体参数map:{}", map);
                }
            } else {
                // 参数不带 @RequestBody 注解
                // 非请求体参数
                Enumeration<String> parameterNames = request.getParameterNames();
                // 遍历参数名,并获取对应的参数值
                while (parameterNames.hasMoreElements()) {
                    String paramName = parameterNames.nextElement();
                    String paramValue = request.getParameter(paramName);
                    // 处理动态参数,如打印参数名和参数值
                    map.put(paramName, paramValue);
                }
                log.info("非请求体参数map:{}", map);
            }
        }
        // 1、校验请求中的签名
        SaSignUtil.checkParamMap(map);
        return true;
    }
}

5.使用拦截器

配置那些接口需要校验参数签名

package com.xhs.filter;

import com.xhs.interceptor.SignInterceptor;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @desc: 检查签名过滤器
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 15:17
 * @version: JDK 1.8
 */
@Configuration
public class SignFilter implements WebMvcConfigurer {

    /**
     * 注册拦截器
     *
     * @param registry
     * @return void
     */
    @Override
    public void addInterceptors(@NotNull InterceptorRegistry registry) {
        // 校验规则为
        registry.addInterceptor(new SignInterceptor())
                //需要校验的接口
                .addPathPatterns("/tools/getUser","/tools/getName")
                // 不需要校验的接口
                .excludePathPatterns();
    }
}

6.创建HttpServletRequestFilter过滤器

解决@RequestBody注解接受的参数,校验完签名后报:
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:
异常

package com.xhs.filter;


import com.xhs.interceptor.RequestWrapper;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @desc: 过滤器
 * 解决:在拦截器中获取body后,接口报错:Required request body is missing
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 20:07
 * @version: JDK 1.8
 */
@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
public class HttpServletRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String contentType = request.getContentType();
        String method = "multipart/form-data";

        if (contentType != null && contentType.contains(method)) {
            // 将转化后的 request 放入过滤链中
            request = new StandardServletMultipartResolver().resolveMultipart(request);
        }
        request = new RequestWrapper((HttpServletRequest) servletRequest);
        //获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中
        // 在chain.doFiler方法中传递新的request对象
        if (request == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(request, servletResponse);
        }
    }
}

7.创建生成签名的方法
7.1 controller层代码

package com.xhs.controller;

import com.xhs.message.ReturnResult;
import com.xhs.service.SignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Map;

/**
 * @desc: 生成签名
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 16:03
 * @version: JDK 1.8
 */
@Slf4j
@RestController
public class SignController {

    @Resource
    private SignService signService;

    /**
     * 生成签名 参数拼接到url后面
     *
     * @param paramsMap 参数
     * @return ReturnResult
     */
    @PostMapping("/signGet")
    public ReturnResult<Object> signGet(@RequestBody Map<String, Object> paramsMap) {
        return signService.signGet(paramsMap);
    }

    /**
     * 生成签名 JSON格式的参数
     *
     * @param paramsMap 参数
     * @return ReturnResult
     */
    @PostMapping("/signPost")
    public ReturnResult<Object> signPost(@RequestBody Map<String, Object> paramsMap) {
        return signService.signPost(paramsMap);
    }
}


7.2 service层代码

package com.xhs.service;

import com.xhs.message.ReturnResult;

import java.util.Map;

/**
 * @desc:
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 16:36
 * @version: JDK 1.8
 */
public interface SignService {

    /**
     * 生成签名 GET请求方式
     *
     * @param paramsMap 参数
     * @return ReturnResult
     */
    ReturnResult<Object> signGet(Map<String, Object> paramsMap);

    /**
     * 生成签名 POST请求方式
     *
     * @param paramsMap 参数
     * @return ReturnResult
     */
    ReturnResult<Object> signPost(Map<String, Object> paramsMap);
}


7.3 service实现层代码

package com.xhs.service.impl;

import cn.dev33.satoken.sign.SaSignUtil;
import com.xhs.message.Result;
import com.xhs.message.ReturnResult;
import com.xhs.service.SignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.Map;

/**
 * @desc:
 * @projectName: java-tools-parent
 * @author: xhs
 * @date: 2023-8-27 027 16:36
 * @version: JDK 1.8
 */
@Slf4j
@Service
public class SignServiceImpl implements SignService {

    /**
     * 生成签名 POST请求方式
     *
     * @param paramsMap 参数
     * @return ReturnResult
     */
    @Override
    public ReturnResult<Object> signGet(Map<String, Object> paramsMap) {
        log.info("生成签名的入参-paramsMap:{}", paramsMap);
        String signParams = SaSignUtil.addSignParamsAndJoin(paramsMap);
        log.info("生成签名后的参数-signParams:{}", signParams);
        return ReturnResult.build(Result.SUCCESS).setData(signParams);
    }

    /**
     * 生成签名 POST请求方式
     *
     * @param paramsMap 参数
     * @return ReturnResult
     */
    @Override
    public ReturnResult<Object> signPost(Map<String, Object> paramsMap) {
        log.info("生成签名的入参-paramsMap:{}", paramsMap);
        Map<String, Object> map = SaSignUtil.addSignParams(paramsMap);
        log.info("生成签名后的参数-signParams:{}", map);
        return ReturnResult.build(Result.SUCCESS).setData(map);
    }
}

8.调用接口并校验签名是否合法
8.1 GET请求参数拼接到url后面

http://127.0.0.1:1000/tools/getName?name=admin×tamp=1693145776820&nonce=kaTqdadO4u04hZG0gekEIvXmeN5QZD8A&sign=4b5f414e24290ed7766c2d79910264a7

【JAVA】实现API 接口参数签名_第1张图片

【JAVA】实现API 接口参数签名_第2张图片
8.2 POST请求 使用@RequestBody接受参数
【JAVA】实现API 接口参数签名_第3张图片
【JAVA】实现API 接口参数签名_第4张图片
注意

使用@RequestBody接受参数需要创建过滤器将请求体内容传递给下一个处理器,否则会报错
org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing:

在这里插入图片描述
解决方法:参照 第五步”5.创建HttpServletRequestFilter过滤器“

9.源码地址
https://gitee.com/xhs101/java-tools-parent

你可能感兴趣的:(java)