目录

接口对外安全交互新姿势

接口对外安全交互新姿势

1.前言

由于这久做了一个乐企数电开票的项目,已经上线了,真的是一言难尽,再回首已经是轻舟已过万重山,接口通过外网暴露给业务方使用,由于业务方的服务是在阿里云上,我的这个服务是在华为云上,所以k8s上的服务只能通过service对外暴露出去给阿里云上的业务侧使用,所以我就有了下面的这个思路,使用ip白名单和sm2对请求body的做解密,响应做加密,只需要把秘钥和加密工具类对给业务侧就可以安全的调用接口,其实在华为云上可以在负载均衡器上配置白名单,只允许阿里云上那几台服务器的ip访问,所以在项目中就不用加这个ip白名单,那如果在没有云的情况下,这个方案也是可以的,安全性更高且可以灵活配置,本文介绍三种方式,至于其它的方式还有很多,毕竟密码学是一门高深的学科,常用的加密算法有MD5,AES(加密在前端使用会被找到秘钥),RSA(文本太长就会有点慢的,这个我之前也试过的,网上也有好多的工具类,去搜几个来调试一下就可以用了,之前搞的那个懒得去找了,后面有时间找一下看看分享一波),SM国密系列的,可以说非常的多,但是常用的就这几种。

2.姿势

2.1 AES

https://www.jianshu.com/p/f9284c3f732c

之前我分享那个Hutool工具包中搞了一个工具类,对接前端的CryptoJS实现是无法使用的,我那个工具类之适用于后端client和server之间的的加密解密交互,vue的CryptoJS实现后端加密前端解密需要参看上面那篇博客,在项目中亲测是有效的,这种方式的弊端就是AES的秘钥在前端的源码中会被轻易的找到,导致key泄露,从而可以轻松抓取到接口的参数并使用网页上的key解密看到解密之后的明文数据,这种方式也不推荐使用,安全性太差。

2.2 body参数签名及验签

这种方式只适用于接口数据不重要的情况下使用一个sign的参数,将body中的其它map参数进行排序之后签名,被调用端收掉body中的sign之后,对body中的参数进行验签,这种方式可能判断参数是否被篡改,数据明文传输就类似于裸奔,所以这种方式不推荐使用,安全性太差。

SignUtils工具类代码如下:

package xxx.xxx.xxx.util;

import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

/**
 * Description:签名工具类
 */
public class SignUtils {

    public static String getSign(Map<String, Object> requestMap, String appKey) {
        return hmacSHA256Encrypt(requestMap2Str(requestMap), appKey);
    }


    private static String hmacSHA256Encrypt(String encryptText, String encryptKey) {
        byte[] result = null;
        try {
            //根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称
            SecretKeySpec signinKey = new SecretKeySpec(encryptKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            //生成一个指定 Mac 算法 的 Mac 对象
            Mac mac = Mac.getInstance("HmacSHA256");
            //用给定密钥初始化 Mac 对象
            mac.init(signinKey);
            //完成 Mac 操作
            byte[] rawHmac = mac.doFinal(encryptText.getBytes(StandardCharsets.UTF_8));
            return bytesToHexString(rawHmac);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    private static String requestMap2Str(Map<String, Object> requestMap) {
        String[] keys = requestMap.keySet().toArray(new String[0]);
        Arrays.sort(keys);
        StringBuilder stringBuilder = new StringBuilder();
        for (String str : keys) {
            if (!str.equals("sign")) {
                stringBuilder.append(str).append(requestMap.get(str).toString());
            }
        }
        return stringBuilder.toString();
    }

    public static String bytesToHexString(byte[] bArray) {
        StringBuffer sb = new StringBuffer(bArray.length);

        for (byte aBArray : bArray) {
            String sTemp = Integer.toHexString(255 & aBArray);
            if (sTemp.length() < 2) {
                sb.append(0);
            }

            sb.append(sTemp.toUpperCase());
        }

        return sb.toString();
    }

    /**
     * MD5生成签名字符串
     *
     * @param str 需签名参数
     * @return
     */
    public static String md5(String str) {
        if (StringUtils.isEmpty(str)) {
            return "";
        }
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
            byte[] bytes = md5.digest(str.getBytes());
            String result = "";
            for (byte b : bytes) {
                String temp = Integer.toHexString(b & 0xff);
                if (temp.length() == 1) {
                    temp = "0" + temp;
                }
                result += temp;
            }
            return result;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
}

2.3使用sm2 加ip白名单

这个思路是来源于hutool官网:

https://doc.hutool.cn/pages/SmUtil/#%E5%AF%B9%E7%A7%B0%E5%8A%A0%E5%AF%86sm4

需要引入 Bouncy Castle 依赖

<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcpkix-jdk18on</artifactId>
  <version>1.78.1</version>
</dependency>

说明 bcprov-jdk18on 的版本请前往Maven中央库搜索,查找对应JDK的最新版本。

body中栗子如下:

{
  
  "appId":"xxxx",//哪个业务的标识
    
  "method":"xxxx",//调用那个方法,被调用端可以将其它接口全部聚合到一个接口上对外暴露出去
  "encryptStr":"04A51E4C0CA2766B9F382C7D308AB7E8C3DD1E104CAF83D95C72C46154F6D1ACBCD199A4BAC57C9FF9EF9864728BFE20F8568ADDD1C1BD51ECFF24D6F37095091F55AE3A3C1518B308092E8338D4A8BDD87DEC5C68DB6373A0D50709AD82B0FA484CEE169FD7DA7156133E396FAE77E315DE49D7F3F99BE4701B1686763F026412DEA8334123D8A3E186B16B4E806D4E866229029973F34D9A298F715304AEDEE069A6E1CBBF86D3757DC4A02F66EBE7652E64465081313C6242F67687F48D67A29C61F2ED6C115F627DD032331A7F04D1475B3D8A86D640BA8C64E310B4689EFA835943F7C3057B057E5B888A0E87D167EAE2F4028564406C2A08ACF3516453B099A89CE66AF4634079F79E710182E967530ACF1E4D39A349F3083CDD5E05ACB5A866BE49164BD3C26A163ADD8D2BA7ADF499EEAF220B9545B67CE654D80C84E51E02D3975633812B7FFF97E65179FBB13B74D337615E8D04C9C1A534F554DA1F9D626BDC4EE985D9326986A07618EDA24623AE254EA5FC3A32E79443E8ED5F15C8D586D91752AC488CD0600F6509F422A11B7766CD1FA2332BC28B3E676A50985292E6CE6FB7891B358AACAB94C974EA34437DD8154F4CB79B35BA0AE5588562E5F93255FA5BF60742E82EB530E379B7117B12EBFD42B6D2643884FD360350B4AB92D5C11F04D326C1EB8C0674A972755C25B06B73209653A922C2BF0F64EB1750453E4308D6E791BAC903786ECABF493E0B19118AA7A76D8422C1FBDD1191B401FB113551460AE301383A10C23D18994E00FFBB99059B05D31F86F3DDED25C5B45CAB08EDA46FBA357DF5CEBEE9846D81D6A06B308466BE44EB0DD2800A457C8AF698CCBE3D2C0B0D013F830E6F5153BA95EFC10C98597909D9BDE39B14C30B6AEEE274D80A576326D04774C43B84778B2724E2344F54E793487023823FA0F5F6506CBC9E49DD25852D7DD2918AB440EE27975456DF80F65CECC51167929E0B96A4F98D770959166B348F0F202173A5EE7B3D8CED79E8BB4757A877F97D725BED91E07634B45B0E890D860277C15D9ADFA87C2EEDD359D47048BEA1AEF7A568E5F6EFFE9C530C5B67A7C94D67BC2FE3D065C50C2384C786DF637A39E827D2BCE31BD493D8874498C7F75C3F75DA5E8ED300DB9842059D923E9DD136500863121324A909BA3A8"
}

被调用端服务只需要配置一个对应业务的秘钥对且是否启用该秘钥对,如果秘钥对泄露,重新在数据库里面加一对新的秘钥对且启用密钥对,将之前的秘钥对停用了,这种就可以保证接口的安全性。

IpUtil工具类:

package xxxxx.xxxx.util;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletRequest;

/**
 * 常用获取客户端信息的工具
 */
@Slf4j
public final class IpUtil {

    /**
     * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
     *
     * @param request
     * @return
     */
    public static String getIpAddress(HttpServletRequest request) {
        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        String ip = request.getHeader("X-Forwarded-For");
        log.info("getIpAddress(HttpServletRequest) - X-Forwarded-For - String ip=" + ip);
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("Proxy-Client-IP");
                log.info("getIpAddress(HttpServletRequest) - Proxy-Client-IP - String ip=" + ip);
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("WL-Proxy-Client-IP");
                log.info("getIpAddress(HttpServletRequest) - WL-Proxy-Client-IP - String ip=" + ip);
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_CLIENT_IP");
                log.info("getIpAddress(HttpServletRequest) - HTTP_CLIENT_IP - String ip=" + ip);
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getHeader("HTTP_X_FORWARDED_FOR");
                log.info("getIpAddress(HttpServletRequest) - HTTP_X_FORWARDED_FOR - String ip=" + ip);
            }
            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddr();
                log.info("getIpAddress(HttpServletRequest) - getRemoteAddr - String ip=" + ip);
            }
        } else if (ip.length() > 15) {
            String[] ips = ip.split(",");
            for (int index = 0; index < ips.length; index++) {
                String strIp = (String) ips[index];
                if (!("unknown".equalsIgnoreCase(strIp))) {
                    ip = strIp;
                    break;
                }
            }
        }
        return ip;
    }

}

WhiteIpConfig白名单配置类:

package xxx.xxxx.config;

import xxxx.xxxx.IpUtil;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Data
@Configuration
@RefreshScope
public class WhiteIpConfig {

    @Value("${whiteIp:192.168.40.60}")
    private String whiteIp;


    public List<String> getWhiteIps() {
        List<String> appIds = new ArrayList<>();
        if (StringUtils.isNotEmpty(whiteIp)) {
            String[] split = whiteIp.split(",");
            appIds = Arrays.asList(split);
        }
        return appIds;
    }

    public void checkWhiteIp() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        List<String> whiteIps = this.getWhiteIps();
        String ipAddress = IpUtil.getIpAddress(request);
        if (StringUtils.isEmpty(ipAddress)) {
            throw new RuntimeException("未获取到请求的ip地址!");
        }
        if (!whiteIps.contains(ipAddress)) {
            throw new RuntimeException("请求ip地址不在ip白名单内,不允许访问!");
        }
    }

}

还需要对响应的数据进行sm2加密,接收响应端使用密钥对进行解密就可以拿到响应的明文数据了,这种接口就非常的安全。

这里在分享一个采集服务器的ip地址和mac地址的工具类,亲测好用:

NetUtil工具类:

package xxxx.xxxx.util;

import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

@Slf4j
public class NetUtil {

    /**
     * 此方法描述的是:获得服务器的IP地址
     *
     * @return
     */
    public static String getLocalIP() {
        String sIP = "";
        InetAddress ip = null;
        try {
            boolean bFindIP = false;
            Enumeration<NetworkInterface> netInterfaces = (Enumeration<NetworkInterface>) NetworkInterface
                    .getNetworkInterfaces();
            while (netInterfaces.hasMoreElements()) {
                if (bFindIP) {
                    break;
                }
                NetworkInterface ni = (NetworkInterface) netInterfaces
                        .nextElement();
                Enumeration<InetAddress> ips = ni.getInetAddresses();
                while (ips.hasMoreElements()) {
                    ip = (InetAddress) ips.nextElement();
                    if (!ip.isLoopbackAddress()
                            && ip.getHostAddress().matches(
                            "(\\d{1,3}\){3}\\d{1,3}")) {
                        bFindIP = true;
                        break;
                    }
                }
            }
        } catch (Exception e) {
            log.error("getLocalIP.ex:{}", e.getMessage());
        }
        if (null != ip) {
            sIP = ip.getHostAddress();
        }
        return sIP;
    }

    /**
     * 此方法描述的是:获得服务器的IP地址(多网卡)
     *
     * @return
     */
    public static List<String> getLocalIPS() {
        InetAddress ip = null;
        List<String> ipList = new ArrayList<String>();
        try {
            Enumeration<NetworkInterface> netInterfaces = (Enumeration<NetworkInterface>) NetworkInterface
                    .getNetworkInterfaces();
            while (netInterfaces.hasMoreElements()) {
                NetworkInterface ni = (NetworkInterface) netInterfaces
                        .nextElement();
                Enumeration<InetAddress> ips = ni.getInetAddresses();
                while (ips.hasMoreElements()) {
                    ip = (InetAddress) ips.nextElement();
                    if (!ip.isLoopbackAddress()
                            && ip.getHostAddress().matches(
                            "(\\d{1,3}\){3}\\d{1,3}")) {
                        ipList.add(ip.getHostAddress());
                    }
                }
            }
        } catch (Exception e) {
            log.error("getLocalIPS.ex:{}", e.getMessage());
        }
        return ipList;
    }

    /**
     * 此方法描述的是:获得服务器的MAC地址
     *
     * @return
     */
    public static String getMacId() {
        String macId = "";
        InetAddress ip = null;
        NetworkInterface ni = null;
        try {
            boolean bFindIP = false;
            Enumeration<NetworkInterface> netInterfaces = (Enumeration<NetworkInterface>) NetworkInterface
                    .getNetworkInterfaces();
            while (netInterfaces.hasMoreElements()) {
                if (bFindIP) {
                    break;
                }
                ni = (NetworkInterface) netInterfaces
                        .nextElement();
                // ----------特定情况,可以考虑用ni.getName判断
                // 遍历所有ip
                Enumeration<InetAddress> ips = ni.getInetAddresses();
                while (ips.hasMoreElements()) {
                    ip = (InetAddress) ips.nextElement();
                    if (!ip.isLoopbackAddress() // 非127.0.0.1
                            && ip.getHostAddress().matches(
                            "(\\d{1,3}\){3}\\d{1,3}")) {
                        bFindIP = true;
                        break;
                    }
                }
            }
        } catch (Exception e) {
            log.error("getMacId.ex1:{}", e.getMessage());
        }
        if (null != ip) {
            try {
                macId = getMacFromBytes(ni.getHardwareAddress());
            } catch (SocketException e) {
                log.error("getMacId.ex2:{}", e.getMessage());
            }
        }
        return macId;
    }

    /**
     * 此方法描述的是:获得服务器的MAC地址(多网卡)
     *
     * @return
     */
    public static List<String> getMacIds() {
        InetAddress ip = null;
        NetworkInterface ni = null;
        List<String> macList = new ArrayList<String>();
        try {
            Enumeration<NetworkInterface> netInterfaces = (Enumeration<NetworkInterface>) NetworkInterface
                    .getNetworkInterfaces();
            while (netInterfaces.hasMoreElements()) {
                ni = (NetworkInterface) netInterfaces
                        .nextElement();
                // ----------特定情况,可以考虑用ni.getName判断
                // 遍历所有ip
                Enumeration<InetAddress> ips = ni.getInetAddresses();
                while (ips.hasMoreElements()) {
                    ip = (InetAddress) ips.nextElement();
                    if (!ip.isLoopbackAddress() // 非127.0.0.1
                            && ip.getHostAddress().matches(
                            "(\\d{1,3}\){3}\\d{1,3}")) {
                        macList.add(getMacFromBytes(ni.getHardwareAddress()));
                    }
                }
            }
        } catch (Exception e) {
            log.error("getMacIds.ex2:{}", e.getMessage());
        }
        return macList;
    }

    private static String getMacFromBytes(byte[] bytes) {
        StringBuffer mac = new StringBuffer();
        byte currentByte;
        boolean first = false;
        for (byte b : bytes) {
            if (first) {
                mac.append("-");
            }
            currentByte = (byte) ((b & 240) >> 4);
            mac.append(Integer.toHexString(currentByte));
            currentByte = (byte) (b & 15);
            mac.append(Integer.toHexString(currentByte));
            first = true;
        }
        return mac.toString().toUpperCase();
    }

    public static void main(String[] args) {
        String localIP = NetUtil.getLocalIP();
        String macId = NetUtil.getMacId();
        log.info("localIP:{},macId:{}", localIP, macId);
    }

}

3.总结

本次分享到此结束,希望我的分享对你有所启发和帮助,思路基本上都是大同小异,套路基本上是一个套路,主要是的是姿势,姿势真的很重要,姿势不对努力白费,好久没有写文章了,请尊重作者原创,转载请注明出处,否则发现抄袭直接举报,创作不易,请一键三连,么么么哒!