Spring Boot接口如何设计防篡改、防重放攻击

Spring Boot 防篡改、防重放攻击

本示例主要内容

  • 请求参数防止篡改攻击
  • 基于 timestamp 方案,防止重放攻击
  • 使用 swagger 接口文档自动生成

API 接口设计

API 接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数,为了防止被别有用心之人获取到真实请求参数后再次发起请求获取信息,需要采取很多安全机制。

  • 需要采用 https 方式对第三方提供接口,数据的加密传输会更安全,即便是被破解,也需要耗费更多时间
  • 需要有安全的后台验证机制,达到防参数篡改 + 防二次请求(本示例内容)

防止重放攻击必须要保证请求只在限定的时间内有效,需要通过在请求体中携带当前请求的唯一标识,并且进行签名防止被篡改,所以防止重放攻击需要建立在防止签名被串改的基础之上

防止篡改

  • 客户端使用约定好的秘钥对传输参数进行加密,得到签名值 sign1,并且将签名值存入 headers,发送请求给服务端
  • 服务端接收客户端的请求,通过过滤器使用约定好的秘钥对请求的参数(headers 除外)再次进行签名,得到签名值 sign2。
  • 服务端对比 sign1 和 sign2 的值,如果对比一致,认定为合法请求。如果对比不一致,说明参数被篡改,认定为非法请求

基于 timestamp 的方案,防止重放

每次 HTTP 请求,headers 都需要加上 timestamp 参数,并且 timestamp 和请求的参数一起进行数字签名。因为一次正常的 HTTP 请求,从发出到达服务器一般都不会超过 60s,所以服务器收到 HTTP 请求之后,首先判断时间戳参数与当前时间相比较,是否超过了 60s,如果超过了则提示签名过期(这个过期时间最好做成配置)。

一般情况下,黑客从抓包重放请求耗时远远超过了 60s,所以此时请求中的 timestamp 参数已经失效了。
如果黑客修改 timestamp 参数为当前的时间戳,则 sign 参数对应的数字签名就会失效,因为黑客不知道签名秘钥,没有办法生成新的数字签名(前端一定要保护好秘钥和加密算法)。

相关核心思路代码

过滤器

@Slf4j
@Component
/**
 * 防篡改、防重放攻击过滤器
 */
public class SignAuthFilter implements Filter {
    @Autowired
    private SecurityProperties securityProperties;
<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">init</span><span class="hljs-params">(FilterConfig filterConfig)</span> {
    log.info(<span class="hljs-string">"初始化 SignAuthFilter"</span>);
}

<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">doFilter</span><span class="hljs-params">(ServletRequest request, ServletResponse response, FilterChain filterChain)</span> <span class="hljs-keyword">throws</span> ServletException, IOException {
    <span class="hljs-comment">// 防止流读取一次后就没有了, 所以需要将流继续写出去</span>
    <span class="hljs-type">HttpServletRequest</span> <span class="hljs-variable">httpRequest</span> <span class="hljs-operator">=</span> (HttpServletRequest) request;
    <span class="hljs-type">HttpServletRequest</span> <span class="hljs-variable">requestWrapper</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">RequestWrapper</span>(httpRequest);

    Set&lt;String&gt; uriSet = <span class="hljs-keyword">new</span> <span class="hljs-title class_">HashSet</span>&lt;&gt;(securityProperties.getIgnoreSignUri());
    <span class="hljs-type">String</span> <span class="hljs-variable">requestUri</span> <span class="hljs-operator">=</span> httpRequest.getRequestURI();
    <span class="hljs-type">boolean</span> <span class="hljs-variable">isMatch</span> <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
    <span class="hljs-keyword">for</span> (String uri : uriSet) {
        isMatch = requestUri.contains(uri);
        <span class="hljs-keyword">if</span> (isMatch) {
            <span class="hljs-keyword">break</span>;
        }
    }
    log.info(<span class="hljs-string">"当前请求的URI是==&gt;{},isMatch==&gt;{}"</span>, httpRequest.getRequestURI(), isMatch);
    <span class="hljs-keyword">if</span> (isMatch) {
        filterChain.doFilter(requestWrapper, response);
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-type">String</span> <span class="hljs-variable">sign</span> <span class="hljs-operator">=</span> requestWrapper.getHeader(<span class="hljs-string">"Sign"</span>);
    <span class="hljs-type">Long</span> <span class="hljs-variable">timestamp</span> <span class="hljs-operator">=</span> Convert.toLong(requestWrapper.getHeader(<span class="hljs-string">"Timestamp"</span>));

    <span class="hljs-keyword">if</span> (StrUtil.isEmpty(sign)) {
        returnFail(<span class="hljs-string">"签名不允许为空"</span>, response);
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">if</span> (timestamp == <span class="hljs-literal">null</span>) {
        returnFail(<span class="hljs-string">"时间戳不允许为空"</span>, response);
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-comment">//重放时间限制(单位分)</span>
    <span class="hljs-type">Long</span> <span class="hljs-variable">difference</span> <span class="hljs-operator">=</span> DateUtil.between(DateUtil.date(), DateUtil.date(timestamp * <span class="hljs-number">1000</span>), DateUnit.MINUTE);
    <span class="hljs-keyword">if</span> (difference &gt; securityProperties.getSignTimeout()) {
        returnFail(<span class="hljs-string">"已过期的签名"</span>, response);
        log.info(<span class="hljs-string">"前端时间戳:{},服务端时间戳:{}"</span>, DateUtil.date(timestamp * <span class="hljs-number">1000</span>), DateUtil.date());
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-type">boolean</span> <span class="hljs-variable">accept</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    SortedMap&lt;String, String&gt; paramMap;
    <span class="hljs-keyword">switch</span> (requestWrapper.getMethod()) {
        <span class="hljs-keyword">case</span> <span class="hljs-string">"GET"</span>:
            paramMap = HttpUtil.getUrlParams(requestWrapper);
            accept = SignUtil.verifySign(paramMap, sign, timestamp);
            <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">case</span> <span class="hljs-string">"POST"</span>:
        <span class="hljs-keyword">case</span> <span class="hljs-string">"PUT"</span>:
        <span class="hljs-keyword">case</span> <span class="hljs-string">"DELETE"</span>:
            paramMap = HttpUtil.getBodyParams(requestWrapper);
            accept = SignUtil.verifySign(paramMap, sign, timestamp);
            <span class="hljs-keyword">break</span>;
        <span class="hljs-keyword">default</span>:
            accept = <span class="hljs-literal">true</span>;
            <span class="hljs-keyword">break</span>;
    }
    <span class="hljs-keyword">if</span> (accept) {
        filterChain.doFilter(requestWrapper, response);
    } <span class="hljs-keyword">else</span> {
        returnFail(<span class="hljs-string">"签名验证不通过"</span>, response);
    }
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">returnFail</span><span class="hljs-params">(String msg, ServletResponse response)</span> <span class="hljs-keyword">throws</span> IOException {
    response.setCharacterEncoding(<span class="hljs-string">"UTF-8"</span>);
    response.setContentType(<span class="hljs-string">"application/json; charset=utf-8"</span>);
    <span class="hljs-type">PrintWriter</span> <span class="hljs-variable">out</span> <span class="hljs-operator">=</span> response.getWriter();
    <span class="hljs-type">String</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> JSONObject.toJSONString(AjaxResult.fail(msg));
    out.println(result);
    out.flush();
    out.close();
}

<span class="hljs-meta">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">destroy</span><span class="hljs-params">()</span> {
    log.info(<span class="hljs-string">"销毁 SignAuthFilter"</span>);
}

}

签名验证

@Slf4j
public class SignUtil {
<span class="hljs-comment">/**
 * 验证签名
 *
 * <span class="hljs-doctag">@param</span> params
 * <span class="hljs-doctag">@param</span> sign
 * <span class="hljs-doctag">@return</span>
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">verifySign</span><span class="hljs-params">(SortedMap&lt;String, String&gt; params, String sign, Long timestamp)</span> {
    <span class="hljs-type">String</span> <span class="hljs-variable">paramsJsonStr</span> <span class="hljs-operator">=</span> <span class="hljs-string">"Timestamp"</span> + timestamp + JSONObject.toJSONString(params);
    <span class="hljs-keyword">return</span> verifySign(paramsJsonStr, sign);
}

<span class="hljs-comment">/**
 * 验证签名
 *
 * <span class="hljs-doctag">@param</span> params
 * <span class="hljs-doctag">@param</span> sign
 * <span class="hljs-doctag">@return</span>
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">verifySign</span><span class="hljs-params">(String params, String sign)</span> {
    log.info(<span class="hljs-string">"Header Sign : {}"</span>, sign);
    <span class="hljs-keyword">if</span> (StringUtils.isEmpty(params)) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }
    log.info(<span class="hljs-string">"Param : {}"</span>, params);
    <span class="hljs-type">String</span> <span class="hljs-variable">paramsSign</span> <span class="hljs-operator">=</span> getParamsSign(params);
    log.info(<span class="hljs-string">"Param Sign : {}"</span>, paramsSign);
    <span class="hljs-keyword">return</span> sign.equals(paramsSign);
}

<span class="hljs-comment">/**
 * <span class="hljs-doctag">@return</span> 得到签名
 */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">getParamsSign</span><span class="hljs-params">(String params)</span> {
    <span class="hljs-keyword">return</span> DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
}

}

不做签名验证的接口做成配置 (application.yml)

spring:
  security:
    # 签名验证超时时间
    signTimeout: 300
    # 允许未签名访问的 url 地址
    ignoreSignUri:
      - /swagger-ui.html
      - /swagger-resources
      - /v2/api-docs
      - /webjars/springfox-swagger-ui
      - /csrf

属性代码(SecurityProperties.java)

@Component
@ConfigurationProperties(prefix = "spring.security")
@Data
public class SecurityProperties {
<span class="hljs-comment">/**
 * 允许忽略签名地址
 */</span>
List&lt;String&gt; ignoreSignUri;

<span class="hljs-comment">/**
 * 签名超时时间(分)
 */</span>
Integer signTimeout;

}

签名测试控制器

@RestController
@Slf4j
@RequestMapping("/sign")
@Api(value = "签名 controller", tags = {"签名测试接口"})
public class SignController {
<span class="hljs-meta">@ApiOperation("get测试")</span>
<span class="hljs-meta">@ApiImplicitParams({
        @ApiImplicitParam(name = "username", value = "用户名", required = true, dataType = "String"),
        @ApiImplicitParam(name = "password", value = "密码", required = true, dataType = "String")
})</span>
<span class="hljs-meta">@GetMapping("/testGet")</span>
<span class="hljs-keyword">public</span> AjaxResult <span class="hljs-title function_">testGet</span><span class="hljs-params">(String username, String password)</span> {
    log.info(<span class="hljs-string">"username:{},password:{}"</span>, username, password);
    <span class="hljs-keyword">return</span> AjaxResult.success(<span class="hljs-string">"GET参数检验成功"</span>);
}

<span class="hljs-meta">@ApiOperation("post测试")</span>
<span class="hljs-meta">@ApiImplicitParams({
        @ApiImplicitParam(name = "data", value = "测试实体", required = true, dataType = "TestVo")
})</span>
<span class="hljs-meta">@PostMapping("/testPost")</span>
<span class="hljs-keyword">public</span> AjaxResult&lt;TestVo&gt; <span class="hljs-title function_">testPost</span><span class="hljs-params">(<span class="hljs-meta">@Valid</span> <span class="hljs-meta">@RequestBody</span> TestVo data)</span> {
    <span class="hljs-keyword">return</span> AjaxResult.success(<span class="hljs-string">"POST参数检验成功"</span>, data);
}

<span class="hljs-meta">@ApiOperation("put测试")</span>
<span class="hljs-meta">@ApiImplicitParams({
        @ApiImplicitParam(name = "id", value = "编号", required = true, dataType = "Integer"),
        @ApiImplicitParam(name = "data", value = "测试实体", required = true, dataType = "TestVo")
})</span>
<span class="hljs-meta">@PutMapping("/testPut/{id}")</span>
<span class="hljs-keyword">public</span> AjaxResult <span class="hljs-title function_">testPut</span><span class="hljs-params">(<span class="hljs-meta">@PathVariable</span> Integer id, <span class="hljs-meta">@RequestBody</span> TestVo data)</span> {
    data.setId(id);
    <span class="hljs-keyword">return</span> AjaxResult.success(<span class="hljs-string">"PUT参数检验成功"</span>, data);
}

<span class="hljs-meta">@ApiOperation("delete测试")</span>
<span class="hljs-meta">@ApiImplicitParams({
        @ApiImplicitParam(name = "idList", value = "编号列表", required = true, dataType = "List&lt;Integer&gt; ")
})</span>
<span class="hljs-meta">@DeleteMapping("/testDelete")</span>
<span class="hljs-keyword">public</span> AjaxResult <span class="hljs-title function_">testDelete</span><span class="hljs-params">(<span class="hljs-meta">@RequestBody</span> List&lt;Integer&gt; idList)</span> {
    <span class="hljs-keyword">return</span> AjaxResult.success(<span class="hljs-string">"DELETE参数检验成功"</span>, idList);
}

}

前端 js 请求示例

var settings = {
  "async": true,
  "crossDomain": true,
  "url": "http://localhost:8080/sign/testGet?username=abc&password=123",
  "method": "GET",
  "headers": {
    "Sign": "46B1990701BCF090E3E6E517751DB02F",
    "Timestamp": "1564126422",
    "User-Agent": "PostmanRuntime/7.15.2",
    "Accept": "*/*",
    "Cache-Control": "no-cache",
    "Postman-Token": "a9d10ef5-283b-4ed3-8856-72d4589fb61d,6e7fa816-000a-4b29-9882-56d6ae0f33fb",
    "Host": "localhost:8080",
    "Cookie": "SESSION=OWYyYzFmMDMtODkyOC00NDg5LTk4ZTYtODNhYzcwYjQ5Zjg2",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "keep-alive",
    "cache-control": "no-cache"
  }
}

$.ajax(settings).done(function (response) {
console.log(response);
});

注意事项

  • 该示例没有设置秘钥, 只做了参数升排然后创建 md5 签名
  • 示例请求的参数 md5 原文本为:Timestamp1564126422
  • 注意 headers 请求头带上了 Sign 和 Timestamp 参数
  • js 读取的 Timestamp 必须要在服务端获取
  • 该示例不包括分布试环境下, 多台服务器时间同步问题

自动生成接口文档

  • 配置代码
@Configuration
@EnableSwagger2
public class Swagger2Config {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.easy.sign"))
                .paths(PathSelectors.any())
                .build();
    }
<span class="hljs-comment">//构建 api文档的详细信息函数,注意这里的注解引用的是哪个</span>
<span class="hljs-keyword">private</span> ApiInfo <span class="hljs-title function_">apiInfo</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ApiInfoBuilder</span>()
            .title(<span class="hljs-string">"签名示例"</span>)
            .contact(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Contact</span>(<span class="hljs-string">"签名示例网站"</span>, <span class="hljs-string">"http://www.baidu.com"</span>, <span class="hljs-string">"test@qq.com"</span>))
            .version(<span class="hljs-string">"1.0.0"</span>)
            .description(<span class="hljs-string">"签名示例接口描述"</span>)
            .build();
}

}

资料