Когда я впервые начал работать с куки в ASP.NET Core, меня поразило, насколько отличается работа с ними от классического ASP.NET. В Core все стало более декомпозированным - больше нет удобного доступа через HttpContext.Current. Вместо этого мы имеем дело с модульной структурой, где доступ к куки осуществляется через объекты Request и IHttpContextAccessor. В основе механизма лежит система ключей и значений. Куки - это, по сути, небольшие фрагменты данных, хранящиеся в браузере пользователя. В ASP.NET Core доступ к ним осуществляется через два основных пути:
| C# | 1
2
3
4
5
| // Через IHttpContextAccessor
string cookieValue = _httpContextAccessor.HttpContext.Request.Cookies["key"];
// Напрямую через Request в контроллере
string anotherCookieValue = Request.Cookies["anotherKey"]; |
|
Что интересно, ASP.NET Core меняет парадигму доступа к куки - теперь они доступны через специализированые интерфейсы IRequestCookieCollection и IResponseCookies. Это позволяет более гибко управлять жизненым циклом куки и делает код тестируемым. Внутри платформы работа с куки происходит на уровне конвеера HTTP-запросов. Когда запрос поступает в приложение, middleware-компоненты имеют возможность читать и модифицировать куки до того, как запрос достигнет контроллера. Аналогично, при формировании ответа, middleware может влиять на куки, которые будут отправлены обратно клиенту. Порядок регистрации middleware имеет критическое значение при работе с куки. Если вы поместите middleware аутентификации перед компонентом, который должен проверять куки, могут возникать странные ситуации, когда данные не доступны там, где ожидались. Я на собственном опыте убедился, что такие ошибки бывает непросто отладить - всё выглядит правильно, но почему-то не работает. Жизненый цикл куки в ASP.NET Core выглядит примерно так:
1. Запрос приходит в приложение.
2. CookieMiddleware (внутренний компонент ASP.NET Core) десериализует куки из заголовка Cookie .
3. Создается коллекция Request.Cookies, доступная через HttpContext.
4. Приложение читает/пишет куки через соответствующие интерфейсы.
5. При формировании ответа куки добавляются в заголовок Set-Cookie.
Теперь, когда мы понимаем общую картину, давайте копнем глубже. Под капотом Request.Cookies является простой коллекцией пар ключ-значение, но Response.Cookies - это более сложный механизм. Когда вы вызываете:
| C# | 1
| Response.Cookies.Append("myKey", "myValue"); |
|
Происходит несколько интересных процессов. ASP.NET Core создает внутренний объект, который преобразуется в правильно форматированый HTTP-заголовок Set-Cookie со всеми необходимыми атрибутами. Для записи куки платформа предоставляет несколько методов:
| C# | 1
2
3
4
5
6
7
8
| // Записать куки с основными параметрами
Response.Cookies.Append(key, value, options);
// Удалить куки
Response.Cookies.Delete(key);
// Удалить куки с дополнительными опциями
Response.Cookies.Delete(key, options); |
|
Важной частью механизма является объект CookieOptions, который позволяет настраивать различные параметры куки:
| C# | 1
2
3
4
5
6
7
8
9
| CookieOptions options = new CookieOptions
{
Domain = ".example.com", // Домен куки
Path = "/", // Путь
Expires = DateTime.Now.AddDays(1), // Срок действия
HttpOnly = true, // Недоступность для JavaScript
Secure = true, // Только по HTTPS
SameSite = SameSiteMode.Strict // Защита от CSRF
}; |
|
Когда я только начинал работать с ASP.NET Core, меня сбивала с толку разница между Expires и MaxAge. Первый задает конкретную дату истечения срока, а второй - продолжительность жизни куки в секундах от момента создания. На практике я чаще использую Expires, так как это более предсказуемое поведение.
ASP.NET Core использует паттерн фабрики для создания и настройки куки. Под капотом это выглядит примерно так:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Примерный код из исходников ASP.NET Core (упрощен)
public class ResponseCookies : IResponseCookies
{
private readonly IHeaderDictionary _headers;
private readonly ICookieFactory _cookieFactory;
public void Append(string key, string value, CookieOptions options)
{
var cookieValue = _cookieFactory.CreateCookieHeader(key, value, options);
_headers.Append("Set-Cookie", cookieValue);
}
// Остальные методы
} |
|
Одна из интересных особеностей, о которой редко говорят - ASP.NET Core имеет встроенную защиту от переполнения куки. Если вы попытаетесь записать слишком большое значение, фреймворк выбросит исключение. Это защищает от DoS-атак, когда злоумышленик может попытаться переполнить память сервера через механизм куки.
Что касается IHttpContextAccessor, это мощный инструмент для доступа к контексту HTTP в любом месте приложения. Однако его использование имеет свою цену - он использует AsyncLocal<T>, что может влиять на производительность при интенсивном использовании. Я предпочитаю передавать HttpContext напрямую там, где это возможно, и использовать IHttpContextAccessor только когда действительно нужен доступ к контексту в сервисах.
Интеграция IHttpContextAccessor в приложение выполняется через DI-контейнер:
| C# | 1
2
3
4
5
| public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
// Остальные сервисы
} |
|
После этого вы можете внедрять его в любой сервис:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class MyCookieService
{
private readonly IHttpContextAccessor _contextAccessor;
public MyCookieService(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public string GetCookie(string key)
{
return _contextAccessor.HttpContext?.Request.Cookies[key];
}
} |
|
Когда вы работаете с куки в ASP.NET Core, важно помнить о скоупах куки. Каждый куки имеет домен и путь, определяющие, когда он будет отправляться на сервер. Это может привести к интересным ситуациям, когда куки доступны на одних страницах и недоступны на других. Я провел немало времени, отлаживая проблему, когда куки устанавливались для /account и не были доступны на /dashboard.
Еще один важный аспект, о котором стоит упомянуть - это обработка нескольких куки с одинаковым именем. HTTP спецификация позволяет это, но ASP.NET Core обрабатывает только последнее значение для каждого ключа. Если вам нужно получить все значения, придется работать напрямую с заголовками:
| C# | 1
2
3
4
| var cookieValues = Request.Headers["Cookie"]
.SelectMany(header => header.Split(';'))
.Where(cookie => cookie.TrimStart().StartsWith("myKey="))
.Select(cookie => cookie.Split('=')[1]); |
|
Наконец, стоит обратить внимание на то, как ASP.NET Core обрабатывает куки в middleware. Если вы создаете собственный middleware для работы с куки, помните о порядке выполнения. Middleware работает как луковица: сначала выполняются внешние слои (в порядке добавления), а затем внутренние (в обратном порядке). Это означает, что если вы хотите проверить куки перед аутентификацией, ваш middleware должен быть добавлен после middleware аутентификации.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| public void Configure(IApplicationBuilder app)
{
// Этот middleware выполнится первым при запросе
// и последним при ответе
app.UseMiddleware<MyCookieMiddleware>();
// Этот middleware выполнится после MyCookieMiddleware при запросе
// и перед ним при ответе
app.UseAuthentication();
// Остальные middleware
} |
|
Понимание этих внутренних механизмов поможет вам более эффективно использовать куки в ваших ASP.NET Core приложениях и избежать распространеных ошибок и проблем с производительностью.
Особенности сериализации сложных объектов в куки
Куки в своей природе предназначены для хранения простых пар ключ-значение, но реальные приложения часто требуют сохранения сложных объектов. В отличие от классического ASP.NET, где сериализация в куки была встроена в платформу, ASP.NET Core оставляет этот вопрос на усмотрение разработчика. Давайте разберемся, как эффективно сериализовать объекты для хранения в куки. Когда я впервые попытался сохранить объект в куки в ASP.NET Core, я был немного обескуражен - прямого метода для этого не предусмотрено. Решение оказалось простым: сначала сериализовать объект в строку, а затем сохранить эту строку в куки. Но дьявол, как всегда, кроется в деталях.
Вот несколько основных способов сериализации объектов для хранения в куки:
1. JSON-сериализация - наиболее распространеный подход:
| C# | 1
2
3
4
5
6
7
8
9
10
11
| public void SetObjectInCookie<T>(string key, T value, CookieOptions options = null)
{
string serializedValue = JsonSerializer.Serialize(value);
Response.Cookies.Append(key, serializedValue, options ?? new CookieOptions());
}
public T GetObjectFromCookie<T>(string key)
{
string value = Request.Cookies[key];
return value == null ? default : JsonSerializer.Deserialize<T>(value);
} |
|
2. Base64-кодирование - полезно для бинарных данных или когда нужно гарантировать совместимость с URL:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public void SetBase64InCookie<T>(string key, T value, CookieOptions options = null)
{
var json = JsonSerializer.Serialize(value);
var bytes = Encoding.UTF8.GetBytes(json);
var base64 = Convert.ToBase64String(bytes);
Response.Cookies.Append(key, base64, options ?? new CookieOptions());
}
public T GetBase64FromCookie<T>(string key)
{
var base64 = Request.Cookies[key];
if (string.IsNullOrEmpty(base64))
return default;
var bytes = Convert.FromBase64String(base64);
var json = Encoding.UTF8.GetString(bytes);
return JsonSerializer.Deserialize<T>(json);
} |
|
3. MessagePack - компактная бинарная сериализация для экономии места:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public void SetMessagePackInCookie<T>(string key, T value, CookieOptions options = null)
{
byte[] bytes = MessagePackSerializer.Serialize(value);
string base64 = Convert.ToBase64String(bytes);
Response.Cookies.Append(key, base64, options ?? new CookieOptions());
}
public T GetMessagePackFromCookie<T>(string key)
{
string base64 = Request.Cookies[key];
if (string.IsNullOrEmpty(base64))
return default;
byte[] bytes = Convert.FromBase64String(base64);
return MessagePackSerializer.Deserialize<T>(bytes);
} |
|
Каждый из этих подходов имеет свои плюсы и минусы. JSON прост и понятен, но занимает больше места. Base64 универсален, но увеличивает размер примерно на 33%. MessagePack компактен, но требует дополнительных зависимостей. Однако есть важный момент, о котором я узнал на своем горьком опыте - браузеры имеют ограничение на размер куки. Большинство современых браузеров ограничивают размер одной куки примерно 4 КБ, а общий размер всех куки для домена - около 4-10 КБ (в зависимости от браузера). Превышение этих лимитов может привести к неожиданному поведению - куки могут быть обрезаны или полностью отброшены.
При работе со сложными объектами я обычно следую этим рекомендациям:
1. Сохраняйте только необходимые данные - никакого мусора.
2. Разделяйте большие объекты на несколько куки.
3. Используйте компактные форматы сериализации.
4. Всегда проверяйте результат десериализации на null.
Вот пример разделения большого объекта на несколько куки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public void SetLargeObjectInCookies<T>(string keyPrefix, T value, int maxChunkSize = 3500)
{
string json = JsonSerializer.Serialize(value);
int chunksCount = (int)Math.Ceiling((double)json.Length / maxChunkSize);
for (int i = 0; i < chunksCount; i++)
{
int startIndex = i * maxChunkSize;
int length = Math.Min(maxChunkSize, json.Length - startIndex);
string chunk = json.Substring(startIndex, length);
Response.Cookies.Append($"{keyPrefix}_{i}", chunk, new CookieOptions
{
Expires = DateTime.Now.AddHours(1)
});
}
// Сохраняем количество чанков в отдельной куки
Response.Cookies.Append($"{keyPrefix}_count", chunksCount.ToString(), new CookieOptions
{
Expires = DateTime.Now.AddHours(1)
});
}
public T GetLargeObjectFromCookies<T>(string keyPrefix)
{
if (!Request.Cookies.TryGetValue($"{keyPrefix}_count", out string countStr) ||
!int.TryParse(countStr, out int count))
return default;
StringBuilder jsonBuilder = new StringBuilder();
for (int i = 0; i < count; i++)
{
if (!Request.Cookies.TryGetValue($"{keyPrefix}_{i}", out string chunk))
return default;
jsonBuilder.Append(chunk);
}
return JsonSerializer.Deserialize<T>(jsonBuilder.ToString());
} |
|
Для централизованного управления куки я часто использую паттерн Репозиторий. Это позволяет инкапсулировать всю логику работы с куки в одном месте и предоставить чистый интерфейс для остальной части приложения.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| public interface ICookieRepository
{
void Set<T>(string key, T value, TimeSpan? expiration = null);
T Get<T>(string key);
void Remove(string key);
bool Exists(string key);
}
public class CookieRepository : ICookieRepository
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CookieRepository(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void Set<T>(string key, T value, TimeSpan? expiration = null)
{
CookieOptions options = new CookieOptions();
if (expiration.HasValue)
options.Expires = DateTime.Now.Add(expiration.Value);
string serializedValue = JsonSerializer.Serialize(value);
_httpContextAccessor.HttpContext.Response.Cookies.Append(key, serializedValue, options);
}
public T Get<T>(string key)
{
string value = _httpContextAccessor.HttpContext.Request.Cookies[key];
if (string.IsNullOrEmpty(value))
return default;
return JsonSerializer.Deserialize<T>(value);
}
public void Remove(string key)
{
_httpContextAccessor.HttpContext.Response.Cookies.Delete(key);
}
public bool Exists(string key)
{
return _httpContextAccessor.HttpContext.Request.Cookies.ContainsKey(key);
}
} |
|
Такой репозиторий регистрируется в DI-контейнере:
| C# | 1
| services.AddSingleton<ICookieRepository, CookieRepository>(); |
|
И затем может использоваться в любом месте приложения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class UserService
{
private readonly ICookieRepository _cookieRepository;
public UserService(ICookieRepository cookieRepository)
{
_cookieRepository = cookieRepository;
}
public void SaveUserPreferences(UserPreferences preferences)
{
_cookieRepository.Set("user_preferences", preferences, TimeSpan.FromDays(30));
}
public UserPreferences GetUserPreferences()
{
return _cookieRepository.Get<UserPreferences>("user_preferences") ?? new UserPreferences();
}
} |
|
Что касается производительности сериализации, здесь стоит учитывать несколько факторов. JSON-сериализация относительно медленная, особенно если используется полная рефлексия. System.Text.Json в .NET 6+ показывает хорошую производительность, но все равно уступает бинарным форматам. MessagePack может быть до 5-10 раз быстрее и компактнее JSON, что особенно важно для куки, где размер имеет значение. При выборе формата сериализации я рекомендую учитывать не только производительность, но и совместимость. Если ваши куки должны быть доступны для JavaScript на клиенте, JSON будет лучшим выбором. Если куки используются только на сервере и важна производительность - рассмотрите MessagePack или Protobuf.
Нельзя забывать и о безопасности. Сериализованые данные в куки могут быть легко прочитаны и изменены пользователем. Если вы храните чувствительные данные, их необходимо шифровать или подписывать. Мы подробнее рассмотрим это в разделе о безопасности, но вот простой пример подписи данных:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public string SignData(string data, string secretKey)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
{
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
byte[] hashBytes = hmac.ComputeHash(dataBytes);
string hash = Convert.ToBase64String(hashBytes);
return $"{data}.{hash}";
}
}
public bool VerifySignedData(string signedData, string secretKey)
{
var parts = signedData.Split('.');
if (parts.Length != 2)
return false;
string data = parts[0];
string providedHash = parts[1];
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey)))
{
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
byte[] computedHashBytes = hmac.ComputeHash(dataBytes);
string computedHash = Convert.ToBase64String(computedHashBytes);
return providedHash == computedHash;
}
} |
|
Для ещё большей оптимизации я иногда применяю сжатие данных перед сохранением в куки. Этот подход особенно полезен для больших объектов, когда даже после разделения на чанки размер остаётся проблемой:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| public string CompressAndEncode(string input)
{
if (string.IsNullOrEmpty(input))
return input;
using (var memoryStream = new MemoryStream())
{
using (var gzipStream = new GZipStream(memoryStream, CompressionLevel.Optimal))
{
using (var writer = new StreamWriter(gzipStream))
{
writer.Write(input);
}
}
return Convert.ToBase64String(memoryStream.ToArray());
}
}
public string DecodeAndDecompress(string compressedInput)
{
if (string.IsNullOrEmpty(compressedInput))
return compressedInput;
byte[] bytes = Convert.FromBase64String(compressedInput);
using (var memoryStream = new MemoryStream(bytes))
{
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
{
using (var reader = new StreamReader(gzipStream))
{
return reader.ReadToEnd();
}
}
}
} |
|
На практике я обнаружил, что сжатие может уменьшить размер JSON-данных на 60-80%, в зависимости от их структуры. Это значительно расширяет возможности по хранению объектов в куки.
Ещё один аспект, заслуживающий внимания - это версионирование объектов. В реальных проектах структура данных может меняться со временем, и куки, сохраненные старой версией приложения, могут быть несовместимы с новой. Я разработал простой, но эффективный подход:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public void SetVersionedObject<T>(string key, T value, int version, CookieOptions options = null)
{
var wrapper = new VersionedObject<T>
{
Version = version,
Data = value
};
string json = JsonSerializer.Serialize(wrapper);
Response.Cookies.Append(key, json, options ?? new CookieOptions());
}
public T GetVersionedObject<T>(string key, int currentVersion)
{
string json = Request.Cookies[key];
if (string.IsNullOrEmpty(json))
return default;
VersionedObject<T> wrapper = JsonSerializer.Deserialize<VersionedObject<T>>(json);
// Если версия устарела, конвертируем или возвращаем значение по умолчанию
if (wrapper.Version < currentVersion)
{
// Здесь может быть логика миграции данных между версиями
return default;
}
return wrapper.Data;
}
private class VersionedObject<T>
{
public int Version { get; set; }
public T Data { get; set; }
} |
|
При работе с куки важно учитывать настройки CookieOptions, о которых часто забывают. Например, параметр IsEssential определяет, считается ли куки необходимым для функционирования сайта, что важно в контексте GDPR и политики согласия на использование куки:
| C# | 1
| options.IsEssential = true; // Куки будет установлен даже если пользователь не дал согласие на необязательные куки |
|
Самостоятельно разрабатывая механизмы сериализации, я натолкнулся на несколько интересных проблем. Одна из них связана с тем, как разные браузеры обрабатывают юникод-символы в куки. Chrome и Firefox обрабатывают их корректно, а вот Internet Explorer может создавать проблемы. Поэтому иногда лучше явно кодировать строки:
| C# | 1
2
3
4
5
6
7
8
9
| public string EncodeForCookie(string value)
{
return HttpUtility.UrlEncode(value);
}
public string DecodeFromCookie(string value)
{
return HttpUtility.UrlDecode(value);
} |
|
Ещё одна малоизвестная особенность - это взаимодействие с куки из JavaScript. Если вы хотите, чтобы сериализованные данные были доступны с клиентской стороны, нужно избегать установки флага HttpOnly:
| C# | 1
| options.HttpOnly = false; // Куки будет доступен из JavaScript |
|
Но учтите, что это делает куки уязвимыми для XSS-атак.
Альтернативой классической сериализации в куки может быть использование локального хранилища (LocalStorage) вместе с API для обмена данными. В таком подходе в куки хранится только идентификатор, а сами данные передаются через API:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // Установка идентификатора в куки
string sessionId = Guid.NewGuid().ToString();
Response.Cookies.Append("session_id", sessionId);
// Сохранение данных в кэше на сервере
_cache.Set(sessionId, largeObject, TimeSpan.FromHours(1));
// Контроллер для получения данных
[HttpGet("api/user-data")]
public ActionResult<T> GetUserData()
{
string sessionId = Request.Cookies["session_id"];
if (string.IsNullOrEmpty(sessionId))
return Unauthorized();
if (_cache.TryGetValue(sessionId, out T data))
return data;
return NotFound();
} |
|
Этот подход решает проблему ограничения размера куки и позволяет более гибко управлять данными, хотя и требует дополнительных запросов.
Наконец, стоит упомянуть о потенциальных уязвимостях при десериализации данных из куки. Поскольку куки контролируются пользователем, злоумышленик может попытаться подделать их содержимое, что может привести к атакам на десериализацию. Я всегда следую принципу "никогда не доверяй входным данным" и использую валидацию:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| public T GetObjectSafely<T>(string key) where T : class
{
try
{
string value = Request.Cookies[key];
if (string.IsNullOrEmpty(value))
return default;
// Установка лимита на глубину вложенности и размер строк
var options = new JsonSerializerOptions
{
MaxDepth = 10,
// В System.Text.Json нет прямого ограничения на размер,
// но можно реализовать собственный JsonConverter
};
T result = JsonSerializer.Deserialize<T>(value, options);
// Дополнительная валидация объекта
if (!IsValid(result))
return default;
return result;
}
catch
{
// Логирование ошибки
return default;
}
}
private bool IsValid<T>(T obj) where T : class
{
if (obj == null)
return false;
// Здесь может быть специфичная для типа валидация
return true;
} |
|
При работе со сложными объектами и их сериализацией в куки важно найти баланс между удобством, производительностью и безопасностю. Тщательное проектирование моделей данных, оптимальный выбор формата сериализации и правильные настройки куки позволят избежать большинства проблем.
Cookie set-cookie с сервера в response ASP.NET не устанавливаются в браузер Доброго дня, пишу React приложение и бэк на ASP.NET Core 7.0
Реализовал авторизацию JWT с... Браузер отклоняет добавление Cookie через Set-Cookie У меня есть клиент (Blazor Web Assembly - https://localhost:7101), сервер (Asp.Net Core -... Cookie (не выводятся русские символы) Делаю 1:
public void cooki(string S)
{
Response.Cookies.Value = "вава";
... Cookie Добрый день. Надо сделать так, что бы при первом запросе к сайту сохранялись Cookie и потом когда...
Практические сценарии создания и чтения куки
Разберем реальные сценарии использования куки в ASP.NET Core. За годы работы я выработал несколько подходов, которые помогают избежать типичных проблем и сделать код более надежным.
Начнем с базового класса для управления куки, который я использую практически в каждом проекте. Это существенно упрощенная версия того, что было в предыдущем разделе, но с дополнительной обработкой ошибок:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| public class CookieManager
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<CookieManager> _logger;
public CookieManager(IHttpContextAccessor httpContextAccessor, ILogger<CookieManager> logger)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public void Set(string key, string value, int? expirationMinutes = null)
{
try
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
_logger.LogWarning("Попытка установить куки без доступного HttpContext");
return;
}
var options = new CookieOptions();
if (expirationMinutes.HasValue)
{
options.Expires = DateTime.Now.AddMinutes(expirationMinutes.Value);
}
context.Response.Cookies.Append(key, value, options);
_logger.LogDebug("Куки {Key} успешно установлен", key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при установке куки {Key}", key);
throw;
}
}
public string Get(string key)
{
try
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
_logger.LogWarning("Попытка получить куки без доступного HttpContext");
return null;
}
if (context.Request.Cookies.TryGetValue(key, out string value))
{
return value;
}
_logger.LogDebug("Куки {Key} не найден", key);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при получении куки {Key}", key);
return null;
}
}
public void Remove(string key)
{
try
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
_logger.LogWarning("Попытка удалить куки без доступного HttpContext");
return;
}
context.Response.Cookies.Delete(key);
_logger.LogDebug("Куки {Key} успешно удален", key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при удалении куки {Key}", key);
throw;
}
}
} |
|
Обратите внимание на несколько ключевых моментов:
1. Проверка наличия HttpContext - это защита от ситуаций, когда куки используются вне контекста HTTP-запроса.
2. Использование TryGetValue вместо индексатора для безопасного получения значений.
3. Интеграция с системой логирования для отслеживания операций с куки.
Типичным сценарием использования куки является "запоминание" пользовательских предпочтений. Например, вы можете сохранять выбраную пользователем тему оформления:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class ThemeService
{
private const string ThemeCookieKey = "user_theme";
private readonly CookieManager _cookieManager;
public ThemeService(CookieManager cookieManager)
{
_cookieManager = cookieManager;
}
public void SetTheme(string theme)
{
if (string.IsNullOrWhiteSpace(theme))
{
throw new ArgumentException("Тема не может быть пустой", nameof(theme));
}
// Валидация допустимых значений
if (!new[] { "light", "dark", "system" }.Contains(theme.ToLower()))
{
throw new ArgumentException("Недопустимая тема", nameof(theme));
}
// Сохраняем на год
_cookieManager.Set(ThemeCookieKey, theme, 60 * 24 * 365);
}
public string GetTheme()
{
return _cookieManager.Get(ThemeCookieKey) ?? "system";
}
} |
|
А вот пример реализации языковых предпочтений с использованием куки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
| public class LocalizationMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LocalizationMiddleware> _logger;
public LocalizationMiddleware(RequestDelegate next, ILogger<LocalizationMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
// Попытка получить язык из куки
if (context.Request.Cookies.TryGetValue("culture", out string cultureName))
{
// Проверка на валидность культуры
if (IsValidCulture(cultureName))
{
var culture = new CultureInfo(cultureName);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
_logger.LogDebug("Установлена культура {Culture} из куки", cultureName);
}
else
{
_logger.LogWarning("Обнаружена некорректная культура в куки: {Culture}", cultureName);
}
}
// Если в куки нет, смотрим заголовок Accept-Language
else if (context.Request.Headers.ContainsKey("Accept-Language"))
{
var headerValue = context.Request.Headers["Accept-Language"].ToString();
var languages = headerValue.Split(',')
.Select(x => x.Split(';').First().Trim())
.ToList();
foreach (var lang in languages)
{
if (IsValidCulture(lang))
{
var culture = new CultureInfo(lang);
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
// Сохраняем в куки для следующих запросов
context.Response.Cookies.Append("culture", lang, new CookieOptions
{
Expires = DateTime.Now.AddMonths(3),
IsEssential = true // Важно для GDPR
});
_logger.LogDebug("Установлена культура {Culture} из заголовка Accept-Language", lang);
break;
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при обработке локализации");
// Не прерываем конвейер запросов
}
await _next(context);
}
private bool IsValidCulture(string cultureName)
{
if (string.IsNullOrWhiteSpace(cultureName))
return false;
try
{
// Проверяем, что такая культура существует
CultureInfo.GetCultureInfo(cultureName);
return true;
}
catch (CultureNotFoundException)
{
return false;
}
}
} |
|
Этот middleware автоматически устанавливает язык приложения на основе предпочтений пользователя, сохраненных в куки, или заголовка Accept-Language. Обратите внимание на флаг IsEssential = true, который указывает, что эти куки важны для функционирования сайта в контексте GDPR.
Еще один распространенный сценарий - отслеживание согласия пользователя с политикой использования куки (cookie consent):
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| public class CookieConsentService
{
private const string ConsentCookieKey = "cookie_consent";
private readonly CookieManager _cookieManager;
public CookieConsentService(CookieManager cookieManager)
{
_cookieManager = cookieManager;
}
public bool HasConsent()
{
var consent = _cookieManager.Get(ConsentCookieKey);
return !string.IsNullOrEmpty(consent) && consent.Equals("accepted", StringComparison.OrdinalIgnoreCase);
}
public void SetConsent(bool accepted)
{
// Сохраняем на год
_cookieManager.Set(ConsentCookieKey, accepted ? "accepted" : "declined", 60 * 24 * 365);
}
public void ResetConsent()
{
_cookieManager.Remove(ConsentCookieKey);
}
public Dictionary<string, bool> GetDetailedConsent()
{
var detailedConsent = _cookieManager.Get("detailed_cookie_consent");
if (string.IsNullOrEmpty(detailedConsent))
return new Dictionary<string, bool>();
try
{
return JsonSerializer.Deserialize<Dictionary<string, bool>>(detailedConsent);
}
catch
{
return new Dictionary<string, bool>();
}
}
public void SetDetailedConsent(Dictionary<string, bool> consentOptions)
{
var json = JsonSerializer.Serialize(consentOptions);
_cookieManager.Set("detailed_cookie_consent", json, 60 * 24 * 365);
}
} |
|
Куки часто используются для реализации функционала "Remember Me" при аутентификации:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
| public class RememberMeService
{
private const string RememberTokenKey = "remember_token";
private readonly CookieManager _cookieManager;
private readonly IUserRepository _userRepository;
private readonly ILogger<RememberMeService> _logger;
public RememberMeService(
CookieManager cookieManager,
IUserRepository userRepository,
ILogger<RememberMeService> logger)
{
_cookieManager = cookieManager;
_userRepository = userRepository;
_logger = logger;
}
public async Task CreateRememberMeTokenAsync(int userId)
{
try
{
// Генерируем случайный токен
var token = GenerateSecureToken();
// Хешируем для хранения в БД
var hashedToken = HashToken(token);
// Сохраняем в БД связку userId + hashedToken
await _userRepository.SaveRememberTokenAsync(userId, hashedToken, DateTime.Now.AddMonths(1));
// Создаем строку для куки: userId:token
var cookieValue = $"{userId}:{token}";
// Сохраняем в куки на месяц
_cookieManager.Set(RememberTokenKey, cookieValue, 60 * 24 * 30);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при создании токена 'запомнить меня' для пользователя {UserId}", userId);
throw;
}
}
public async Task<int?> ValidateRememberMeTokenAsync()
{
try
{
var cookieValue = _cookieManager.Get(RememberTokenKey);
if (string.IsNullOrEmpty(cookieValue))
return null;
var parts = cookieValue.Split(':');
if (parts.Length != 2 || !int.TryParse(parts[0], out int userId))
return null;
var token = parts[1];
var hashedToken = HashToken(token);
// Проверяем токен в БД
var isValid = await _userRepository.ValidateRememberTokenAsync(userId, hashedToken);
return isValid ? userId : null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при проверке токена 'запомнить меня'");
return null;
}
}
public void ClearRememberMeToken()
{
_cookieManager.Remove(RememberTokenKey);
}
private string GenerateSecureToken()
{
using var rng = RandomNumberGenerator.Create();
var tokenData = new byte[32]; // 256 bit
rng.GetBytes(tokenData);
return Convert.ToBase64String(tokenData);
}
private string HashToken(string token)
{
using var sha = SHA256.Create();
var tokenBytes = Encoding.UTF8.GetBytes(token);
var hashBytes = sha.ComputeHash(tokenBytes);
return Convert.ToBase64String(hashBytes);
}
} |
|
Этот сервис обеспечивает безопасное создание и проверку "токенов запоминания", используя подход с разделением токена: в куки хранится сам токен, а в базе данных - его хеш. Это защищает от украденных куки, так как злоумышленник не сможет воссоздать токен из его хеша.
Для удобного отладки куки и диагностики проблем я часто создаю специальный middleware:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
| public class CookieDebugMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CookieDebugMiddleware> _logger;
private readonly bool _isEnabled;
public CookieDebugMiddleware(
RequestDelegate next,
ILogger<CookieDebugMiddleware> logger,
IConfiguration configuration)
{
_next = next;
_logger = logger;
_isEnabled = configuration.GetValue<bool>("Debug:CookieLogging");
}
public async Task InvokeAsync(HttpContext context)
{
if (_isEnabled)
{
// Логируем все входящие куки
var requestCookies = context.Request.Cookies;
if (requestCookies.Count > 0)
{
var cookieInfo = new StringBuilder("Входящие куки: ");
foreach (var cookie in requestCookies)
{
// Не логируем полное содержимое чувствительных куки
var value = IsSensitiveCookie(cookie.Key)
? "[Скрыто для безопасности]"
: cookie.Value.Substring(0, Math.Min(20, cookie.Value.Length)) + (cookie.Value.Length > 20 ? "..." : "");
cookieInfo.Append($"{cookie.Key}={value}; ");
}
_logger.LogDebug(cookieInfo.ToString());
}
else
{
_logger.LogDebug("Запрос не содержит куки");
}
// Отслеживаем все куки, устанавливаемые в ответе
var originalResponseCookies = context.Response.Cookies;
var cookiesList = new List<(string Key, string Value, CookieOptions Options)>();
// Заменяем стандартную реализацию на свою
context.Response.Cookies = new CookieLoggingWrapper(originalResponseCookies, cookiesList);
try
{
await _next(context);
}
finally
{
// Логируем все установленные куки
if (cookiesList.Count > 0)
{
var cookieInfo = new StringBuilder("Исходящие куки: ");
foreach (var (key, value, options) in cookiesList)
{
var valueToLog = IsSensitiveCookie(key)
? "[Скрыто для безопасности]"
: value.Substring(0, Math.Min(20, value.Length)) + (value.Length > 20 ? "..." : "");
cookieInfo.Append($"{key}={valueToLog} (Expires: {options.Expires}); ");
}
_logger.LogDebug(cookieInfo.ToString());
}
}
}
else
{
await _next(context);
}
}
private bool IsSensitiveCookie(string key)
{
var sensitiveKeys = new[] { "auth", "token", "session", "remember" };
return sensitiveKeys.Any(k => key.ToLower().Contains(k));
}
// Обертка для логирования куки
private class CookieLoggingWrapper : IResponseCookies
{
private readonly IResponseCookies _originalCookies;
private readonly List<(string Key, string Value, CookieOptions Options)> _cookiesList;
public CookieLoggingWrapper(
IResponseCookies originalCookies,
List<(string Key, string Value, CookieOptions Options)> cookiesList)
{
_originalCookies = originalCookies;
_cookiesList = cookiesList;
}
public void Append(string key, string value, CookieOptions options)
{
_cookiesList.Add((key, value, options ?? new CookieOptions()));
_originalCookies.Append(key, value, options);
}
public void Append(string key, string value)
{
var options = new CookieOptions();
_cookiesList.Add((key, value, options));
_originalCookies.Append(key, value);
}
public void Delete(string key, CookieOptions options)
{
_cookiesList.Add((key, "[Deleted]", options ?? new CookieOptions()));
_originalCookies.Delete(key, options);
}
public void Delete(string key)
{
var options = new CookieOptions();
_cookiesList.Add((key, "[Deleted]", options));
_originalCookies.Delete(key);
}
}
} |
|
Этот middleware логирует все входящие и исходящие куки, что чрезвычайно полезно при отладке. Заметьте, что он учитывает безопасность и не записывает полное содержимое чувствительных куки в логи.
Для валидации данных в куки можно использовать подход с "канарейками" - добавляем некоторую контрольную информацию, которую потом проверяем:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| public class SecureCookieService
{
private readonly CookieManager _cookieManager;
private readonly string _validationKey;
public SecureCookieService(CookieManager cookieManager, IConfiguration configuration)
{
_cookieManager = cookieManager;
_validationKey = configuration["Security:CookieValidationKey"];
}
public void SetSecure(string key, string value, int? expirationMinutes = null)
{
// Добавляем метку времени и "канарейку" для защиты
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var data = $"{value}|{timestamp}|{ComputeCanary(value, timestamp)}";
_cookieManager.Set(key, data, expirationMinutes);
}
public string GetSecure(string key, TimeSpan? maxAge = null)
{
var data = _cookieManager.Get(key);
if (string.IsNullOrEmpty(data))
return null;
var parts = data.Split('|');
if (parts.Length != 3)
return null;
var value = parts[0];
var timestamp = long.Parse(parts[1]);
var canary = parts[2];
// Проверяем "канарейку"
if (canary != ComputeCanary(value, timestamp))
return null;
// Проверяем срок годности, если указан
if (maxAge.HasValue)
{
var age = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp;
if (age > maxAge.Value.TotalSeconds)
return null;
}
return value;
}
private string ComputeCanary(string value, long timestamp)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_validationKey));
var data = Encoding.UTF8.GetBytes($"{value}|{timestamp}");
var hash = hmac.ComputeHash(data);
return Convert.ToBase64String(hash).Substring(0, 8); // Используем только часть хеша
}
} |
|
Для работы с зашифрованными данными в куки я разработал специальный сервис на основе Data Protection API, который входит в ASP.NET Core:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| public class EncryptedCookieService
{
private readonly CookieManager _cookieManager;
private readonly IDataProtector _protector;
public EncryptedCookieService(
CookieManager cookieManager,
IDataProtectionProvider dataProtectionProvider)
{
_cookieManager = cookieManager;
// Создаем протектор с определенным назначением
_protector = dataProtectionProvider.CreateProtector("EncryptedCookies");
}
public void SetEncrypted(string key, string value, int? expirationMinutes = null)
{
if (string.IsNullOrEmpty(value))
{
_cookieManager.Remove(key);
return;
}
var encryptedValue = _protector.Protect(value);
_cookieManager.Set(key, encryptedValue, expirationMinutes);
}
public string GetEncrypted(string key)
{
var encryptedValue = _cookieManager.Get(key);
if (string.IsNullOrEmpty(encryptedValue))
return null;
try
{
return _protector.Unprotect(encryptedValue);
}
catch (CryptographicException)
{
// Невозможно расшифровать - возможно, ключи были изменены
// или данные повреждены
_cookieManager.Remove(key); // Удаляем поврежденную куки
return null;
}
}
} |
|
Data Protection API обеспечивает надежное шифрование с минимальными усилиями со стороны разработчика и автоматическое управление ключами. Это гарантирует, что даже если злоумышленик получит доступ к куки, он не сможет прочитать их содержимое.
Еще один практический сценарий - использование куки для отслеживания шагов в многоэтапных процессах, например, при оформлении заказа:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
| public class CheckoutStateService
{
private const string CheckoutCookieKey = "checkout_state";
private readonly EncryptedCookieService _cookieService;
public CheckoutStateService(EncryptedCookieService cookieService)
{
_cookieService = cookieService;
}
public CheckoutState GetState()
{
var stateJson = _cookieService.GetEncrypted(CheckoutCookieKey);
if (string.IsNullOrEmpty(stateJson))
return new CheckoutState { CurrentStep = CheckoutStep.Cart };
try
{
return JsonSerializer.Deserialize<CheckoutState>(stateJson);
}
catch
{
// При ошибке десериализации возвращаем состояние по умолчанию
return new CheckoutState { CurrentStep = CheckoutStep.Cart };
}
}
public void SaveState(CheckoutState state)
{
if (state == null)
{
_cookieService.SetEncrypted(CheckoutCookieKey, null);
return;
}
// Устанавливаем время последнего обновления
state.LastUpdated = DateTime.UtcNow;
var stateJson = JsonSerializer.Serialize(state);
_cookieService.SetEncrypted(CheckoutCookieKey, stateJson, 60); // Срок жизни 1 час
}
public void MoveToNextStep(CheckoutStep nextStep)
{
var state = GetState();
state.CurrentStep = nextStep;
state.StepHistory.Add(new CheckoutStepInfo
{
Step = nextStep,
Timestamp = DateTime.UtcNow
});
SaveState(state);
}
public void ResetCheckout()
{
_cookieService.SetEncrypted(CheckoutCookieKey, null);
}
}
public class CheckoutState
{
public CheckoutStep CurrentStep { get; set; }
public List<CheckoutStepInfo> StepHistory { get; set; } = new List<CheckoutStepInfo>();
public DateTime LastUpdated { get; set; }
// Другие поля состояния оформления заказа
}
public class CheckoutStepInfo
{
public CheckoutStep Step { get; set; }
public DateTime Timestamp { get; set; }
}
public enum CheckoutStep
{
Cart,
Address,
Shipping,
Payment,
Review,
Complete
} |
|
Для работы с корзиной покупок в интернет-магазине куки также часто используются, особенно для неавторизованных пользователей:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
| public class CartService
{
private const string CartCookieKey = "shopping_cart";
private readonly EncryptedCookieService _cookieService;
private readonly IProductRepository _productRepository;
public CartService(
EncryptedCookieService cookieService,
IProductRepository productRepository)
{
_cookieService = cookieService;
_productRepository = productRepository;
}
public async Task<Cart> GetCartAsync()
{
var cartJson = _cookieService.GetEncrypted(CartCookieKey);
if (string.IsNullOrEmpty(cartJson))
return new Cart();
try
{
var cart = JsonSerializer.Deserialize<Cart>(cartJson);
// Обновляем информацию о продуктах из базы данных
await LoadProductDetailsAsync(cart);
return cart;
}
catch
{
return new Cart();
}
}
public async Task AddToCartAsync(int productId, int quantity = 1)
{
var cart = await GetCartAsync();
var existingItem = cart.Items.FirstOrDefault(i => i.ProductId == productId);
if (existingItem != null)
{
existingItem.Quantity += quantity;
}
else
{
// Получаем базовую информацию о продукте
var product = await _productRepository.GetProductBasicInfoAsync(productId);
if (product == null)
throw new ArgumentException($"Продукт с ID {productId} не найден");
cart.Items.Add(new CartItem
{
ProductId = productId,
Quantity = quantity,
ProductName = product.Name,
UnitPrice = product.Price
});
}
cart.LastUpdated = DateTime.UtcNow;
SaveCart(cart);
}
public void SaveCart(Cart cart)
{
if (cart == null)
{
_cookieService.SetEncrypted(CartCookieKey, null);
return;
}
var cartJson = JsonSerializer.Serialize(cart);
// Сохраняем на 30 дней
_cookieService.SetEncrypted(CartCookieKey, cartJson, 60 * 24 * 30);
}
private async Task LoadProductDetailsAsync(Cart cart)
{
// Получаем актуальные данные о продуктах
var productIds = cart.Items.Select(i => i.ProductId).ToList();
var products = await _productRepository.GetProductsBasicInfoAsync(productIds);
foreach (var item in cart.Items)
{
var product = products.FirstOrDefault(p => p.Id == item.ProductId);
if (product != null)
{
item.ProductName = product.Name;
item.UnitPrice = product.Price;
// Обновляем другие поля при необходимости
}
}
}
}
public class Cart
{
public List<CartItem> Items { get; set; } = new List<CartItem>();
public DateTime LastUpdated { get; set; }
public decimal TotalPrice => Items.Sum(i => i.TotalPrice);
public int TotalItems => Items.Sum(i => i.Quantity);
}
public class CartItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice => UnitPrice * Quantity;
} |
|
Куки также могут быть использованы для A/B тестирования и персонализации контента:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class AbTestingService
{
private const string AbTestCookieKeyPrefix = "ab_test_";
private readonly CookieManager _cookieManager;
private readonly Random _random = new Random();
public AbTestingService(CookieManager cookieManager)
{
_cookieManager = cookieManager;
}
public string GetVariant(string experimentName, params string[] variants)
{
if (variants == null || variants.Length == 0)
throw new ArgumentException("Необходимо указать варианты для эксперимента", nameof(variants));
var cookieKey = $"{AbTestCookieKeyPrefix}{experimentName}";
var existingVariant = _cookieManager.Get(cookieKey);
// Если пользователь уже участвует в эксперименте, возвращаем его вариант
if (!string.IsNullOrEmpty(existingVariant) && variants.Contains(existingVariant))
return existingVariant;
// Иначе случайно выбираем вариант
var selectedVariant = variants[_random.Next(variants.Length)];
// Сохраняем в куки на 30 дней
_cookieManager.Set(cookieKey, selectedVariant, 60 * 24 * 30);
return selectedVariant;
}
public void TrackConversion(string experimentName)
{
var cookieKey = $"{AbTestCookieKeyPrefix}{experimentName}";
var variant = _cookieManager.Get(cookieKey);
if (!string.IsNullOrEmpty(variant))
{
// Здесь может быть код для отправки события в аналитическую систему
// или запись в базу данных
// Например:
// _analyticsService.TrackEvent($"ab_test_{experimentName}_conversion", new { variant });
}
}
} |
|
Продвинутые настройки безопасности
Работая с куки в ASP.NET Core, я часто вижу, как разработчики упускают из виду критически важные аспекты безопасности. Думаю, каждый из нас хотя бы раз задавался вопросом: "А насколько уязвимы данные, которые я храню в куки?". На своем опыте могу сказать - без должной настройки уровень защиты стремится к нулю.
HttpOnly, Secure, SameSite - три кита безопасности куки
Начнем с базовых, но невероятно эффективных флагов, которые должны быть настроены для любой куки, содержащей чувствительные данные:
| C# | 1
2
3
4
5
6
| var options = new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict
}; |
|
HttpOnly - когда я только начинал работать с веб-разработкой, я не понимал, насколько важен этот флаг. Он запрещает доступ к куки из JavaScript, что критически важно для предотвращения XSS-атак. Даже если злоумышленник сумеет внедрить вредоносный скрипт на страницу, он не сможет прочитать куки, помеченные как HttpOnly.
Secure - гарантирует, что куки будут передаваться только по защищенному HTTPS-соединению. В современных проектах я всегда включаю этот флаг - без исключений. Даже для девелоперского окружения стараюсь настроить HTTPS.
SameSite - относительно новое, но исключительно полезное свойство. У него есть три значения:- Strict - куки отправляются только при запросах с того же домена,
- Lax - куки отправляются при переходе по ссылкам на этот домен,
- None - куки отправляются всегда (требует установки флага Secure).
Выбор правильного значения SameSite критически важен для защиты от CSRF-атак. Для аутентификационных куки я обычно выбираю Strict, но это может создавать проблемы, если пользователи переходят на ваш сайт по ссылкам из email-рассылок или других сайтов. В таких случаях Lax становится разумным компромисом.
Вот как я обычно настраиваю куки для аутентификации в ASP.NET Core:
| C# | 1
2
3
4
5
6
7
8
9
10
11
| services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
// Другие настройки
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
options.Cookie.Name = "MyApp.Auth";
}); |
|
Защита от CSRF-атак
Cross-Site Request Forgery (CSRF) - одна из самых коварных атак, с которыми я сталкивался. Суть в том, что злоумышленик может заставить пользователя выполнить действия на вашем сайте без его ведома, используя куки аутентификации, которые браузер автоматически отправляет с каждым запросом. ASP.NET Core предоставляет встроенную защиту от CSRF через Anti-Forgery токены. Вот как я обычно настраиваю это:
| C# | 1
2
3
4
5
6
7
8
| services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.Name = "XSRF-TOKEN";
options.Cookie.HttpOnly = false; // Важно: должен быть доступен для JavaScript
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
}); |
|
В контроллерах я затем добавляю атрибут [ValidateAntiForgeryToken] ко всем POST, PUT, DELETE методам:
| C# | 1
2
3
4
5
6
| [HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ProcessForm(FormModel model)
{
// ...
} |
|
А в представлениях добавляю вызов хелпера:
| HTML5 | 1
2
3
4
| <form asp-action="ProcessForm" method="post">
@Html.AntiForgeryToken()
<!-- ... -->
</form> |
|
Для API и SPA-приложений я использую подход с токенами в заголовках:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public class ValidateAntiforgeryTokenMiddleware
{
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
public ValidateAntiforgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
_next = next;
_antiforgery = antiforgery;
}
public async Task InvokeAsync(HttpContext context)
{
if (HttpMethods.IsPost(context.Request.Method) ||
HttpMethods.IsPut(context.Request.Method) ||
HttpMethods.IsDelete(context.Request.Method))
{
await _antiforgery.ValidateRequestAsync(context);
}
else if (HttpMethods.IsGet(context.Request.Method))
{
// Генерируем токен для GET-запросов
var tokens = _antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken,
new CookieOptions
{
HttpOnly = false, // Должен быть доступен для JavaScript
Secure = true,
SameSite = SameSiteMode.Strict
});
}
await _next(context);
}
} |
|
Защита от XSS-атак
Cross-Site Scripting (XSS) позволяет злоумышленнику внедрить JavaScript-код на страницу, который затем может украсть куки, не имеющие флага HttpOnly. Помимо установки этого флага, я всегда использую Content Security Policy (CSP) для дополнительной защиты:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| app.Use(async (context, next) =>
{
context.Response.Headers.Add("Content-Security-Policy",
"default-src 'self'; script-src 'self' https://trusted-cdn.com; " +
"style-src 'self' https://trusted-cdn.com; " +
"img-src 'self' data: https://trusted-cdn.com; " +
"font-src 'self' https://trusted-cdn.com; " +
"connect-src 'self' https://api.myapp.com; " +
"frame-ancestors 'none'; " +
"form-action 'self';");
await next();
}); |
|
CSP существенно ограничивает возможности для проведения XSS-атак, указывая браузеру, из каких источников можно загружать ресурсы и выполнять скрипты.
Для обработки пользовательского ввода я всегда использую встроенные средства экранирования или библиотеки для санитизации HTML:
| C# | 1
2
3
4
5
6
7
8
9
| // В контроллере
public IActionResult DisplayUserContent(string userInput)
{
ViewBag.SanitizedContent = System.Web.HttpUtility.HtmlEncode(userInput);
return View();
}
// В представлении
@Html.Raw(ViewBag.SanitizedContent) |
|
Защита от Session Hijacking и подмены куки
Session Hijacking (перехват сессии) - атака, при которой злоумышленник крадет идентификатор сессии пользователя для получения доступа к его учетной записи. Чтобы минимизировать риск успешной атаки, я использую несколько техник:
1. Ротация идентификаторов сессий - периодическая смена ID сессии, особенно после аутентификации:
| C# | 1
2
3
4
5
| // После успешной аутентификации
HttpContext.Session.Clear();
HttpContext.Response.Cookies.Delete(".AspNetCore.Session");
HttpContext.Features.Set<ISessionFeature>(new SessionFeature());
HttpContext.Session = new DistributedSession(/* ... */); |
|
2. Привязка сессии к IP и User-Agent - для обнаружения подозрительных изменений:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| public class SessionSecurityMiddleware
{
private readonly RequestDelegate _next;
public SessionSecurityMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.User.Identity.IsAuthenticated)
{
var currentIp = context.Connection.RemoteIpAddress?.ToString();
var currentAgent = context.Request.Headers["User-Agent"].ToString();
var sessionIp = context.Session.GetString("UserIP");
var sessionAgent = context.Session.GetString("UserAgent");
if (sessionIp != null && sessionAgent != null)
{
if (sessionIp != currentIp || sessionAgent != currentAgent)
{
// Подозрительное изменение - возможно, перехват сессии
// Сбрасываем аутентификацию
await context.SignOutAsync();
context.Response.Redirect("/Account/Login?suspicious=true");
return;
}
}
else
{
// Сохраняем данные для будущих проверок
context.Session.SetString("UserIP", currentIp);
context.Session.SetString("UserAgent", currentAgent);
}
}
await _next(context);
}
} |
|
3. Использование дополнительного токена безопасности вместе с куки для двойной проверки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // При аутентификации
var securityToken = GenerateSecureToken();
_userRepository.SaveSecurityToken(userId, securityToken);
Response.Cookies.Append("X-Security-Check", securityToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict
});
// В middleware для проверки
if (context.User.Identity.IsAuthenticated)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var securityToken = context.Request.Cookies["X-Security-Check"];
if (string.IsNullOrEmpty(securityToken) ||
!_userRepository.ValidateSecurityToken(userId, securityToken))
{
// Токен отсутствует или недействителен
await context.SignOutAsync();
context.Response.Redirect("/Account/Login");
return;
}
} |
|
Интеграция с ASP.NET Core Identity
ASP.NET Core Identity предоставляет мощный фреймворк для аутентификации и авторизации, который также использует куки. Вот как я обычно настраиваю его безопасность:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Настройки пароля
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequiredLength = 12;
// Настройки блокировки
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
// Настройки пользователей
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Настройка аутентификационных куки
services.ConfigureApplicationCookie(options =>
{
options.Cookie.Name = "MyApp.Identity";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.SlidingExpiration = true;
options.LoginPath = "/Identity/Account/Login";
options.LogoutPath = "/Identity/Account/Logout";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
}); |
|
Для защиты от кражи куки также важно настроить срок жизни токенов доступа. Я часто наблюдаю, как разработчики устанавливают слишком долгий срок действия аутентификационных куки, что повышает риск их компрометации. Оптимальный баланс зависит от уровня безопасности приложения - для банковских систем я использую 15-30 минут, для обычных сервисов - несколько часов.
Работа с внешними провайдерами аутентификации
Интеграция с внешними провайдерами (Google, Facebook, Microsoft) требует особой осторожности при работе с куки. Вот как я настраиваю внешнюю аутентификацию:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
options.SaveTokens = true; // Сохраняем токены для возможного использования API
// Настройка куки для временного хранения состояния
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
options.CorrelationCookie.SameSite = SameSiteMode.Lax; // Нужен Lax для редиректов с внешних сайтов
})
.AddFacebook(options =>
{
// Аналогичные настройки
}); |
|
Обратите внимание на SameSiteMode.Lax для корреляционной куки - это необходимо, чтобы куки отправлялись при редиректе с сайта провайдера обратно на ваш сайт. Установка Strict приведет к проблемам в процессе аутентификации.
Синхронизация куки в кластерных средах
В проектах с несколькими экземплярами приложения синхронизация куки становится критической проблемой. Без правильной настройки пользователь может аутентифицироваться на одном сервере, но при следующем запросе попасть на другой сервер, где его куки не распознаются. Для решения этой проблемы я использую общее хранилище Data Protection API:
| C# | 1
2
3
| services.AddDataProtection()
.PersistKeysToAzureBlobStorage(new Uri("https://mystore.blob.core.windows.net/keys/"))
.ProtectKeysWithAzureKeyVault("https://mykeyvault.vault.azure.net/keys/dataprotection/"); |
|
Или для инфраструктуры на базе Redis:
| C# | 1
2
| services.AddDataProtection()
.PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect("redis-connection-string"), "DataProtection-Keys"); |
|
Это гарантирует, что все экземпляры приложения используют одни и те же ключи шифрования для защиты куки. Без этой настройки каждый экземпляр будет генерировать свои ключи, и куки, зашифрованные на одном сервере, нельзя будет расшифровать на другом.
Мониторинг и аудит операций с куки
Для обнаружения попыток взлома я внедряю мониторинг аномальных операций с куки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| public class SecurityAuditMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<SecurityAuditMiddleware> _logger;
private readonly ISecurityEventService _securityService;
public SecurityAuditMiddleware(
RequestDelegate next,
ILogger<SecurityAuditMiddleware> logger,
ISecurityEventService securityService)
{
_next = next;
_logger = logger;
_securityService = securityService;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
// Проверка на подозрительные модификации куки
if (IsSuspiciousCookieModification(context))
{
_logger.LogWarning("Обнаружена подозрительная модификация куки: {IP}",
context.Connection.RemoteIpAddress);
await _securityService.RecordSecurityEventAsync(
"suspicious_cookie_modification",
context.Connection.RemoteIpAddress.ToString());
}
await _next(context);
}
catch (Exception ex) when (ex is CryptographicException)
{
// Перехватываем исключения, связанные с расшифровкой куки
_logger.LogWarning(ex, "Ошибка при расшифровке куки: {IP}",
context.Connection.RemoteIpAddress);
await _securityService.RecordSecurityEventAsync(
"cookie_decryption_failure",
context.Connection.RemoteIpAddress.ToString());
// Очищаем проблемные куки
foreach (var key in context.Request.Cookies.Keys)
{
if (key.StartsWith(".AspNetCore.") || key.StartsWith("MyApp."))
{
context.Response.Cookies.Delete(key);
}
}
// Перенаправляем на страницу входа
context.Response.Redirect("/Account/Login?securityIssue=true");
}
}
private bool IsSuspiciousCookieModification(HttpContext context)
{
// Реализация проверки на подозрительные модификации
// Например, анализ структуры куки, сравнение с ожидаемыми паттернами и т.д.
return false; // Упрощенная версия
}
} |
|
Управление куки в мобильных приложениях
Особое внимание стоит уделить гибридным мобильным приложениям, использующим WebView. В таких сценариях стандартные механизмы безопасности куки работают иначе:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Настройка для API, обслуживающего мобильные приложения
services.ConfigureApplicationCookie(options =>
{
// Для мобильных приложений может потребоваться более гибкая настройка
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
// Увеличенное время жизни для мобильных приложений
options.ExpireTimeSpan = TimeSpan.FromDays(30);
// Использование заголовка для передачи ошибок аутентификации
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
}); |
|
Я всегда рекомендую сочетать правильные настройки куки с глубокой защитой на уровне приложения, регулярными аудитами безопасности и постоянным мониторингом. Помните, что даже самые защищенные куки могут быть скомпрометированы через другие уязвимости приложения, поэтому безопасность должна быть комплексной и непрерывной.
Архитектурные решения для масштабных приложений
Когда речь заходит о крупных корпоративных приложениях, подход к работе с куки должен быть принципиально иным. Тут уже не обойтись простыми вызовами Request.Cookies - нужна продуманная архитектура, которая выдержит рост команды и кодовой базы. В своей практике я часто применяю паттерн Репозиторий для абстрагирования работы с куки. Но в действительно больших проектах и этого недостаточно. Здесь на помощь приходит Cookie Factory - мощный паттерн, который я активно использую последние пару лет:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| public interface ICookieFactory
{
T Create<T>() where T : ICookieContainer, new();
}
public interface ICookieContainer
{
string Key { get; }
CookieCategory Category { get; }
}
public enum CookieCategory
{
Essential,
Functional,
Analytics,
Marketing
}
public class CookieFactory : ICookieFactory
{
private readonly ICookieManager _cookieManager;
private readonly ICookieConsentService _consentService;
public CookieFactory(ICookieManager cookieManager, ICookieConsentService consentService)
{
_cookieManager = cookieManager;
_consentService = consentService;
}
public T Create<T>() where T : ICookieContainer, new()
{
var container = new T();
// Проверяем, что пользователь согласился на этот тип куки
if (container.Category != CookieCategory.Essential &&
!_consentService.HasConsentFor(container.Category))
{
throw new CookieConsentException($"Пользователь не давал согласие на куки категории {container.Category}");
}
return container;
}
} |
|
Этот паттерн позволяет создавать типизированые куки-контейнеры для разных типов данных. Каждый контейнер - это класс с определенным набором свойств, привязанных к конкретному ключу куки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public class UserPreferencesContainer : ICookieContainer
{
private readonly ICookieManager _cookieManager;
public string Key => "user_preferences";
public CookieCategory Category => CookieCategory.Functional;
// Внедрение зависимостей через конструктор
public UserPreferencesContainer(ICookieManager cookieManager)
{
_cookieManager = cookieManager;
}
public string Theme
{
get => _cookieManager.Get<string>($"{Key}.theme");
set => _cookieManager.Set($"{Key}.theme", value);
}
public string Language
{
get => _cookieManager.Get<string>($"{Key}.language");
set => _cookieManager.Set($"{Key}.language", value);
}
} |
|
Использование фабрики выглядит так:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class UserPreferencesService
{
private readonly ICookieFactory _cookieFactory;
public UserPreferencesService(ICookieFactory cookieFactory)
{
_cookieFactory = cookieFactory;
}
public void SetTheme(string theme)
{
try
{
var preferences = _cookieFactory.Create<UserPreferencesContainer>();
preferences.Theme = theme;
}
catch (CookieConsentException)
{
// Пользователь не дал согласие - используем настройки по умолчанию
}
}
} |
|
Преимущество этого подхода в том, что он естественным образом интегрируется с механизмами согласия на куки (GDPR), дает типобезопасный доступ к данным и централизованно контролирует все операции с куки.
Для авторизации и аутентификации в крупных проектах я рекомендую использовать специализированные куки-контейнеры, которые интегрируются с вашей системой управления доступом:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| public class AuthTokenContainer : ICookieContainer
{
private readonly ICookieManager _cookieManager;
private readonly ITokenValidator _tokenValidator;
public string Key => "auth_tokens";
public CookieCategory Category => CookieCategory.Essential;
public AuthTokenContainer(ICookieManager cookieManager, ITokenValidator tokenValidator)
{
_cookieManager = cookieManager;
_tokenValidator = tokenValidator;
}
public string AccessToken
{
get => _cookieManager.Get<string>($"{Key}.access");
set => _cookieManager.Set($"{Key}.access", value, new CookieOptions {
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTime.Now.AddMinutes(30)
});
}
public bool IsValid => _tokenValidator.ValidateToken(AccessToken);
} |
|
Не забывайте о том, что куки в масштабных приложениях часто становятся узким местом производительности. Я реализую кеширование значений куки внутри запроса, чтобы избежать многократного чтения из коллекции Request.Cookies:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class CachedCookieManager : ICookieManager
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
public string Get(string key)
{
if (_cache.TryGetValue(key, out string cachedValue))
return cachedValue;
var value = _httpContextAccessor.HttpContext?.Request.Cookies[key];
_cache[key] = value;
return value;
}
// Остальные методы...
} |
|
Полный рабочий пример приложения
Давайте соберем все рассмотренные концепции в одном месте. Я подготовил небольшое приложение, которое демонстрирует комплексный подход к работе с куки:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Регистрация основных сервисов
builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
// Регистрация сервисов для работы с куки
builder.Services.AddSingleton<ICookieManager, CookieManager>();
builder.Services.AddScoped<IEncryptedCookieService, EncryptedCookieService>();
builder.Services.AddScoped<ICookieConsentService, CookieConsentService>();
builder.Services.AddScoped<ICookieFactory, CookieFactory>();
// Настройка Data Protection
builder.Services.AddDataProtection()
.SetApplicationName("MyCookieApp");
var app = builder.Build();
// Настройка middleware
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseMiddleware<CookieDebugMiddleware>();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
// Middleware для безопасности
app.UseMiddleware<SecurityAuditMiddleware>();
app.UseMiddleware<LocalizationMiddleware>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
}
} |
|
Это ядро приложения, которое собирает все рассмотренные нами компоненты. Обратите внимание на порядок регистрации middleware - он критически важен для правильной работы с куки.
Для тестирования в разных окружениях я обычно использую разные настройки безопасности. В dev-среде включаю отладочный middleware, а в production активирую все защитные механизмы и шифрование. Этот простой скелет приложения можно адаптировать под любой проект, добавляя конкретные имплементации сервисов в зависимости от ваших потребностей.
Работа c cookie Здравствуйте уважаемые кодеры. У меня такая проблема. Я получил кукисы после запроса с... Как получить cookie с сайта icq.com? Не удаётся получить coockie с сайта icq.com, использую код:
HttpWebRequest myHttpWebRequest =... Cookie Здраствуйте.
Хотел узнать как получить данные из файла Cookie в читабельном виде. Открываю файл... получить Cookie никак не могу получить куки, в консоли пусто
static string GetCookie()
{
string... Как с помощью HttpWebResponse получить cookie? Здравствуйте!
Не подскажите как с помощью HttpWebRequest и HttpWebResponse получить cookie с... Cookie наборы Вопрос пока больше теоретический. К практике ещё не приступал.
Можно ли сделать так что для... cookie запись и отправка Всем привет!
Требуется выполнить два запроса.
При осуществлении первого запроса сервер создает... HttpWebRequest авторизация, проблема с cookie Всем привет!
Пытаюсь с помощью компонента HttpWebRequest авторизоваться на одном сайте.
... Пример использования Cookie Не получается!!!!
Пришлите пожалуйста хотя бы малюсенький примерчик использования Cookie. Но не... Когда сохраняем в cookie сессионные переменные сбрасываются? Это опять я. Все получилось, все сохранилось в cookie? но переключаясь на другую страницу у меня... Запись русских символов в Cookie Страница в unicode. Ввожу в текстовое поле слово (на русском языке),
потом запоминаю его в Cookie,... I have a problem with cookie. I have a problem with cookie.
I create some cookie in lava script function on the client side. ...
|