前言

当今互联网Web各种利用H5、Android、ios、web、小程序等开发时大都采纳前后端拆散架构,公司为了商业变现会凋谢本人零碎接口给其它公司应用。例如: 调用微信领取。

既然波及到前后端拆散,前端页面调用后端API接口,那么接口的平安设计是十分重要的一项工作。我的项目的架构师在我的项目布局过程中,会着重思考平安,最常见的平安问题就是,用户在挪动端提交数据向后端传输,黑客在传输过程中拦挡提交的数据,进行篡改,进而达到伪造申请数据的目标。

例如前端提交金额,商品编号信息,黑客中途拦挡,批改成低价商品,而后申请下单,早年间国内某电商技术不成熟时,抓包剖析下单是很常见的。这时如果咱们对一些惯例的我的项目能够通过申请数据报文进行签名、加密、加盐、加工夫戳、后端依据数据再次加密,与报文中的签名进行比照是否统一来管制接口平安,这种做法在大厂我的项目中也是罕用手法。


什么是加密解密

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

常见的加密算法

加密技术通常分为三大类:对称式非对称式散列算法

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

    • 应用公钥对明文加密,有且只有对应的私钥能力解开密文。
    • 应用私钥对明文加密,有且只有对应的公钥能力解开密文。
    • 大多数做法:公钥加密,私钥解密,公钥会在加密前发放给解密方。

      例子:Git 中ssh连贯Github,本地电脑生成public key,与private key,将public key提前配置到GitHub账户中,private key留在本地,上传文件时Git便会自动识别认证身份。

      常见的非对称性加密算法:RSADSA

      • 散列算法:次要用于验证,避免信息被修。具体用处如:文件校验、数字签名、鉴权协定。

      常见的Hash散列算法:MD5SHA1SHA256HMAC等等

      • MD5: MD5是一种不可逆的加密算法,目前是最可靠的加密算法之一,尚没有可能逆运算的程序被开发进去,它对应任何字符串都能够加密成一段惟一的固定长度的代码。

其余算法介绍查看连贯详情


应用MD5算法凋谢接口加密验签实现

需要剖析:

  1. 内部利用调用接口,做到极简丝滑调用。
  2. 接口提供方零碎不能影响原有业务。
  3. 对接口需求方提交的数据进行校验,若不非法在接口被申请前就应终止这一次申请。
  4. 合乎支流大厂接口凋谢形式。

实现思路:

  • 接口提供方给接口需求方也就是第三方公司发放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秒。

    上代码

pom.xml 引入依赖

 <dependencies>            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-starter-web</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>            <!-- Shiro+JWT start -->            <dependency>                <groupId>org.apache.shiro</groupId>                <artifactId>shiro-spring</artifactId>                <version>1.5.1</version>            </dependency>            <dependency>                <groupId>com.auth0</groupId>                <artifactId>java-jwt</artifactId>                <version>3.10.1</version>            </dependency>            <!-- Shiro+JWT end -->            <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->            <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>            <!-- XSS -->            <dependency>                <groupId>org.apache.commons</groupId>                <artifactId>commons-text</artifactId>                <version>1.8</version>            </dependency>        </dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <configuration>                    <excludes>                        <exclude>                            <groupId>org.projectlombok</groupId>                            <artifactId>lombok</artifactId>                        </exclude>                    </excludes>                </configuration>            </plugin>        </plugins>    </build>

MD5签名算法

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();    }}

签名是否统一验证工具

/** * 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 "";    }}

错误信息提醒工具类

/** * 客户端工具类 * @author  */public class ServletUtils {     // 获取request    public static HttpServletRequest getRequest() {        return getRequestAttributes().getRequest();    }    // 获取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;    }  }

重写WebMvcConfigurer

/** * @Author 真香 * @Date 2021/4/20 16:30 * @Version 1.0 */@Slf4j@Configurationpublic class OpenSignWebMvcConfig  implements WebMvcConfigurer {    @Autowired    private SignAuthInterceptor signAuthInterceptor;    @Autowired    private OpenSignProperties openSignProperties;    // 拦截器配置    private OpenSignInterceptorProperties interceptorConfig;    // 注入spring 容器    @Bean    public SignAuthInterceptor signAuthInterceptor () {        return new SignAuthInterceptor();    }    @PostConstruct    public void init () {        interceptorConfig = openSignProperties.getInterceptor();        log.debug("openSignProperties:{}", JSON.toJSONString(interceptorConfig));    }    @Override    public void addInterceptors(InterceptorRegistry registry) {        // 注册签名拦截器        if (interceptorConfig.getSign().isEnable()) {            registry.addInterceptor(signAuthInterceptor())                    .addPathPatterns(interceptorConfig.getSign().getIncludePaths())                    .excludePathPatterns(interceptorConfig.getSign().getExcludePaths());        }    }}

application.yml配置文件

server:   port: 9999   ######################## Spring Shiro start ########################   shiro:       # 是否启用      enable: true       # 权限配置      anon:          # 排除登录登出         - /login,/logout,          # 排除动态资源         - /static/**,/templates/**          # 排除actuator         - /actuator/**         # 排除首页, 不再凋谢此页面         #      - /,/welcome.html          # 排除测试门路         - /hello/world,       # 多行字符串权限配置      filter-chain-definitions: |         /resource/**=anon         /upload/**=anon         /verificationCode/**=anon         /enum=anon      # 权限配置      permission:          # 排除登陆登出相干         - urls:           permission: anon######################## Spring Shiro end ########################################################open sign start ######################open-sign:    # Filter配置   filter:      request:         enable: true         url-patterns: /*         order: 1         async: true      xss:         enable: true         url-patterns: /*         order: 2         async: true      repeatedlyread:         enable: true         url-patterns: /*         order: 2         async: true    # 拦截器配置   interceptor:      # 配置须要进行签名拦挡的接口地址      sign:         enable: true         include-paths:

SignAuthInterceptor 最重要的拦截器拦挡申请

@Slf4jpublic class SignAuthInterceptor implements HandlerInterceptor {    private static final String NONCE_KEY_STR = "nonce-";    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        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 签名比照        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======  ");    }}

测试接口

/** * @Author 真香 * @Date 2021/4/20 17:01 * @Version 1.0 */@RestController@RequestMapping("/user")@Slf4jpublic class UserController {    @RequestMapping(value = "/add",method = RequestMethod.POST)    public ApiResult<Boolean> addUser (User user) {        log.info("user=="+user);        return ApiResult.ok(true);    }}

以上展现了一些要害代码,还有一些辅助代码因为排版问题未一一展现,后续能够通过仓库地址克隆。

来吧 展现

模仿申请失常 这里采纳HttpClient 跟靠近实在开发方式
先来一次所有参数都失常的申请

 @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",UUIDUtil.getUuid());        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://localhost:9999/user/add", params);        System.out.println(result);    }

拿到参数


第三方失去申请返回值

{"code":200,"success":true,"message":"操作胜利","data":true,"time":"2021-04-23 08:47:56"}

模仿谬误请 这一次我成心去除appid不传

@Test    public void testOpenSign() {        Map<String, String> params = new ConcurrentHashMap<>(10);        String secret = "1ae41230bd1b4383a44f1b114ceba13c";        Long timesTamp = System.currentTimeMillis()/1000;        System.out.println("time ==" + timesTamp);        params.put("timestamp",String.valueOf(timesTamp));        params.put("nonce",UUIDUtil.getUuid());        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://localhost:9999/user/add", params);        System.out.println(result);    }

断点捕捉

第三方失去敌对返回值

{"code":500000,"message":"appid不能为空","success":false,"time":"2021-04-23 16:55:25"}

以上只应用了appid作为例子,其余的都是大差不差,算法代码曾经写好,错了,漏传、篡改都会帮咱们校验。

我的项目中应用了平安框架例如Shiro、SpringSecurity须要提前放开权限校验,否则申请还没有到签名拦截器就被平安框架拦挡了,我这我的项目因为外围是校验签名所以没搭建shiro认证,咱们应用签名校验简化了权限校验,不再须要注册账号,通过颁发token形式给第三方公司。

仓库地址:https://gitee.com/JameZhan/ch...
欢送Issues与PR

写在最初

很长时间没有更新了,也有一些好友来催更。上半年因为业务忙,也自我放松了,过多的工夫放在了看书,前期如果有可能会写一些书籍读后感与玩耍杂记。

我是顾北,一个集才华与技术的男生,有问题欢送后盾加微信交换。