秒杀系统防护 —— 应用级风控事件

392 阅读3分钟

背景

平台级防控是当前对抗黑灰产的有效手段,各大企业都有自己的安全风控产品,提供各种不同的安全服务,例如,阿里云的运营反诈骗系统,腾讯的天御系统,网易的易盾等。

但是也是不可或缺的,至少在针对非专业级灰产时,应用级防护仍然可能发挥关键专用。应用级防护应该从流量限制人机安全业务安全三个方面入手。

实现方式

流量限制

  1. sentinel 流量限制

Sentinel提供了丰富的功能特性,如流量控制异常熔断集群限流速率控制等。此外,Sentinel也提供了丰富的数据采集如RPCHTTP以及API Gateway等。

参考文章:www.yuque.com/ezreal-fizp…

  1. IP 地址限制

根据用户的IP进行限流,同一IP短时间内多次访问,只允许某一次访问通过。

人机安全

  1. 普通验证码

比如常见的文字验证码算术验证码答题验证码等。此类验证码在离散普通用户的前端流量方面作用明显,其原因在于,现有的OCR技术和打码平台的发展,早已攻克普通验证码那点事。

  1. 人机验证码

近些年国内互联网企业也纷纷启用滑块验证码,通过采集过程中的多种数据特征(访问评率、轨迹、地理位置和历史记录等多维度数据),进行分析是否为机器。

业务安全

在设计产品逻辑时要严谨,比如活动要设置特定的购买金额、用户人群限制等,即使平台级防护能力再强,一旦产品逻辑存在严重漏洞,那也白忙活。

风控代码简易实现

主要是基于springcloud gateway的全局过滤器来实现。

这里主要是基于限流的思想实现 IP风控、资源风控。而实现限流的措施有计数器算法令牌桶算法、漏桶算法。文章使用计数器算法来实现。具体可以参考这篇文章

基于Redis实现计数器算法

基本思想是:使用zset 记录当前时间过去的某一个时间(这个时间段是固定的),这一段时间中访问的次数,若访问的次数大于某个值,就拒绝服务。类似于滑动窗口的实现。

   /**
     *
     * @param userActionKey 用户及行为标识
     * @param period        限流周期,单位毫秒
     * @param size          滑动窗口大小
     * @return
     */
    @Override
    public boolean pass(String userActionKey, int period, int size) {

        long current = System.currentTimeMillis();
        int length = period * size;
        long start = current - length;
        long expireTime = length + period;

        // 添加新的请求
        redisTemplate.opsForZSet().add(userActionKey, String.valueOf(current), current);
        // 过期时间 窗口长度+一个时间间隔
        redisTemplate.expire(userActionKey, expireTime, TimeUnit.MILLISECONDS);
        // 移除[0,start]区间内的值
        redisTemplate.opsForZSet().removeRangeByScore(userActionKey, 0 ,start);

        // 统计个数
        Long count = redisTemplate.opsForZSet().zCard(userActionKey);
        if (count == null) {
            return false;
        }
        return count <= size;
    }

IP风控实现

获取IP地址

直接上代码:

public static String getIpAddr(ServerHttpRequest request) {
        String ip = null;
        try {
            //以下两个获取在k8s中,将真实的客户端IP,放到了x-Original-Forwarded-For。而将WAF的回源地址放到了 x-Forwarded-For了。
            ip = request.getHeaders().getFirst("X-Original-Forwarded-For");
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeaders().getFirst("X-Forwarded-For");
            }
            //获取nginx等代理的ip
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeaders().getFirst("x-forwarded-for");
            }
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeaders().getFirst("Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");
            }
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeaders().getFirst("HTTP_CLIENT_IP");
            }
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getHeaders().getFirst("HTTP_X_FORWARDED_FOR");
            }
            //兼容k8s集群获取ip
            if (StringUtils.isEmpty(ip) || UNKNOWN.equalsIgnoreCase(ip)) {
                ip = request.getRemoteAddress().getAddress().getHostAddress();
                if (LOCALHOST_IP1.equalsIgnoreCase(ip) || LOCALHOST_IP.equalsIgnoreCase(ip)) {
                    //根据网卡取本机配置的IP
                    InetAddress iNet = null;
                    try {
                        iNet = InetAddress.getLocalHost();
                    } catch (UnknownHostException e) {
                        logger.error("getClientIp error: {}", e);
                    }
                    ip = iNet.getHostAddress();
                }
            }
        } catch (Exception e) {
            logger.error("IPUtils ERROR ", e);
        }
        //使用代理,则获取第一个IP地址
        if (!StringUtils.isEmpty(ip) && ip.indexOf(IP_UTILS_FLAG) > 0) {
            ip = ip.substring(0, ip.indexOf(IP_UTILS_FLAG));
        }

        return ip;
    }

调用计数器算法进行限流

@Component
public class IPRuleChainService extends SecurityRuleChainServiceBase implements SecurityRuleChainService {
    private static final Logger logger = LoggerFactory.getLogger(IPRuleChainService.class);

    @Override
    public boolean run(ServerHttpRequest request, ServerHttpResponse response) {
        Rule ipRule = securityRulesConfigurationComponent.getIpRule();
        if (!ipRule.isEnable()) {
            return true;
        }

        try {
            String clientIp = IPUtil.getIpAddr(request);
            // 计数器限流
            boolean isPass = slidingWindowLimitService.pass(clientIp, ipRule.getWindowPeriod(), ipRule.getWindowSize());
            if (!isPass) {

                logger.info("ipLimit|IP被限制|{}", clientIp);
                return false;
            }
        } catch (Exception e) {
            logger.error("ipLimit|IP限制异常|", e);
            return false;
        }
        return true;
    }

    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public String getName() {
        return "IP防护服务";
    }
}

资源安全风控实现

@Component
public class ResourcePathRuleChainService extends SecurityRuleChainServiceBase implements SecurityRuleChainService {
    private static final Logger logger = LoggerFactory.getLogger(ResourcePathRuleChainService.class);

    @Override
    public boolean run(ServerHttpRequest request, ServerHttpResponse response) {
        Rule pathRule = securityRulesConfigurationComponent.getPathRule(request.getURI().getPath());
        if (!pathRule.isEnable()) {
            return true;
        }
        try {
            Long userId = getUserId(request);
			// 获取路径
            String userResourcePath = StringUtil.link(userId, request.getURI().getPath());
            boolean isPass = slidingWindowLimitService.pass(userResourcePath, pathRule.getWindowPeriod(), pathRule.getWindowSize());
            if (!isPass) {

                logger.info("resourcePathLimit|资源路径限制|{}", userResourcePath);
                return false;
            }
        } catch (Exception e) {
            logger.error("resourcePathLimit|资源路径限制异常|", e);
            return false;
        }
        return false;
    }

    @Override
    public int getOrder() {
        return 1;
    }

    @Override
    public String getName() {
        return "资源安全服务";
    }
}