1.前言

当今互联网Web各种应用H5、Android、ios、web、小程序等开发时大都采用前后端分离架构,公司为了商业变现会开放自己系统接口给其它公司使用。例如: 调用微信支付。

既然涉及到前后端分离,前端页面调用后端API接口,那么接口的安全设计是非常重要的一项工作。项目的架构师在项目布局过程中,会着重考虑安全,最常见的安全问题就是,用户在移动端提交数据向后端传输,黑客在传输过程中拦截提交的数据,进行篡改,进而达到伪造请求数据的目的。

例如前端提交金额,商品编号信息,黑客中途拦截,修改成低价商品,然后请求下单,早年间国内某电商技术不成熟时,抓包分析下单是很常见的。这时如果我们对一些常规的项目可以通过请求数据报文进行签名、加密、加盐、加时间戳、后端根据数据再次加密,与报文中的签名进行对比是否一致来控制接口安全,这种做法在大厂项目中也是常用手法。

2.什么是加密解密

  • 加密:数据加密的基本过程,就是对原来为明文,用户输入的数据通过某种处理,变成一串不可直接提取信息的代码,类似于英文字母加阿拉伯数字组合,通常称之为 密文。在战争年代的电报发报加密成密文,对方电台人员收到电文,根据约定的密码本进行破译便可得到明文,这就是为什么密码本对一个军队如此重要。
  • 解密:加密的逆过程,也就是破译电报。

3.常见的加密算法
加密技术通常分为三大类:对称式、非对称式、散列算法。

  • 对称式:通俗的说就是锁上一把锁与打开这把锁,用的都是同一样一把钥匙。常见的对称加密算法有:DES、3DES、AES等
  • 非对称式:俗名公开秘钥加密算法,它需要一对代码,一个为公钥 (public key)、另一个为私钥(private key) 加密解密用的不是一个秘钥,所以被称之非对称加密。

    • 使用公钥对明文加密,有且只有对应的私钥才能解开密文。
    • 使用私钥对明文加密,有且只有对应的公钥才能解开密文。
    • 大多数做法:公钥加密,私钥解密,公钥会在加密前发放给解密方。
 例子:Git 中ssh连接Github,本地电脑生成public key,与private key,将public key提前配置到GitHub账户中,private key留在本地,上传文件时Git便会自动识别认证身份。
  常见的非对称性加密算法:RSA、DSA 等
  • 散列算法:主要用于验证,防止信息被修。具体用途如:文件校验、数字签名、鉴权协议。
    常见的Hash散列算法:MD5、SHA1、SHA256、HMAC等等
  • MD5: MD5是一种不可逆的加密算法,目前是最牢靠的加密算法之一,尚没有能够逆运算的程序被开发出来,它对应任何字符串都可以加密成一段唯一的固定长度的代码。本文采用md5加密算法进行签名加密

4.使用MD5算法开放接口加密验签实现
需求分析:

  • 外部应用调用接口,做到极简丝滑调用。
  • 接口提供方系统不能影响原有业务。
  • 对接口需求方提交的数据进行校验,若不合法在接口被请求前就应终止这一次请求。
  • 符合主流大厂接口开放方式。

实现思路:

  • 接口提供方给接口需求方也就是第三方公司发放appid、secret,并要求严格保管。在系统内新建一个合作公司表,使用UUID生成appid与secret,对合作公司进行增删改查。简单 在此文章中略
  • 接口需求方使用约定的MD5算法将appid应用唯一识别、secret秘钥、timestamp时间戳、nonce随机数、业务参数、生成sign签名并一起传递给接口提供方。
  • 接口提供方接收获取appid、secret、timestamp、nonce、并逐个判断是否为空,为空就停止请求,并给第三方友好提示。
  • 接口提供方获取第三方公司提交的timestamp与当前系统时间做对比,如果差值大于120秒,则timestamp无效,如果差值小于120秒,则timestamp有效。目的是防止过期的提交。
  • 根据第三方提交的appid查询数据库内secret,与提交的secret进行对比,这一步可以根据appid判断权限 高级做法
  • 接口提供方获取第三方公司提交的nonce,比较redis中存储的nonce,不一致则通过。防止暴力请求接口。
  • 接口提供方将获取的appid、secret、timestamp、nonce、业务参数通过MD5算法运算得到sign2,与第三方公司提交的sign对比,如果不一致则为不合法请求。
  • 将nonce存入redis,过期时间设置为120秒。
  • 本文会演示采用拦截器和aop切面两种方式来拦截请求

5.上代码
pom.xml 引入依赖 springboot版本:2.3.7.RELEASE

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.75</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.7</version>
        </dependency>
    </dependencies>

MD5签名算法

package com.wsh.signature.utils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5 {

    /**
     * 生成 MD5
     *
     * @param data 待处理数据
     * @return MD5结果
     */
    public static String md5(String data) {
        StringBuilder sb = null;
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] array = md.digest(data.getBytes("UTF-8"));
            sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return sb.toString().toUpperCase();
    }


    /**
     * 生成 HMACSHA256
     * @param data 待处理数据
     * @param key 密钥
     * @return 加密结果
     * @throws Exception
     */
    public static String HMACSHA256(String data, String key){
        StringBuilder sb = null;
        try {
            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
            sha256_HMAC.init(secret_key);
            byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
            sb = new StringBuilder();
            for (byte item : array) {
                sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
        return sb.toString().toUpperCase();
    }


    public static void main(String[] args) {
        System.out.println(md5("mj1"));
        System.out.println(md5("123456"));
    }
}

签名是否一致验证工具

package com.wsh.signature.utils;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * MD5 sign签名校验宇生成工具
 */
public class GenerateSignatureUtil {


    public static final String FIELD_SIGN = "sign";

    /**
     * 判断签名是否正确,必须包含sign字段,否则返回false。
     *
     * @param data Map类型数据
     * @param key  API密钥
     * @return 签名是否正确
     * @throws Exception
     */
    public static boolean isSignatureValid(Map<String, String> data, String key) {
        if (!data.containsKey(FIELD_SIGN)) {
            return false;
        }
        String sign = data.get(FIELD_SIGN);
        return generateSignature(data, key).equals(sign);
    }

    public static String generateSignature(final Map<String, String> data, String key) {
        try {
            Set<String> keySet = data.keySet();
            String[] keyArray = keySet.toArray(new String[keySet.size()]);
            Arrays.sort(keyArray);
            StringBuilder sb = new StringBuilder();
            for (String k : keyArray) {
                if (k.equals(FIELD_SIGN)) {
                    continue;
                }
                // 参数值为空,则不参与签名
                if (data.get(k).trim().length() > 0)  {
                    sb.append(k).append("=").append(data.get(k).trim()).append("&");
                }
            }
            sb.append("key=").append(key);
            return MD5.md5(sb.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    public static void main(String[] args) {
        Map<String, String> data = new HashMap<>();
        data.put("user_phone", "18802011391");
        data.put("coupon_code", "2167 3908 4433");
//        data.put("status","100");
        data.put("coupon_type", "A025");
        data.put("timestamp", "1531730432");
        data.put("partner_id", "84513315");
        data.put("sign", "3389ACEBB3AEC93DB89C91614D3CC1C0");
        String secret = "JYHS1yWXCW00ore71TXE4aDatAEN6j0G";
        System.out.println(generateSignature(data, secret));
        System.out.println("校验:" + isSignatureValid(data, secret));

    }
}

错误信息提示工具类

package com.wsh.signature.utils;

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * 客户端工具类
 *
 * @author ruoyi
 */
public class ServletUtils {

    /**
     * 获取request
     */
    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    /**
     * 获取response
     */
    public static HttpServletResponse getResponse() {
        return getRequestAttributes().getResponse();
    }

    /**
     * 获取session
     */
    public static HttpSession getSession() {
        return getRequest().getSession();
    }

    /**
     * 获取ServletRequestAttributes
     */
    public static ServletRequestAttributes getRequestAttributes() {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        return (ServletRequestAttributes) attributes;
    }

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 将字符串渲染到客户端
     *
     * @param response 渲染对象
     * @param string   待渲染的字符串
     * @return null
     */
    public static String renderResultString(ServletResponse response, String string) {
        try {
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

第一种方法重写WebMvcConfigurer注册拦截器,根据配置文件来获取需要签名的就接口地址和不需要签名的地址

package com.wsh.signature.config;

import com.wsh.signature.interceptor.SignAuthInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


/**
 * @Author wangsh
 * @Date 2023/2/20 16:30
 * @Version 1.0
 * @deprecated  拦截器配置
 */
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    //需要签名的地址从配置文件取
    @Value("${open-sign.interceptor.sign.include-paths}")
    private String[] includePaths;
    //不需要签名的地址从配置文件取
    @Value("${open-sign.interceptor.sign.exclude-paths}")
    private String[] excludePaths;


    @Autowired
    private SignAuthInterceptor signAuthInterceptor;
    //注入拦截器
    @Bean
    public SignAuthInterceptor signAuthInterceptor () {
        return new SignAuthInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册签名拦截器
        registry.addInterceptor(signAuthInterceptor())
                //配置需要签名的地址
                .addPathPatterns(includePaths)
                //配置不需要签名的地址
                .excludePathPatterns(excludePaths);


        //registry.addInterceptor(signAuthInterceptor());
    }

}

SignAuthInterceptor 拦截器拦截请求

package com.wsh.signature.interceptor;

import com.alibaba.fastjson.JSON;
import com.wsh.signature.common.ApiResult;
import com.wsh.signature.utils.GenerateSignatureUtil;
import com.wsh.signature.utils.ServletUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class SignAuthInterceptor implements HandlerInterceptor {


    private static final String NONCE_KEY_STR = "nonce-";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("进入拦截器");
        Map<String, String[]> map = request.getParameterMap();
        // 从数组中取出参数放入Map中
        Map<String,String> param = new ConcurrentHashMap<>(10);
        for (Map.Entry<String, String[]> entry  : map.entrySet()) {
            String key = entry.getKey();
            String[] values = entry.getValue();
            for (int i = 0; i < values.length; i++) {
                String value = values[i];
                param.put(key,value);
            }
        }
        //  1、获取请求参数appId
        String appid = param.get("appid");
        if (StringUtils.isBlank(appid)) {
            log.info("appid不能为空");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("appid不能为空")));
            return false;
        }

        // 2、获取请求参数secret
        String secret =request.getParameter("secret");
        if (StringUtils.isBlank(secret)){
            log.info("secret不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret不能为空")));
            return false;
        }

        /** 3、验证secret权限、来源是否合法
         *  此处可以用appId,条件为 已开启,未封禁等进行数据库合作机构表查询,有可能已经终止合作禁止了此有用访问,
         *  业务上达到一定条件的可以根据appId分配权限,选择不同的接口能力进行开放
         */
       /* TDrivingCooperation drivingCooperationByPartnerkey = drivingCooperationService.getDrivingCooperationByPartnerkey(partnerkey);
        if (drivingCooperationByPartnerkey == null || drivingCooperationByPartnerkey.getStatus().equals(StatusEnum.DISABLE.getCode())) {
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("partnerkey无法查询到合作公司信息或已被封禁")));
            return false;
        }*/
        // 获取secret 与数据库值对比,判断请求来源是否合法
       /* if (!secret.equals(drivingCooperationByPartnerkey.getSecret())) {
            log.debug("secret与接口提供方不一致...........");
            System.out.println("secret与接口提供方不一致...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret与接口提供方不一致")));
            return false;
        }*/

        // 4、 获取请求参数timestamp 时间戳,
        String timestamp = request.getParameter("timestamp");
        if (StringUtils.isBlank(timestamp)){
            log.info("timestamp不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp不能为空")));
            return false;
        }

        /** 5、 防止过期时间的提交
         * 从前端传递的timestamp 与服务器端当前系统时间之差大于120s,则此次请求的timestamp无效
         *  留出短时间考虑网络问题提交速度慢,若时间过长中间时间足以挟持篡改参数,所以折中考虑了120秒
         */
        Long time = System.currentTimeMillis()/1000;
        if (Math.abs(Long.valueOf(timestamp)-time)>120) {
            log.info("timestamp失效...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp失效")));
            return false;
        }

        // 6、获取请求参数nonce随机数,防止重复的暴力请求
        String nonce = param.get("nonce");
        if (StringUtils.isBlank(nonce)) {
            log.debug("nonce不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("nonce不能为空")));
            return false;
        }
        /**
         *  如果设计得规范一些可以防止重复提交,我这因为是小项目,Demo演示就不做redis缓存随机数了
         *   流程:1、获取当前提交的随机数,作为key前往redis 查询,若有值则为重复提交
         *         2、redis中查询不到结果,将当前随机数作为key,value为随机数,过期时间设置为120s
         */
        // 7、获取请求sign签名参数,
        String sign = param.get("sign");
        if (StringUtils.isBlank(sign)){
            log.info("sign不能为空...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign不能为空")));
            return false;
        }
        //8.通过后台MD5重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改
        boolean reuslt = GenerateSignatureUtil.isSignatureValid(param, secret);
        if (!reuslt){
            log.debug("sign签名校验失败...........");
            ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign签名校验失败")));
            return false;
        }
        log.info("签名校验通过,放行...........");
        // 获取sign签名,与服务端生成的sign 签名对比
        System.out.println("sign ====================");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("SignAuthInterceptor postHandle======  ");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("SignAuthInterceptor afterCompletion======  ");
    }
}

application.yml配置文件

server:
  port: 8080

open-sign:
  interceptor:
    # 配置需要进行签名拦截的接口地址
    sign:
      include-paths: /user/**,/static
      exclude-paths: /admin

controller测试接口

package com.wsh.signature.controller;

import com.wsh.signature.annotation.Signature;
import com.wsh.signature.common.ApiResult;
import com.wsh.signature.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @RequestMapping(value = "/add",method = RequestMethod.POST)
    public ApiResult<Boolean> addUser (User user) {
        log.info("user=="+user);
        return ApiResult.ok(true);
    }
}

单元测试,这里采用HttpClient 跟接近真实开发方式 先来一次所有参数都正常的请求

  1. HttpClient工具类HttpClientUtil
    package com.wsh.signature.utils;
    import com.sun.deploy.net.HttpResponse;
    import org.apache.http.NameValuePair;
    import org.apache.http.client.entity.UrlEncodedFormEntity;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.client.utils.URIBuilder;
    import org.apache.http.entity.ContentType;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.message.BasicNameValuePair;
    import org.apache.http.util.EntityUtils;
    import org.springframework.http.HttpEntity;    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.URI;
    import java.nio.charset.Charset;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;
    
    /*
     * 利用HttpClient进行post请求的工具类
     */
    public class HttpClientUtil {
    
        public static String doGet(String url, Map<String, String> param) {
    
            // 创建Httpclient对象
            CloseableHttpClient httpclient = HttpClients.createDefault();
    
            String resultString = "";
            CloseableHttpResponse response = null;
            try {
                // 创建uri
                URIBuilder builder = new URIBuilder(url);
                if (param != null) {
                    for (String key : param.keySet()) {
                        builder.addParameter(key, param.get(key));
                    }
                }
                URI uri = builder.build();
    
                // 创建http GET请求
                HttpGet httpGet = new HttpGet(uri);
    
                // 执行请求
                response = httpclient.execute(httpGet);
                // 判断返回状态是否为200
                if (response.getStatusLine().getStatusCode() == 200) {
                    resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (response != null) {
                        response.close();
                    }
                    httpclient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return resultString;
        }
    
        public static String doGet(String url) {
            return doGet(url, null);
        }
    
        public static String doPost(String url, Map<String, Object> param) {
            // 创建Httpclient对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
            CloseableHttpResponse response = null;
            String resultString = "";
            try {
                // 创建Http Post请求
                HttpPost httpPost = new HttpPost(url);
                // 创建参数列表
                if (param != null) {
                    List<NameValuePair> paramList = new ArrayList<>();
                    for (String key : param.keySet()) {
                        paramList.add(new BasicNameValuePair(key, (String) param.get(key)));
                    }
                    // 模拟表单
                    UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList, Charset.forName("UTF-8"));
                    httpPost.setEntity(entity);
                }
                // 执行http请求
                response = httpClient.execute(httpPost);
                resultString = EntityUtils.toString(response.getEntity(), "utf-8");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    response.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
    
            return resultString;
        }
    
        public static String doPost(String url) {
            return doPost(url, null);
        }
    
        public static String doPostJson(String url, String json) {
            // 创建Httpclient对象
            CloseableHttpClient httpClient = HttpClients.createDefault();
            CloseableHttpResponse response = null;
            String resultString = "";
            try {
                // 创建Http Post请求
                HttpPost httpPost = new HttpPost(url);
                // 创建请求内容
                StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
                httpPost.setEntity(entity);
                // 执行http请求
                response = httpClient.execute(httpPost);
                resultString = EntityUtils.toString(response.getEntity(), "utf-8");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    response.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
    
            return resultString;
        }
    }
  1. 单元测试代码

    @Test
    public void testOpenSign() {
        Map<String, String> params = new ConcurrentHashMap<>(10);
        String secret = "1ae41230bd1b4383a44f1b114ceba13c";
        params.put("appid","6ee781ae6ef4496a");
        // 获取时间戳单位S
        Long timesTamp = System.currentTimeMillis()/1000;
        System.out.println("time ==" + timesTamp);
        params.put("timestamp",String.valueOf(timesTamp));
        params.put("nonce", UUID.randomUUID().toString());
        params.put("secret",secret);
        params.put("name", "张三");
        params.put("address","中国");
        params.put("sex","0");
        // 调用MD5算法加密生成签名
        String signature = GenerateSignatureUtil.generateSignature(params, secret);
        System.out.println("sign = " + signature);
        // 签名加入请求参数
        params.put("sign",signature);
        log.info("开始请求open-sign接口==========:{}",params);
        String result = HttpClientUtil.doPost("http://127.0.0.1:8080/user/add",new HashMap<>(params));
        System.out.println(result);
    }
    

使用aop切面形式,运用注解来拦截需要

  1. 自定义注解
    package com.wsh.signature.annotation;
    import org.springframework.core.annotation.AliasFor;
    import java.lang.annotation.*;
    
    /**
     * 接口签名注解
     */
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Signature {
    
        /**
         * 签名的配置代码
         */
        @AliasFor("signatureCode")
        String value() default "";
    
        /**
         * 签名的配置代码
         */
        @AliasFor("value")
        String signatureCode() default "";
    
    }
  1. 签名aop配置类
    package com.wsh.signature.aspect;
    
    import com.alibaba.fastjson.JSON;
    import com.wsh.signature.common.ApiResult;
    import com.wsh.signature.utils.GenerateSignatureUtil;
    import com.wsh.signature.utils.ServletUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.StringUtils;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * AOP的形式来拦截请求
     */
    @Configuration
    @Slf4j
    @Aspect
    public class SignatureAspect {
    
    
        ThreadLocal<Long> startTime = new ThreadLocal<Long>();
    
        @Pointcut("@annotation(com.wsh.signature.annotation.Signature) " +
                "&& (@annotation(org.springframework.web.bind.annotation.RequestMapping)" +
                "|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +
                "|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +
                "|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +
                "|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +
                "|| @annotation(org.springframework.web.bind.annotation.PatchMapping))")
        public void signature() {
        }
    
    
    
    
        @Before("signature()")
        public boolean doBefore(JoinPoint joinPoint) {
            startTime.set(System.currentTimeMillis());
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            HttpServletResponse response = attributes.getResponse();
            Map<String, String[]> map = request.getParameterMap();
            // 从数组中取出参数放入Map中
            Map<String,String> param = new ConcurrentHashMap<>(10);
            for (Map.Entry<String, String[]> entry  : map.entrySet()) {
                String key = entry.getKey();
                String[] values = entry.getValue();
                for (int i = 0; i < values.length; i++) {
                    String value = values[i];
                    param.put(key,value);
                }
            }
            //  1、获取请求参数appId
            String appid = param.get("appid");
            if (StringUtils.isBlank(appid)) {
                log.info("appid不能为空");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("appid不能为空")));
                return false;
            }
    
            // 2、获取请求参数secret
            String secret =request.getParameter("secret");
            if (StringUtils.isBlank(secret)){
                log.info("secret不能为空...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret不能为空")));
                return false;
            }
    
            /** 3、验证secret权限、来源是否合法
             *  此处可以用appId,条件为 已开启,未封禁等进行数据库合作机构表查询,有可能已经终止合作禁止了此有用访问,
             *  业务上达到一定条件的可以根据appId分配权限,选择不同的接口能力进行开放
             */
           /* TDrivingCooperation drivingCooperationByPartnerkey = drivingCooperationService.getDrivingCooperationByPartnerkey(partnerkey);
            if (drivingCooperationByPartnerkey == null || drivingCooperationByPartnerkey.getStatus().equals(StatusEnum.DISABLE.getCode())) {
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("partnerkey无法查询到合作公司信息或已被封禁")));
                return false;
            }*/
            // 获取secret 与数据库值对比,判断请求来源是否合法
           /* if (!secret.equals(drivingCooperationByPartnerkey.getSecret())) {
                log.debug("secret与接口提供方不一致...........");
                System.out.println("secret与接口提供方不一致...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("secret与接口提供方不一致")));
                return false;
            }*/
    
            // 4、 获取请求参数timestamp 时间戳,
            String timestamp = request.getParameter("timestamp");
            if (StringUtils.isBlank(timestamp)){
                log.info("timestamp不能为空...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp不能为空")));
                return false;
            }
    
            /** 5、 防止过期时间的提交
             * 从前端传递的timestamp 与服务器端当前系统时间之差大于120s,则此次请求的timestamp无效
             *  留出短时间考虑网络问题提交速度慢,若时间过长中间时间足以挟持篡改参数,所以折中考虑了120秒
             */
            Long time = System.currentTimeMillis()/1000;
            if (Math.abs(Long.valueOf(timestamp)-time)>120) {
                log.info("timestamp失效...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("timestamp失效")));
                return false;
            }
    
            // 6、获取请求参数nonce随机数,防止重复的暴力请求
            String nonce = param.get("nonce");
            if (StringUtils.isBlank(nonce)) {
                log.debug("nonce不能为空...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("nonce不能为空")));
                return false;
            }
            /**
             *  如果设计得规范一些可以防止重复提交,我这因为是小项目,Demo演示就不做redis缓存随机数了
             *   流程:1、获取当前提交的随机数,作为key前往redis 查询,若有值则为重复提交
             *         2、redis中查询不到结果,将当前随机数作为key,value为随机数,过期时间设置为120s
             */
            // 7、获取请求sign签名参数,
            String sign = param.get("sign");
            if (StringUtils.isBlank(sign)){
                log.info("sign不能为空...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign不能为空")));
                return false;
            }
            //8.通过后台MD5重新签名校验与前端签名sign值比对,确认当前请求数据是否被篡改
            boolean reuslt = GenerateSignatureUtil.isSignatureValid(param, secret);
            if (!reuslt){
                log.debug("sign签名校验失败...........");
                ServletUtils.renderString(response, JSON.toJSONString(ApiResult.failRequest("sign签名校验失败")));
                return false;
            }
            log.info("签名校验通过,放行...........");
            // 获取sign签名,与服务端生成的sign 签名对比
            System.out.println("sign ====================");
            return true;
        }
    
        @AfterReturning(returning = "ret", pointcut = "signature()")
        public void doAfterReturning(Object ret) {
            // 处理完请求,返回内容
            log.warn("开始响应:RESPONSE: {} ", ret);
            log.warn("响应时间: {} ms", System.currentTimeMillis() - startTime.get());
        }
    
    }
  1. 测试方法
    package com.wsh.signature.controller;
    import com.wsh.signature.annotation.Signature;
    import com.wsh.signature.common.ApiResult;
    import com.wsh.signature.entity.Order;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/order")
    @Slf4j
    public class OrderController {
    
    
        @RequestMapping(value = "/list",method = RequestMethod.POST)
        @Signature
        public ApiResult<Boolean> getList (Order order) {
            log.info("order=="+order);
            return ApiResult.ok(true);
        }
    }
  1. 单元测试
    @Test
    public void testOpenSign2() {
        Map<String, String> params = new ConcurrentHashMap<>(10);
        String secret = "1ae41230bd1b4383a44f1b114ceba13c";
        params.put("appid","6ee781ae6ef4496a");
        // 获取时间戳单位S
        Long timesTamp = System.currentTimeMillis()/1000;
        System.out.println("time ==" + timesTamp);
        params.put("timestamp",String.valueOf(timesTamp));
        params.put("nonce", UUID.randomUUID().toString());
        params.put("secret",secret);
        params.put("name", "张三");
        params.put("address","中国");
        params.put("sex","0");
        // 调用MD5算法加密生成签名
        String signature = GenerateSignatureUtil.generateSignature(params, secret);
        System.out.println("sign = " + signature);
        // 签名加入请求参数
        params.put("sign",signature);
        log.info("开始请求open-sign接口==========:{}",params);
        String result = HttpClientUtil.doPost("http://127.0.0.1:8080/order/list",new HashMap<>(params));
        System.out.println(result);
    }

6.注意事项
项目中使用了安全框架例如Shiro、SpringSecurity需要提前放开权限校验,否则请求还没有到签名拦截器就被安全框架拦截了,我这项目因为核心是校验签名所以没搭建shiro认证,我们使用签名校验简化了权限校验,不再需要注册账号,通过颁发token方式给第三方公司

最后修改:2023 年 02 月 22 日
如果觉得我的文章对你有用,请随意赞赏