背景
平台级防控是当前对抗黑灰产的有效手段,各大企业都有自己的安全风控产品,提供各种不同的安全服务,例如,阿里云的运营反诈骗系统,腾讯的天御系统,网易的易盾等。
但是也是不可或缺的,至少在针对非专业级灰产时,应用级防护仍然可能发挥关键专用。应用级防护应该从流量限制,人机安全,业务安全三个方面入手。
实现方式
流量限制
- sentinel 流量限制
Sentinel提供了丰富的功能特性,如流量控制、异常熔断、集群限流和速率控制等。此外,Sentinel也提供了丰富的数据采集如RPC、HTTP以及API Gateway等。
参考文章:www.yuque.com/ezreal-fizp…
- IP 地址限制
根据用户的IP进行限流,同一IP短时间内多次访问,只允许某一次访问通过。
人机安全
- 普通验证码
比如常见的文字验证码、算术验证码和答题验证码等。此类验证码在离散普通用户的前端流量方面作用明显,其原因在于,现有的OCR技术和打码平台的发展,早已攻克普通验证码那点事。
- 人机验证码
近些年国内互联网企业也纷纷启用滑块验证码,通过采集过程中的多种数据特征(访问评率、轨迹、地理位置和历史记录等多维度数据),进行分析是否为机器。
业务安全
在设计产品逻辑时要严谨,比如活动要设置特定的购买金额、用户人群限制等,即使平台级防护能力再强,一旦产品逻辑存在严重漏洞,那也白忙活。
风控代码简易实现
主要是基于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 "资源安全服务";
}
}