• 超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)
  • 2025-12-12 03:57:39
  • 文章目录

    1、场景2、接口防御措施3、签名认证逻辑4、签名算法规则5、代码示例1、sign工具类2、定义拦截器3、生成accessKey、secretKey 工具类4、signInterceptor类5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

    1、场景

    由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。

    2、接口防御措施

    请求发起时间得在限制范围内请求的用户是否真实存在是否存在重复请求请求参数是否被篡改

    3、签名认证逻辑

    1、服务端生成一对 accessKey/secretKey密钥对,将 accessKey公开给客户端,将 secretKey 保密。

    2、客户端使用 secretKey和一些请求参数(如时间戳、请求内容等),使用 MD5 算法生成签名。

    3、客户端将 accessKey、签名和请求参数一起发送给服务端。

    4、服务端使用 和收到的请求参数,使用 MD5 算法生成签名。

    5、服务端比较客户端发来的签名和自己生成的签名是否相同,如果相同,则认为请求是可信的,否则认为请求是不可信的。 secretKey不进行网络传输,只用于本地MD5运算

    4、签名算法规则

    计算步骤 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。 将请求参数对按key进行字典升序排序,得到有序的参数对列表N 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥) 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名

    注意事项 不同接口要求的参数对不一样,计算签名使用的参数对也不一样 参数名区分大小写,参数值为空不参与签名 URL键值拼接过程value部分需要URL编码

    5、代码示例

    1、sign工具类

    public class SignUtil {

    /**

    * 签名算法

    * 1. 计算步骤

    * 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。

    * 将请求参数对按key进行字典升序排序,得到有序的参数对列表N

    * 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8

    * 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)

    * 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名

    * 2. 注意事项

    * 不同接口要求的参数对不一样,计算签名使用的参数对也不一样

    * 参数名区分大小写,参数值为空不参与签名

    * URL键值拼接过程value部分需要URL编码

    * @return 签名字符串

    */

    private static String getSign(Map map, String secretKey) {

    List> infoIds = new ArrayList<>(map.entrySet());

    Collections.sort(infoIds, new Comparator>() {

    public int compare(Map.Entry arg0, Map.Entry arg1) {

    return (arg0.getKey()).compareTo(arg1.getKey());

    }

    });

    StringBuffer sb = new StringBuffer();

    for (Map.Entry m : infoIds) {

    if(null == m.getValue() || StringUtils.isNotBlank(m.getValue().toString())){

    sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&");

    }

    }

    sb.append("secret-key=").append(secretKey);

    return MD5.create().digestHex(sb.toString()).toUpperCase();

    }

    //获取随机值

    private static String getNonceStr(int length){

    //生成随机字符串

    String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";

    Random random=new Random();

    StringBuffer randomStr=new StringBuffer();

    // 设置生成字符串的长度,用于循环

    for(int i=0; i

    //从62个的数字或字母中选择

    int number=random.nextInt(62);

    //将产生的数字通过length次承载到sb中

    randomStr.append(str.charAt(number));

    }

    return randomStr.toString();

    }

    //签名验证方法

    public static boolean signValidate(Map map,String secretKey,String sign){

    String mySign = getSign(map,secretKey);

    return mySign.equals(sign);

    }

    }

    2、定义拦截器

    @Configuration

    public class SignInterceptorConfig implements WebMvcConfigurer {

    @Override

    public void addInterceptors(InterceptorRegistry registry) {

    registry.addInterceptor(signInterceptor())

    .addPathPatterns("/openapi/**");//只拦截openapi前缀的接口

    }

    //交给spring管理 SignInterceptor bean

    //不然下边 private OpenApiApplyMapper applyMapper;注入为null

    @Bean

    public SignInterceptor signInterceptor(){

    return new SignInterceptor();

    }

    }

    3、生成accessKey、secretKey 工具类

    public class KeyGenerator {

    private static final int KEY_LENGTH = 32; // 指定生成的key长度为32字节

    public static String generateAccessKey() {

    SecureRandom random = new SecureRandom();

    byte[] bytes = new byte[KEY_LENGTH / 2]; // 生成的字节数要除以2

    random.nextBytes(bytes);

    return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 20);

    }

    public static String generateSecretKey() {

    SecureRandom random = new SecureRandom();

    byte[] bytes = new byte[KEY_LENGTH];

    random.nextBytes(bytes);

    return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 40);

    }

    }

    4、signInterceptor类

    public class SignInterceptor implements HandlerInterceptor {

    private static final String ACCESSKEY = "access-key";//调用者身份唯一标识

    private static final String TIMESTAMP = "time-stamp";//时间戳

    private static final String SIGN = "sign";//签名

    private static final String NONCE = "nonce";//随机值

    @Resource

    private OpenApiApplyMapper applyMapper;

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    if(checkSign(request, response)){//签名认证

    return HandlerInterceptor.super.preHandle(request, response, handler);

    }

    return false;

    }

    /**

    * 验证签名

    * @param request

    * @param response

    * @return

    * @throws Exception

    */

    private boolean checkSign(HttpServletRequest request,HttpServletResponse response)throws Exception {

    response.setContentType("application/json");

    response.setCharacterEncoding("utf8");

    String ip = IPUtils.getIpAddr(request);

    FzyLogUtil.infoSafe("开放接口", "访问时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL());

    String accessKey = request.getHeader(ACCESSKEY);

    String timestamp = request.getHeader(TIMESTAMP);

    String nonce = request.getHeader(NONCE);

    String sign = request.getHeader(SIGN);

    if (!StringUtils.isNotBlank(accessKey)) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey无效")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey无效");

    return false;

    }

    if (StringUtils.isBlank(sign)) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("签名无效")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:签名无效");

    return false;

    }

    OpenApiDetailDO openApiDetailDO = applyMapper.selectOneByAccessKey(accessKey);

    if (openApiDetailDO == null) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey不存在")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey不存在");

    return false;

    }

    if (StringUtils.isNotBlank(openApiDetailDO.getBlackList())) {

    for (String bIp : openApiDetailDO.getBlackList().split(",")) {

    if (bIp.equals(ip)) {//黑名单

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:黑名单拒绝请求");

    return false;

    }

    }

    }

    if (StringUtils.isNotBlank(openApiDetailDO.getWhiteList())) {

    boolean flag = false;

    for (String bIp : openApiDetailDO.getWhiteList().split(",")) {

    if (bIp.equals(ip)) {//白名单

    flag = true;

    break;

    }

    }

    if(!flag){

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:白名单未符合拒绝请求");

    return false;

    }

    }

    if ("0".equals(openApiDetailDO.getInvokeStatus() + "")) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("访问权限已被冻结")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:访问权限已被冻结");

    return false;

    }

    if (!"1".equals(openApiDetailDO.getApiStatus() + "")) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口异常,暂停访问")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:接口异常,暂停访问");

    return false;

    }

    if (!StringUtils.isNotBlank(timestamp)) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("时间戳无效")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:时间戳无效");

    return false;

    } else if (openApiDetailDO.getTimeOut() != null) {

    if (System.currentTimeMillis() - Long.valueOf(timestamp) > openApiDetailDO.getTimeOut() * 1000) {

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("请求已过期")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:请求已过期");

    return false;

    }

    ;

    }

    Map hashMap = new HashMap<>();

    String queryStrings = request.getQueryString();//获取url后边拼接的参数

    if (queryStrings != null) {

    for (String queryString : queryStrings.split("&")) {

    String[] param = queryString.split("=");

    if (param.length == 2) {

    hashMap.put(param[0], param[1]);

    }

    }

    }

    hashMap.put(ACCESSKEY, accessKey);

    hashMap.put(TIMESTAMP, timestamp);

    if (StringUtils.isNotBlank(nonce)) {

    hashMap.put(NONCE, nonce);

    }

    String secretKey = openApiDetailDO.getSecretKey();

    String body = new RequestWrapper(request).getBody();

    if (StringUtils.isNotBlank(body)) {

    Map map = JSON.parseObject(body);

    if (map != null) {

    hashMap.putAll(map);

    }

    }

    if (!SignUtil.signValidate(hashMap, secretKey, sign)) {//认证失败

    response.getWriter().write(JSON.toJSONString(ResultUtil.fail("认证失败")));

    FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:认证失败");

    return false;

    }

    return true;

    }

    }

    5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

    通过过滤器解决

    @Component

    @WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")

    @Order(10000)

    public class HttpServletRequestFilter implements Filter {

    @Override

    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @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(null == request) {

    filterChain.doFilter(servletRequest, servletResponse);

    } else {

    filterChain.doFilter(request, servletResponse);

    }

    }

    @Override

    public void destroy() {

    }

    }