Здравствуйте, это Бо Ян, он же Бо Ян, который любит программирование, хип-хоп и напитки.
Каждый раз, когда наш отдел выпускает основную версию, нам необходимо пройти проверку безопасности компании.
Это не потому, что стандарты тестирования безопасности компании в последнее время повысились, а пользовательскому сервису, за который я отвечаю, было задано 10 проблем с безопасностью за один раз.
Молодец, 3.25 не поехал.
Однако пользовательский центр является основным базовым бизнес-сервисом, и его безопасность данных и стабильность системы чрезвычайно важны.Ошибки обнаруживаются, и мы можем исправлять их только одну за другой.
В этой статье будут проанализированы и объяснены решения трех типичных проблем.
1. Подделка ИС
В процессе ежедневного развития бизнеса нам может понадобиться получить информацию об IP-адресе пользователя интерфейса запроса.
Чтобы предотвратить вход хакеров в систему посредством взрыва, я буду записывать IP-адрес каждого входа пользователя и вводить неправильное имя пользователя или пароль в течение определенного периода времени, и IP-адрес будет заблокирован. Этот IP-адрес больше не может запрашивать интерфейс входа в систему во время блокировки.
Получите логику IP перед ремонтом
static String getIpAddr(HttpServletRequest request) {
if (request == null) {
return "unknown";
}
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Forwarded-For");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
return "0:0:0:0:0:0:0:1".equals(ip) ? LOCAL_IP : ip;
}
С точки зрения использования бизнес-функции в этом коде нет ничего плохого, мы можем получить его изHttpServletRequestдля получения данных IP в пакете.
Но я не нашел, данные IP, которые мы получили, получены из заголовка запроса, и вся информация о сообщении в заголовке запроса может быть подделана через сообщение. Пока атакующие хакеры получают пул IP-адресов и продолжают меняться, наш механизм защиты от взрыва не сработает.
Решения
Честно говоря, чтобы выполнить это требование по получению IP в то время, я также напрямую скопировал приведенный выше код из Baidu и использовал его, когда обнаружил, что его можно использовать.
Я не знаю, что означает значение IP, полученное в заголовке(文中不阐述比如:Proxy-lient-IP这些请求头的含义)
.
К счастью, тест безопасности дал рекомендации по ремонту:
Получение данных IP должно быть получено от remoteAddr.
remote_addr указывается сервером в соответствии с IP-адресом запрашивающего TCP-пакета. Предполагая, что нет прокси от клиента к серверу, веб-сервер (Nginx, Apache и т. д.) установит IP-адрес клиента в remote_addr; если есть прокси, пересылающий HTTP-запросы, веб-сервер установит последний прокси-сервер. IP для удаленного_адреса.
Поскольку все наши сервисы являются унифицированными прокси-серверами nginx, мы можем получить remote_addr в nginx, а затем установить независимый заголовок бизнес-запроса для передачи в пользовательский центр.
1. Увеличьте конфигурацию nginx
2. Реализация кода
/**
* 获取真实ip,防止ip伪造
*
* @param request
* @return
*/
private static String getIpAddrFromRemoteAddr(HttpServletRequest request){
String ip = request.getHeader("X-Real-IP");
if (StringUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
return ip;
}
ip = request.getHeader("X-Forwarded-For");
if (StringUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个IP值,第一个为真实IP。
int index = ip.indexOf(',');
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
} else {
return request.getRemoteAddr();
}
}
2. Войти без кода подтверждения
По сути, все входы в систему будут использовать код подтверждения, чтобы предотвратить очистку интерфейса входа в систему.
Наш продукт не хотел использовать логику проверочного кода в начале, чтобы предотвратить взлом пароля методом перебора. Мы используем логику, согласно которой один и тот же IP не может постоянно выходить из строя, чтобы предотвратить мошенничество, но в соответствии с новой спецификацией тест безопасности все равно не распознает его.
Ни в коем случае, они держат в руках жизнь и смерть наших товаров на полках, могу только добавить функцию проверочного кода.
Существует всего две схемы проверочных кодов: фронтенд генерирует проверочные коды или бэкенд генерирует проверочные коды.
Поскольку наши фронтенд-боссы ленивы, мы можем генерировать проверочные коды только на бэкенде.
Инструмент генерации кода подтверждения, который я выбралHutool, из коробки.
Давайте посмотрим, как Hutool генерирует коды подтверждения.
//定义图形验证码的长、宽、验证码字符数、干扰元素个数
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
//获取验证码的base64
String captchaImage = circleCaptcha.getImageBase64Data();
//获取验证码
String code = circleCaptcha.getCode();
Сгенерированный проверочный код, такой как
Логика проверки передней и задней части упрощенной версии проверочного кода:
1. Интерфейс получения кода подтверждения
Внешняя часть запрашивает внутреннюю часть для создания интерфейса кода проверки, внутренняя часть генерирует код проверки, использует base64 в качестве ключа и сохраняет код кода проверки в качестве значения для повторного использования, а затем возвращает base64 на передний план. -конец
2. Войти
Внешний интерфейс передает код и base64, введенные пользователем, на задний конец и проверяет значение base64 в redis.
3. DDoS-атака
После завершения логики проверочного кода обнаруживается, что точка атаки все еще существует.
Когда серверная часть генерирует код подтверждения, ему необходимо сохранить base64 в качестве ключа Redis в Redis.
В случае частых запросов интерфейса кода проверки большое количество ключей base64 приводит к замедлению отклика redis, а то и к взрыву redis.
это DDos атака
Вообще говоря, это означает, что злоумышленник использует «бройлер» для инициирования большого количества запросов к целевому веб-сайту за короткий период времени, потребляя ресурсы хоста целевого веб-сайта в больших масштабах, что делает его неспособным нормально обслуживать. . Онлайн-игры, интернет-финансы и другие области являются сферами, где часто происходят DDoS-атаки.
Наша компания является охранной компанией и имеет специализированные продукты для обеспечения безопасности, чтобы справиться с этим сценарием.
Если мы не купим соответствующие продукты безопасности, как мы можемУровень приложенияКак насчет предотвращения DDoS-атак?
DDos-атаки — это высокочастотные вредоносные запросы, то есть high concurrency, high concurrency anti-brushing, что вы можете придумать?
Разве это не просто текущий предел?
3.1 Ограничение тока шлюза
Если вы используете шлюз шлюза в качестве записи для бизнес-запросов, вы можете напрямую установить текущий ограничитель, который запрашивает один и тот же URL-адрес с одного и того же IP-адреса в единицу времени.
1. Ограничитель тока
@Configuration
public class LimitConfig {
@Bean
@Primary
KeyResolver hostResolver() {
return exchange ->{
ServerHttpRequest serverHttpRequest = Objects.requireNonNull(exchange.getRequest());
return Mono.just(serverHttpRequest.getLocalAddress().getAddress().getHostAddress()+":"+serverHttpRequest.getURI().getPath());
};
}
}
2. Добавить заводской класс фильтра ограничения тока
@Component
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
public class BaiyanRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<BaiyanRateLimiterGatewayFilterFactory.Config> {
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
public BaiyanRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
KeyResolver defaultKeyResolver) {
super(Config.class);
this.defaultRateLimiter = defaultRateLimiter;
this.defaultKeyResolver = defaultKeyResolver;
}
public KeyResolver getDefaultKeyResolver() {
return defaultKeyResolver;
}
public RateLimiter getDefaultRateLimiter() {
return defaultRateLimiter;
}
@SuppressWarnings("unchecked")
@Override
public GatewayFilter apply(BaiyanRateLimiterGatewayFilterFactory.Config config) {
return new InnerFilter(config,this);
}
/**
* 内部配置加载类
*/
public static class Config {
private KeyResolver keyResolver;
private RateLimiter rateLimiter;
private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;
public KeyResolver getKeyResolver() {
return keyResolver;
}
public BaiyanRateLimiterGatewayFilterFactory.Config setKeyResolver(KeyResolver keyResolver) {
this.keyResolver = keyResolver;
return this;
}
public RateLimiter getRateLimiter() {
return rateLimiter;
}
public BaiyanRateLimiterGatewayFilterFactory.Config setRateLimiter(RateLimiter rateLimiter) {
this.rateLimiter = rateLimiter;
return this;
}
public HttpStatus getStatusCode() {
return statusCode;
}
public BaiyanRateLimiterGatewayFilterFactory.Config setStatusCode(HttpStatus statusCode) {
this.statusCode = statusCode;
return this;
}
}
/**
* 内部类,用于指定限流过滤器的级别
*/
private class InnerFilter implements GatewayFilter, Ordered {
private Config config;
private BaiyanRateLimiterGatewayFilterFactory factory;
InnerFilter(BaiyanRateLimiterGatewayFilterFactory.Config config,BaiyanRateLimiterGatewayFilterFactory factory) {
this.config = config;
this.factory = factory;
}
@Override
@SuppressWarnings("unchecked")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
return resolver.resolve(exchange).flatMap(key ->
limiter.isAllowed(route.getId(), key).flatMap(response -> {
for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
}
if (response.isAllowed()) {
return chain.filter(exchange);
}
ServerHttpResponse rs = exchange.getResponse();
byte[] datas = GsonUtil.gsonToString(Result.error(429,"too many request","访问过快",null))
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = rs.bufferFactory().wrap(datas);
rs.setStatusCode(HttpStatus.UNAUTHORIZED);
rs.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return rs.writeWith(Mono.just(buffer));
}));
}
@Override
public int getOrder() {
return GatewayFilterOrderConstant.RATE_LIMITER_FILTER;
}
}
}
3. Добавьте конфигурацию
spring:
cloud:
gateway:
# 网关路由策略
routes:
- id: auth
uri: lb://auth
predicates:
- Path=/api/**
filters:
#限流配置
- name: BaiyanRateLimiter
args:
# 每秒补充10个
redis-rate-limiter.replenishRate: 10
# 突发20个
redis-rate-limiter.burstCapacity: 20
# 每次请求消耗1个
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@hostResolver}"
3.2 Ограничение тока приложения
Без системы, использующей шлюз, мы можем использовать только АОП, фильтры или перехватчики, чтобы ограничить поток служб с одним приложением.
Идея на самом деле очень похожа на ограничение тока шлюза.
Зрелые схемы ограничения тока включают скользящие окна, ведра с маркерами или дырявые ведра, которые не будут объяснены.
4. Резюме
В этой статье дается подробное описание трех проблем тестирования безопасности, с которыми я столкнулся в своей работе, и анализируются проблемы для пошагового решения.
Проблемы и решения резюмируются следующим образом.
5. Свяжитесь со мной
Если есть неточности в тексте, поправьте меня, текст писать непросто, ставьте лайк, ладно~
WeChat: baiyan_lou
Общественный номер: дядя Бай Ян