springboot学习(七十) 使用API接口签名验证权限

前言

API接口签名验证分三步:
1、给应用分配对应的secret
2、发送请求时生成签名。按照请求参数名称将所有请求参数按照字母先后顺序排序,包括请求体和URL中的参数,生成参数的Map,Map中添加时间戳,将secret加在参数字符串的头部后进行MD5加密 ,即得到签名Sign,放入请求参数中。
3、服务端收到请求后验证签名。跟请求时一样,获取请求体和URL中参数并排序生成Map,获取时间戳参数,判断时间是否超过阈值,超过设定阈值此签名失效,Map中删除签名,添加secret做MD5加密生成签名,与Map中删除的签名对比是否一致,如果一致验证就通过了。

1、验证签名和生成签名的工具类

package com.iscas.base.biz.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.iscas.common.web.tools.json.JsonUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpMethod;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zhuquanwen
 * @vesion 1.0
 * @date 2022/2/21 14:11
 * @since jdk1.8
 */
public class ApiSignUtils {
    public static String DEFAULT_SECRET_KEY = "ISCAS123";

    private static String TIME_STAMP_KEY = "timeStamp";
    private static String SIGN_KEY = "sign";
    //超时时效,超过此时间认为签名过期
    private static Long EXPIRE_TIME = 5 * 60 * 1000L;


    /**
     * 生成签名
     */
    public static Map getSignature(Object param, String secretKey) {
        ObjectMapper objectMapper = new ObjectMapper();
        Map params;
        try {
            String jsonStr = objectMapper.writeValueAsString(param);
            params = objectMapper.readValue(jsonStr, Map.class);

        } catch (Exception e) {
            throw new RuntimeException("生成签名:转换json失败");
        }

        if (params.get(TIME_STAMP_KEY) == null) {
            params.put(TIME_STAMP_KEY, System.currentTimeMillis());
        }
        //对map参数进行排序生成参数
        Set<String> keysSet = params.keySet();
        Object[] keys = keysSet.toArray();
        Arrays.sort(keys);
        String temp = Arrays.stream(keys).map(key -> key + "=" + (!params.containsKey(key) ? "" : params.get(key)))
                .collect(Collectors.joining("&"));
        //根据参数生成签名
        String sign = DigestUtils.sha256Hex(temp + secretKey).toUpperCase();
        params.put(SIGN_KEY, sign);
        return params;
    }

    /**
     * 校验签名
     */
    public static boolean checkSignature(HttpServletRequest request, String secretKey) throws IOException {
        //获取request中的json参数转成map
        Map<String, Object> param = getAllParams(request);

        String sign = (String) param.get(SIGN_KEY);
        Long start = convertTimestamp(param.get(TIME_STAMP_KEY));
        long now = System.currentTimeMillis();
        //校验时间有效性
        if (start == null || now - start > EXPIRE_TIME || start - now > 0L) {
            return false;
        }
        //是否携带签名
        if (StringUtils.isBlank(sign)) {
            return false;
        }
        //获取除签名外的参数
        param.remove(SIGN_KEY);
        //校验签名
        Map paramMap = getSignature(param, secretKey);
        String signature = (String) paramMap.get("sign");
        if (sign.equals(signature)) {
            return true;
        }
        return false;
    }

    private static Long convertTimestamp(Object timestampObj) {
        return timestampObj != null ? Long.parseLong(timestampObj.toString()) : null;
    }


    /**
     * 将URL的参数和body参数合并
     *
     * @param request
     * @author show
     * @date 14:24 2019/5/29
     */
    public static SortedMap<String, Object> getAllParams(HttpServletRequest request) throws IOException {

        SortedMap<String, Object> result = new TreeMap<>();
        //获取URL上的参数,并放入结果中
        result.putAll(getUrlParams(request));

        // get请求和DELETE请求不需要拿body参数
        if (!StringUtils.equalsAny(request.getMethod(), HttpMethod.GET.name(), HttpMethod.DELETE.name())) {
            Optional.ofNullable(getBodyParams(request))
                    .ifPresent(result::putAll);
        }
        return result;
    }

    /**
     * 获取 Body 参数
     *
     * @param request
     * @author show
     * @date 15:04 2019/5/30
     */
    public static Map<String, Object> getBodyParams(final HttpServletRequest request) throws IOException {

        BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
        String str = "";
        StringBuilder wholeStr = new StringBuilder();
        //一行一行的读取body体里面的内容;
        while ((str = reader.readLine()) != null) {
            wholeStr.append(str);
        }
        //转化成json对象
        return StringUtils.isNoneBlank(wholeStr) ? JsonUtils.fromJson(wholeStr.toString(), Map.class) : new HashMap<>();
    }

    /**
     * 将URL请求参数转换成Map
     */
    public static Map<String, String> getUrlParams(HttpServletRequest request) {
        return Optional.ofNullable(request.getQueryString())
                .map(queryStr -> URLDecoder.decode(request.getQueryString(), StandardCharsets.UTF_8))
                .map(params -> Arrays.stream(params.split("&"))
                        .collect(Collectors.toMap(s -> s.substring(0, s.indexOf("=")), s -> s.substring(s.indexOf("=") + 1))))
                .orElse(new HashMap<>());

    }


}

其中JsonUtils是自定义的一个Jackson工具类,可以换成Jackson的ObjectMapper,效果一样,DigestUtils、StringUtils都是apache的工具包。

2、使用过滤器验证签名

@Component
public class ApiSignFilterTest extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        String url = StringUtils.substringAfter(requestURI, request.getContextPath());
        if (StringUtils.equalsAny(url, "/api/sign/t1", "/api/sign/t2")) {
            if (!ApiSignUtils.checkSignature(request, ApiSignUtils.DEFAULT_SECRET_KEY)) {
                throw new BaseRuntimeException("验证接口签名失败");
            }
        }
        chain.doFilter(request, response);
    }
}

3、请求体复用的问题

按照上面的测试,在过滤器中获取了HttpServeletRequest请求体中的内容,那么在Controller中就无法再获取此请求体的内容了。因为其InputStream不可重复读,可以替换HttpServletRequest,修改方式可参考上一篇文章:https://blog.csdn.net/u011943534/article/details/123049820

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值