Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Форма логина на AngularJS с ASP.NET, часть 3

Запись от UnmanagedCoder размещена 29.07.2025 в 21:40. Обновил(-а) UnmanagedCoder 29.07.2025 в 21:41
Показов 3863 Комментарии 0

Нажмите на изображение для увеличения
Название: Форма логина на AngularJS с ASP.NET 3.jpg
Просмотров: 493
Размер:	85.3 Кб
ID:	11020
Форма логина на AngularJS с ASP.NET, часть 1
Форма логина на AngularJS с ASP.NET, часть 2
Форма логина на AngularJS с ASP.NET, часть 3
Форма логина на AngularJS с ASP.NET, часть 4

Асинхронные запросы и индикаторы загрузки



Одна из самых досадных ошибок, которую я встречаю в интерфейсах авторизации, — полное отсутствие обратной связи в момент отправки формы. Пользователь жмет на кнопку "Войти", и... ничего не происходит. Никаких признаков того, что запрос вообще отправился. Особенно это раздражает на медленных соединениях, когда проверка учетных данных может занять несколько секунд.

В AngularJS работа с асинхронными запросами строится вокруг сервиса $http и механизма промисов. Правильная обработка таких запросов и отображение состояния загрузки — это не просто "фича", а базовый компонент качественного пользовательского опыта.

Работа с асинхронными запросами в AngularJS



В контексте формы авторизации асинхронность проявляется в момент отправки учетных данных на сервер. Вот как это выглядит в сервисе аутентификации:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular.module('app.auth').factory('LoginService', function($http) {
  var service = {};
  
  service.getUserDetails = function(userData) {
    return $http({
      url: '/api/auth/login',
      method: 'POST',
      data: JSON.stringify(userData),
      headers: {
        'content-type': 'application/json'
      }
    });
  };
  
  return service;
});
Метод getUserDetails возвращает промис, который будет разрешен с ответом от сервера или отклонен в случае ошибки. В контроллере мы обрабатываем этот промис:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$scope.LoginForm = function() {
  $scope.isLoading = true; // Включаем индикатор загрузки
  
  LoginService.getUserDetails($scope.UserModel)
    .then(function(response) {
      // Обработка успешного ответа
      $scope.isLoading = false;
      $scope.successMessage = "Добро пожаловать, " + response.data.FullName;
    })
    .catch(function(error) {
      // Обработка ошибки
      $scope.isLoading = false;
      $scope.errorMessage = "Ошибка при входе: " + (error.data.message || "Неизвестная ошибка");
    });
};
Ключевой момент здесь — флаг isLoading, который мы устанавливаем перед отправкой запроса и сбрасываем после его завершения (независимо от результата).

Я помню один случай, когда забыл добавить .catch() и установить isLoading = false при ошибке. В результате, если сервер возвращал ошибку, индикатор загрузки "зависал" навсегда, блокируя форму. Пользователи были вынуждены перезагружать страницу, чтобы попробовать снова.

Индикаторы загрузки: от простого к сложному



Самый простой способ показать индикатор загрузки — условное отображение элемента:

HTML5
1
2
3
4
5
6
<button type="submit" class="btn btn-primary" ng-disabled="isLoading">
  <span ng-if="isLoading">
    <i class="fa fa-spinner fa-spin"></i> Проверка...
  </span>
  <span ng-if="!isLoading">Войти</span>
</button>
Но для более сложных сценариев я создаю специальную директиву:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
angular.module('app.common').directive('loadingIndicator', function() {
  return {
    restrict: 'E',
    scope: {
      isLoading: '=',
      size: '@?',
      message: '@?'
    },
    template: 
      '<div class="loading-container" ng-if="isLoading">' +
        '<div class="spinner" ng-class="size"></div>' +
        '<div class="message" ng-if="message">{{message}}</div>' +
      '</div>',
    link: function(scope) {
      scope.size = scope.size || 'medium';
    }
  };
});
Такой подход позволяет стандартизировать внешний вид индикаторов загрузки по всему приложению.

Блокировка формы во время загрузки



Важный момент — блокировка формы на время запроса, чтобы пользователь не мог отправить данные повторно до получения ответа:

HTML5
1
2
3
4
5
6
7
8
9
10
<form name="loginForm" ng-submit="login()" novalidate>
  <fieldset ng-disabled="isLoading">
    <!-- Поля формы -->
  </fieldset>
  
  <button type="submit" class="btn btn-primary" ng-disabled="isLoading || loginForm.$invalid">
    <span ng-if="isLoading"><i class="fa fa-spinner fa-spin"></i> Вход...</span>
    <span ng-if="!isLoading">Войти</span>
  </button>
</form>
Обратите внимание на <fieldset ng-disabled="isLoading"> — это блокирует все поля формы, пока идет запрос.

Глобальный индикатор запросов



Для крупных приложений полезно иметь глобальный индикатор, который показывает, что происходит запрос, независимо от конкретной формы. В AngularJS это легко реализовать через HTTP-интерцептор:

JavaScript
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
angular.module('app').config(function($httpProvider) {
  $httpProvider.interceptors.push(function($q, $rootScope) {
    var activeRequests = 0;
    
    return {
      request: function(config) {
        activeRequests++;
        $rootScope.isLoading = true;
        return config;
      },
      
      response: function(response) {
        activeRequests--;
        $rootScope.isLoading = activeRequests > 0;
        return response;
      },
      
      responseError: function(rejection) {
        activeRequests--;
        $rootScope.isLoading = activeRequests > 0;
        return $q.reject(rejection);
      }
    };
  });
});
А в шаблоне главной страницы:

HTML5
1
2
3
<div class="global-loading-indicator" ng-if="isLoading">
  <div class="spinner"></div>
</div>
Я использовал такой подход в приложении, где пользователи жаловались на "непонятное" поведение сайта при медленном интернете. После добавления глобального индикатора количество жалоб заметно снизилось — пользователи стали понимать, что система не "зависла", а просто обрабатывает запрос.

Оптимистичный UI и отложенная валидация



Для еще лучшего пользовательского опыта я иногда применяю подход "оптимистичного UI" — когда мы предполагаем успешное выполнение операции и обновляем интерфейс еще до получения ответа от сервера:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$scope.login = function() {
  var credentials = angular.copy($scope.credentials);
  
  // Сбрасываем форму и показываем оптимистичное сообщение
  $scope.credentials = {email: '', password: ''};
  $scope.statusMessage = "Выполняется вход...";
  
  authService.login(credentials)
    .then(function(user) {
      $scope.statusMessage = "Вход выполнен успешно!";
      $timeout(function() {
        $location.path('/dashboard');
      }, 500);
    })
    .catch(function(error) {
      // В случае ошибки возвращаем данные в форму
      $scope.credentials = credentials;
      $scope.statusMessage = "Ошибка: " + error.message;
    });
};
Этот подход особенно хорош для приложений, где большинство операций завершается успешно. Он создает ощущение мгновенной реакции, даже если реальная обработка занимает время.

Правильная обработка асинхронных запросов и информативные индикаторы загрузки — это те мелочи, которые отличают профессионально сделанный интерфейс от любительского. Не экономьте на них — ваши пользователи скажут вам спасибо.

ASP .NET Отправка форма логина, если страница логина представлена asp:Content
Здравствуйте! Имеется страница логиа. Хочу отправить данные методу класса Login.cs, однако форму...

Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2
Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными...

ASP.NET Core: разный формат даты контроллера ASP.NET и AngularJS
Собственно, проблему пока еще не разруливал, но уже погуглил. Разный формат даты который использует...

ASP.NET MVC 4,ASP.NET MVC 4.5 и ASP.NET MVC 5 большая ли разница между ними?
Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...


Обеспечение безопасности передачи данных



Поговорим о том, без чего любая форма авторизации превращается в решето для хакеров — о безопасной передаче данных. За годы работы я насмотрелся на приложения, где пароли гуляли по сети в открытом виде, словно это не секретная информация, а прогноз погоды. И каждый раз мне хотелось спросить у разработчиков: "А вы сами бы доверили свои банковские реквизиты такому сайту?"

HTTPS — базовый уровень защиты



Первый и самый очевидный шаг к безопасной передаче данных — настройка HTTPS. Это шифрованный протокол, который защищает ваши данные в пути от клиента к серверу. Без него все ваши пароли и токены можно перехватить простейшими сниферами.

Настройка HTTPS в проде раньше была настоящей головной болью. Приходилось покупать дорогие сертификаты, проходить сложную проверку, а потом еще и настраивать веб-сервер. Сейчас, к счастью, ситуация изменилась благодаря Let's Encrypt и другим провайдерам бесплатных сертификатов. Вот как выглядит настройка HTTPS в конфигурационном файле web.config для ASP.NET:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
<system.webServer>
  <rewrite>
    <rules>
      <rule name="HTTP to HTTPS redirect" stopProcessing="true">
        <match url="(.*)" />
        <conditions>
          <add input="{HTTPS}" pattern="off" ignoreCase="true" />
        </conditions>
        <action type="Redirect" redirectType="Permanent" url="https://{HTTP_HOST}/{R:1}" />
      </rule>
    </rules>
  </rewrite>
</system.webServer>
Этот код автоматически перенаправляет все HTTP-запросы на HTTPS, обеспечивая шифрование трафика. Даже если пользователь введет адрес с http://, он все равно будет переброшен на защищенное соединение.
В продакшене я всегда иду дальше и настраиваю HSTS (HTTP Strict Transport Security), который говорит браузеру всегда использовать HTTPS для вашего домена:

XML
1
2
3
4
5
6
7
<system.webServer>
  <httpProtocol>
    <customHeaders>
      <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
    </customHeaders>
  </httpProtocol>
</system.webServer>

Защита от атак "человек посередине"



Но даже HTTPS можно обойти, если атакующий сможет перехватить и подменить сертификат (так называемая атака "человек посередине" или MITM). Для дополнительной защиты я использую Certificate Pinning — технику, при которой клиент проверяет, что сертификат сервера соответствует ожидаемому. В AngularJS это можно реализовать следующим образом:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module('app').config(function($httpProvider) {
  $httpProvider.interceptors.push(function($q) {
    return {
      'responseError': function(rejection) {
        // Проверяем, что произошла ошибка SSL
        if (rejection.status === -1 && rejection.xhrStatus === "error") {
          // Логируем подозрительную активность
          console.error("Возможная MITM-атака: SSL ошибка");
          
          // Показываем предупреждение пользователю
          alert("Внимание! Обнаружена проблема с безопасностью соединения. Пожалуйста, не передавайте никаких данных.");
        }
        return $q.reject(rejection);
      }
    };
  });
});
Конечно, это простейшая реализация, в реальных проектах я использую более сложные механизмы с проверкой отпечатков сертификатов.

Защита от XSS и внедрения скриптов



Следующая угроза, о которой нужно позаботиться — это Cross-Site Scripting (XSS). Если ваше приложение уязвимо к XSS, злоумышленник может внедрить вредоносный JavaScript-код, который украдет токены аутентификации из localStorage или сессии. AngularJS по умолчанию предоставляет защиту от XSS, экранируя весь ввод пользователя. Но есть случаи, когда эту защиту можно случайно отключить:

JavaScript
1
2
3
4
// Опасно! Возможна XSS-атака
$scope.trustHtml = function(html) {
  return $sce.trustAsHtml(html);
};
В ASP.NET на стороне сервера я всегда устанавливаю заголовки безопасности, которые добавляют дополнительный уровень защиты:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void Application_BeginRequest()
{
  // Защита от XSS
  Response.Headers.Add("X-XSS-Protection", "1; mode=block");
  
  // Защита от кликджекинга
  Response.Headers.Add("X-Frame-Options", "DENY");
  
  // Защита от MIME-сниффинга
  Response.Headers.Add("X-Content-Type-Options", "nosniff");
  
  // Content Security Policy
  Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self'; connect-src 'self'");
}
Content Security Policy (CSP) особенно эффективен, поскольку позволяет указать, откуда можно загружать ресурсы, что значительно снижает риск XSS-атак.

Защита токенов и чувствительных данных на клиенте



Даже если вы используете HTTPS и все заголовки безопасности, остается вопрос: где и как хранить токены аутентификации на клиенте? У нас есть несколько вариантов:
1. Cookies с флагом HttpOnly и Secure — браузер не даст JavaScript-коду получить доступ к таким кукам, что защищает от XSS.
2. localStorage/sessionStorage — удобно, но уязвимо к XSS, так как JavaScript имеет полный доступ к хранилищу.
3. Память приложения — токен хранится только в переменной JavaScript и теряется при перезагрузке страницы.
В большинстве моих проектов я использую комбинированный подход: короткоживущий токен доступа в памяти приложения и долгоживущий refresh-токен в HttpOnly cookie.

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Сервис для работы с токенами
angular.module('app.auth').factory('tokenService', function($http) {
  var accessToken = null; // Храним в памяти
  
  return {
    setAccessToken: function(token) {
      accessToken = token;
    },
    
    getAccessToken: function() {
      return accessToken;
    },
    
    refreshTokens: function() {
      // Refresh-токен автоматически отправляется в запросе как HttpOnly cookie
      return $http.post('/api/auth/refresh')
        .then(function(response) {
          accessToken = response.data.accessToken;
          return accessToken;
        });
    }
  };
});
На серверной стороне ASP.NET устанавливаем secure cookie для refresh-токена:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public ActionResult IssueTokens(User user)
{
  var accessToken = _tokenService.GenerateAccessToken(user);
  var refreshToken = _tokenService.GenerateRefreshToken(user);
  
  // Устанавливаем refresh-токен в защищенную куку
  Response.Cookies.Append("refresh_token", refreshToken, new CookieOptions
  {
      HttpOnly = true,
      Secure = true,
      SameSite = SameSiteMode.Strict,
      Expires = DateTimeOffset.Now.AddDays(30)
  });
  
  // Возвращаем только access-токен
  return Json(new { accessToken = accessToken });
}

Защита от CSRF-атак



Cross-Site Request Forgery (CSRF) — это атака, при которой злоумышленник заставляет аутентифицированного пользователя выполнить нежелательное действие. Для защиты я использую анти-CSRF токены:

C#
1
2
3
4
5
6
7
// В контроллере
[ValidateAntiForgeryToken]
[HttpPost]
public ActionResult ChangePassword(PasswordChangeModel model)
{
    // Логика смены пароля
}
На клиенте в AngularJS:

JavaScript
1
2
3
4
angular.module('app').config(function($httpProvider) {
  $httpProvider.defaults.xsrfCookieName = 'XSRF-TOKEN';
  $httpProvider.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
});
А в ASP.NET настраиваем генерацию CSRF-токена:

C#
1
2
3
4
5
6
7
8
9
10
public void ConfigureServices(IServiceCollection services)
{
  services.AddAntiforgery(options => 
  {
      options.HeaderName = "X-XSRF-TOKEN";
      options.Cookie.Name = "XSRF-TOKEN";
      options.Cookie.HttpOnly = false; // Важно! JavaScript должен иметь доступ к этой куке
      options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
  });
}
Тут важно отметить, что куки с CSRF-токеном не должны иметь флаг HttpOnly, иначе JavaScript не сможет прочитать токен и добавить его в заголовок запроса.

Безопасность передачи данных — это не разовая настройка, а постоянный процесс. Регулярно обновляйте свои знания о новых уязвимостях, используйте инструменты сканирования безопасности и проводите пентесты. Помните: у безопасности нет кнопки "включить", это результат комплексного подхода и постоянного внимания к деталям.

Шифрование паролей и защита от CSRF-атак



Помню, как несколько лет назад я подключился к проекту по аудиту безопасности платежной системы. Заказчик был уверен, что у них все в порядке, но первое, что я обнаружил в их базе данных — пароли в открытом виде! А на вопрос о защите от CSRF получил недоуменный взгляд и вопрос: "А что это такое?". Именно тогда я понял, насколько недооценены эти базовые механизмы защиты даже в серьезных проектах.

Почему нельзя хранить пароли в открытом виде



Если вы все еще сравниваете пароли напрямую, как в примере из начала статьи:

C#
1
2
var user = db.Users.Where(x => x.Email.Equals(obj.Email) && 
                            x.Password.Equals(obj.Password)).FirstOrDefault();
То остановитесь прямо сейчас! Этот код — приглашение для хакеров. Утечка базы данных, инсайдеры, SQL-инъекции — и все пароли ваших пользователей окажутся в открытом доступе. А учитывая, что люди часто используют одинаковые пароли на разных сайтах, последствия могут быть катастрофическими.

Правильное хеширование паролей в ASP.NET



Вместо хранения паролей в открытом виде, нужно использовать односторонние криптографические хеш-функции с солью. В современном ASP.NET Identity для этого есть класс PasswordHasher:

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
public class UserService
{
    private readonly UserManager<ApplicationUser> _userManager;
    
    public UserService(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }
    
    public async Task<bool> ValidateUserAsync(string email, string password)
    {
        var user = await _userManager.FindByEmailAsync(email);
        if (user == null)
            return false;
            
        return await _userManager.CheckPasswordAsync(user, password);
    }
    
    public async Task<IdentityResult> CreateUserAsync(string email, string password)
    {
        var user = new ApplicationUser { UserName = email, Email = email };
        return await _userManager.CreateAsync(user, password);
    }
}
Если вы не используете 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
35
36
37
38
39
40
41
public static class PasswordHasher
{
    // Генерация хеша с солью
    public static string HashPassword(string password)
    {
        byte[] salt = new byte[16];
        using (var rng = new RNGCryptoServiceProvider())
        {
            rng.GetBytes(salt);
        }
        
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000);
        byte[] hash = pbkdf2.GetBytes(20);
        
        byte[] hashBytes = new byte[36];
        Array.Copy(salt, 0, hashBytes, 0, 16);
        Array.Copy(hash, 0, hashBytes, 16, 20);
        
        return Convert.ToBase64String(hashBytes);
    }
    
    // Проверка пароля
    public static bool VerifyPassword(string password, string hashedPassword)
    {
        byte[] hashBytes = Convert.FromBase64String(hashedPassword);
        
        byte[] salt = new byte[16];
        Array.Copy(hashBytes, 0, salt, 0, 16);
        
        var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000);
        byte[] hash = pbkdf2.GetBytes(20);
        
        for (int i = 0; i < 20; i++)
        {
            if (hashBytes[i + 16] != hash[i])
                return false;
        }
        
        return true;
    }
}
В этом примере мы используем PBKDF2 с 10,000 итерациями, что делает перебор паролей очень затратным. Каждому паролю назначается уникальная соль, которая защищает от атак с использованием радужных таблиц.

Помню, как одному из клиентов пришлось перехешировать более миллиона паролей после того, как выяснилось, что они использовали MD5 без соли. Миграция заняла неделю и потребовала сброса паролей для части пользователей. Не повторяйте их ошибок!

Что такое CSRF и почему это опасно



CSRF (Cross-Site Request Forgery) — это атака, при которой злоумышленник заставляет авторизованного пользователя выполнить нежелательное действие без его ведома. Классический сценарий: вы авторизованы на банковском сайте, а затем открываете вредоносную страницу, которая автоматически отправляет запрос на перевод денег с вашего счета.

Защита от CSRF в ASP.NET



ASP.NET предоставляет встроенную защиту от CSRF с помощью антиподделочных токенов:

C#
1
2
3
4
5
6
7
8
9
10
11
// В Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddAntiforgery(options => 
    {
        options.HeaderName = "X-XSRF-TOKEN";
        options.Cookie.Name = "XSRF-TOKEN";
        options.Cookie.HttpOnly = false; // JavaScript должен иметь доступ к куке
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    });
}
В контроллерах используем атрибут для защиты:

C#
1
2
3
4
5
6
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult VerifyUser(UserModel model)
{
    // Логика проверки пользователя
}
Генерация токена для клиента:

C#
1
2
3
4
5
6
7
8
9
[Route("api/csrf/token")]
public IActionResult GetAntiforgeryToken()
{
    var tokens = _antiforgery.GetAndStoreTokens(HttpContext);
    Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
        new CookieOptions { HttpOnly = false, Secure = true });
    
    return Ok();
}

Интеграция с AngularJS



AngularJS имеет встроенную поддержку CSRF-защиты. Нужно только настроить имена куки и заголовка:

JavaScript
1
2
3
4
5
angular.module('app').config(function($httpProvider) {
    // Настраиваем имена для CSRF-токена
    $httpProvider.defaults.xsrfCookieName = 'XSRF-TOKEN';
    $httpProvider.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
});
Теперь AngularJS будет автоматически отправлять CSRF-токен со всеми небезопасными запросами (POST, PUT, DELETE и т.д.).

Чтобы это работало, при инициализации приложения нужно получить токен с сервера:

JavaScript
1
2
3
4
angular.module('app').run(function($http) {
    // Получаем CSRF-токен при старте приложения
    $http.get('/api/csrf/token');
});
В реальных проектах я часто добавляю промежуточное ПО, которое автоматически добавляет CSRF-токен в куки при первом запросе:

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 AntiforgeryMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IAntiforgery _antiforgery;
    
    public AntiforgeryMiddleware(RequestDelegate next, IAntiforgery antiforgery)
    {
        _next = next;
        _antiforgery = antiforgery;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        if (context.Request.Method == "GET" && 
            context.Request.Path.Value.StartsWith("/api"))
        {
            // Для API-запросов генерируем токен
            var tokens = _antiforgery.GetAndStoreTokens(context);
            context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, 
                new CookieOptions { HttpOnly = false, Secure = true });
        }
        
        await _next(context);
    }
}

Особый случай: авторизация через JWT



Если ваш API использует токены JWT в заголовке Authorization (а не куки), защита от CSRF может быть не нужна. Это одно из преимуществ JWT: токен передается в заголовке, а не в куках, поэтому не подвержен классическим CSRF-атакам. Однако будьте осторожны: если вы храните JWT в localStorage и вставляете его в заголовки через JavaScript, вы все равно уязвимы для XSS-атак. В этом случае злоумышленник может украсть токен через внедренный JavaScript.

Комплексный подход к безопасности



В одном из проектов мы использовали такую стратегию:
1. Короткоживущий JWT-токен (15 минут) в памяти JavaScript,
2. Долгоживущий refresh-токен в HttpOnly куке,
3. CSRF-защита для операций с refresh-токеном,
4. Хеширование паролей с использованием bcrypt и уникальной солью.

Это обеспечивало хороший баланс между безопасностью и удобством использования. Даже если злоумышленник перехватывал JWT, он имел лимитированное время действия, а для обновления требовался доступ к защищенным кукам.

Я видел множество взломаных систем и могу с уверенностью сказать: большинство из них пренебрегали базовыми мерами безопасности. Правильное хеширование паролей и защита от CSRF — это минимум, который должен быть в любом продакшн-приложении. Это не те места, где стоит экономить время на разработку.

Настройка rate limiting и CORS-политик



Помните старый анекдот про замки на двери, которые защищают только от честных людей? В веб-разработке есть похожая ситуация: многие механизмы безопасности работают только если злоумышленник "играет по правилам". Но настоящие хакеры правил не соблюдают, и тут нам на помощь приходят rate limiting и правильные CORS-политики — инструменты, которые эффективно противостоят наиболее распостраненным типам атак.

Rate limiting: когда количество переходит в "нет"



Rate limiting (ограничение частоты запросов) — это механизм, который позволяет контролировать, сколько запросов может сделать клиент за определенный промежуток времени. Представьте, что кто-то пытается подобрать пароль к учетной записи, делая тысячи попыток в минуту. Без ограничений ваш сервер с радостью обработает их все, пока злоумышленник не найдет правильную комбинацию.

В одном из банковских проектов я столкнулся с атакой, когда боты делали до 200 попыток входа в секунду! Сервер справлялся с нагрузкой, но это была лишь вопрос времени, когда один из паролей сработал бы. После внедрения rate limiting количество успешных взломов снизилось практически до нуля.

Реализация в ASP.NET



В ASP.NET есть несколько способов реализовать rate limiting. Я предпочитаю использовать 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
public class RateLimitMiddleware
{
    private static readonly Dictionary<string, Queue<DateTime>> _requestStore = 
        new Dictionary<string, Queue<DateTime>>();
    private readonly RequestDelegate _next;
    private readonly int _maxRequests;
    private readonly TimeSpan _interval;
    
    public RateLimitMiddleware(RequestDelegate next, int maxRequests = 3, int intervalInSeconds = 60)
    {
        _next = next;
        _maxRequests = maxRequests;
        _interval = TimeSpan.FromSeconds(intervalInSeconds);
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        var endpoint = context.GetEndpoint()?.DisplayName;
        
        // Применяем только к эндпоинтам авторизации
        if (endpoint != null && endpoint.Contains("auth"))
        {
            var key = GetClientKey(context);
            var now = DateTime.UtcNow;
            
            // Очистка устаревших записей
            CleanupRequests(key, now);
            
            // Проверка лимита
            if (IsRateLimited(key, now))
            {
                context.Response.StatusCode = 429; // Too Many Requests
                context.Response.Headers.Add("Retry-After", _interval.TotalSeconds.ToString());
                await context.Response.WriteAsync("Слишком много запросов. Попробуйте позже.");
                return;
            }
            
            // Регистрация запроса
            RegisterRequest(key, now);
        }
        
        await _next(context);
    }
    
    private string GetClientKey(HttpContext context)
    {
        // Используем IP-адрес + путь запроса как ключ
        return $"{context.Connection.RemoteIpAddress}_{context.Request.Path}";
    }
    
    private void CleanupRequests(string key, DateTime now)
    {
        if (!_requestStore.ContainsKey(key))
        {
            _requestStore[key] = new Queue<DateTime>();
            return;
        }
        
        while (_requestStore[key].Count > 0 && now - _requestStore[key].Peek() > _interval)
        {
            _requestStore[key].Dequeue();
        }
    }
    
    private bool IsRateLimited(string key, DateTime now)
    {
        return _requestStore.ContainsKey(key) && _requestStore[key].Count >= _maxRequests;
    }
    
    private void RegisterRequest(string key, DateTime now)
    {
        if (!_requestStore.ContainsKey(key))
        {
            _requestStore[key] = new Queue<DateTime>();
        }
        
        _requestStore[key].Enqueue(now);
    }
}
Регистрация middleware в Startup.cs:

C#
1
2
3
4
5
6
7
8
9
public void Configure(IApplicationBuilder app)
{
    // Другие middleware...
    
    // Ограничиваем запросы к API авторизации: 5 запросов в минуту
    app.UseMiddleware<RateLimitMiddleware>(5, 60);
    
    // Rest of the pipeline...
}
Обратите внимание на заголовок Retry-After — это хороший тон сообщать клиенту, через сколько можно повторить запрос. Добросовестные клиенты будут уважать этот заголовок, а злоумышленники... ну, им это не поможет, но хотя бы замедлит атаку.

CORS: граници между доменами



CORS (Cross-Origin Resource Sharing) — механизм, позволяющий веб-страницам запрашивать ресурсы с доменов, отличных от того, с которого была загружена сама страница. Без правильной настройки CORS ваш API аутентификации может быть уязвим для атак с других сайтов.

Я работал с одним проектом, где разработчики просто отключили CORS полностью, установив Access-Control-Allow-Origin: *. Это было быстрое решение, но оно позволяло любому сайту взаимодействовать с их API! После проведенного мною аудита безопасности они быстро поменяли подход.

Настройка CORS в ASP.NET



Правильная настройка CORS в ASP.NET выглядит так:

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
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy("AuthCorsPolicy", builder =>
        {
            builder.WithOrigins("https://ваш-фронтенд-домен.com")
                   .AllowCredentials()
                   .WithMethods("GET", "POST")
                   .WithHeaders("Content-Type", "Authorization", "X-XSRF-TOKEN");
        });
    });
    
    // Другие сервисы...
}
 
public void Configure(IApplicationBuilder app)
{
    // Другие middleware...
    
    app.UseCors("AuthCorsPolicy");
    
    // Остальной конвейер...
}
Ключевые моменты здесь:
1. WithOrigins — указываем конкретные домены, а не используем "*".
2. AllowCredentials — разрешаем отправку куки с учетными данными.
3. WithMethods — ограничиваем разрешенные HTTP-методы.
4. WithHeaders — указываем допустимые заголовки.

Работа с CORS в AngularJS



На стороне AngularJS необходимо убедиться, что запросы включают учетные данные:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
angular.module('app.auth').factory('authService', function($http) {
    return {
        login: function(credentials) {
            return $http({
                url: 'https://api.ваш-домен.com/auth/login',
                method: 'POST',
                data: credentials,
                withCredentials: true  // Важно для отправки куков!
            });
        }
    };
});
Параметр withCredentials: true говорит браузеру отправлять куки при кросс-доменных запросах, что необходимо, если вы храните сессии или CSRF-токены в куках.

Эволюция CORS-политик



Иногда требования меняются, и вам нужно добавить новый домен в белый список CORS. Вместо хардкодинга доменов в коде, я предпочитаю использовать конфигурацию:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void ConfigureServices(IServiceCollection services)
{
    var allowedOrigins = Configuration.GetSection("AllowedOrigins").Get<string[]>();
    
    services.AddCors(options =>
    {
        options.AddPolicy("AuthCorsPolicy", builder =>
        {
            builder.WithOrigins(allowedOrigins)
                   .AllowCredentials()
                   .WithMethods("GET", "POST")
                   .WithHeaders("Content-Type", "Authorization", "X-XSRF-TOKEN");
        });
    });
}
И в appsettings.json:

JSON
1
2
3
4
5
6
7
{
  "AllowedOrigins": [
    "https://app.ваш-домен.com",
    "https://admin.ваш-домен.com",
    "https://localhost:4200"  // Для локальной разработки
  ]
}
Такой подход позволяет быстро обновлять список разрешенных доменов без перекомпиляции и редеплоя приложения.

Rate limiting и правильные CORS-политики — это мощные инструменты защиты вашего API аутентификации. Они не только повышают безопасность, но и улучшают производительность, ограничивая бесполезную нагрузку от атак. Не пренебрегайте ими, даже если кажется, что ваше приложение слишком маленькое, чтобы привлечь внимание злоумышленников. Поверьте, боты не выбирают цели — они атакуют всё, что найдут.

Работа с токенами аутентификации и сессиями



Токены и сессии — это тот клей, который связывает разные части системы аутентификации. Когда я только начинал работать с веб-приложениями, мне казалось, что достаточно просто проверить логин с паролем и установить куку с пометкой "авторизован". Но реальность быстро развеяла это заблуждение, особенно когда я столкнулся с необходимостью поддерживать десятки тысяч одновременных сессий и защищать их от перехвата.

Типы токенов аутентификации



Существует несколько подходов к реализации токенов, и каждый имеет свои плюсы и минусы:

1. JWT (JSON Web Tokens) — самоподписанные токены, содержащие всю необходимую информацию и подпись для проверки.
2. Непрозрачные токены — случайные строки, которые служат ключами к данным, хранящимся на сервере.
3. Refresh токены — долгоживущие токены для обновления основных токенов доступа.

В большинстве моих проектов я использую комбинацию JWT для краткосрочного доступа и Refresh токенов для долгосрочной аутентификации. Этот подход позволяет создать систему, где пользователю не нужно постоянно вводить учетные данные, сохраняя при этом высокий уровень безопасности.

Реализация JWT в ASP.NET



Для начала нужно добавить необходимые пакеты:

Bash
1
2
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
Install-Package System.IdentityModel.Tokens.Jwt
Затем настраиваем генерацию токенов:

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
public class JwtService
{
    private readonly string _secret;
    private readonly string _issuer;
    private readonly string _audience;
    private readonly int _expirationMinutes;
 
    public JwtService(IConfiguration configuration)
    {
        _secret = configuration["Jwt:Secret"];
        _issuer = configuration["Jwt:Issuer"];
        _audience = configuration["Jwt:Audience"];
        _expirationMinutes = int.Parse(configuration["Jwt:ExpirationMinutes"]);
    }
 
    public string GenerateToken(User user)
    {
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secret));
        var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
 
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };
 
        var token = new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: claims,
            expires: DateTime.Now.AddMinutes(_expirationMinutes),
            signingCredentials: credentials
        );
 
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
 
    public ClaimsPrincipal ValidateToken(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.UTF8.GetBytes(_secret);
        
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(key),
            ValidateIssuer = true,
            ValidIssuer = _issuer,
            ValidateAudience = true,
            ValidAudience = _audience,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
 
        SecurityToken validatedToken;
        return tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
    }
}
Важный момент — установка ClockSkew = TimeSpan.Zero. По умолчанию JWT дает 5 минут "форы" для учета расхождения во времени между серверами, но это может привести к тому, что уже истекшие токены будут все еще приниматься системой.

Refresh токены для долгосрочной аутентификации



JWT обычно имеет короткий срок жизни (15-30 минут) из соображений безопасности. Но заставлять пользователя логиниться каждые полчаса — не лучший UX. Для решения этой проблемы я использую Refresh токены:

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
public class RefreshTokenService
{
    private readonly ApplicationDbContext _context;
    private readonly JwtService _jwtService;
    private readonly IConfiguration _configuration;
 
    public RefreshTokenService(ApplicationDbContext context, JwtService jwtService, IConfiguration configuration)
    {
        _context = context;
        _jwtService = jwtService;
        _configuration = configuration;
    }
 
    public async Task<string> GenerateRefreshToken(User user)
    {
        var randomNumber = new byte[32];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
        }
        
        var refreshToken = Convert.ToBase64String(randomNumber);
        
        // Сохраняем токен в базе с привязкой к пользователю
        _context.RefreshTokens.Add(new RefreshToken
        {
            Token = refreshToken,
            UserId = user.Id,
            ExpiryDate = DateTime.Now.AddDays(30), // 30 дней на рефреш
            IssuedDate = DateTime.Now,
            IsRevoked = false
        });
        
        await _context.SaveChangesAsync();
        
        return refreshToken;
    }
 
    public async Task<(string accessToken, string refreshToken)> RefreshTokens(string refreshToken)
    {
        var storedToken = await _context.RefreshTokens
            .Include(r => r.User)
            .FirstOrDefaultAsync(r => r.Token == refreshToken && !r.IsRevoked);
        
        if (storedToken == null || storedToken.ExpiryDate < DateTime.Now)
        {
            throw new SecurityException("Invalid or expired refresh token");
        }
        
        // Генерируем новый access token
        var accessToken = _jwtService.GenerateToken(storedToken.User);
        
        // Опционально: выпускаем новый refresh token и отзываем старый
        var newRefreshToken = await GenerateRefreshToken(storedToken.User);
        storedToken.IsRevoked = true;
        await _context.SaveChangesAsync();
        
        return (accessToken, newRefreshToken);
    }
}
Это позволяет реализовать схему, где:
1. Пользователь авторизуется и получает access token (JWT) и refresh token.
2. При истечении access token, клиент автоматически запрашивает новый, используя refresh token.
3. Если refresh token действителен, пользователь получает новую пару токенов без необходимости повторного ввода пароля.

Хранение токенов на клиенте в AngularJS



На клиентской стороне я обычно использую следующий подход:

JavaScript
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
angular.module('app.auth').factory('tokenService', function($window, $http) {
    var service = {};
    
    // Access token храним в памяти
    var accessToken = null;
    
    // Методы для работы с access token
    service.setAccessToken = function(token) {
        accessToken = token;
    };
    
    service.getAccessToken = function() {
        return accessToken;
    };
    
    service.removeAccessToken = function() {
        accessToken = null;
    };
    
    // Метод для обновления токенов
    service.refreshTokens = function() {
        // Refresh token автоматически отправляется как HttpOnly cookie
        return $http.post('/api/auth/refresh')
            .then(function(response) {
                accessToken = response.data.accessToken;
                return accessToken;
            });
    };
    
    return service;
});
Обратите внимание, что access token хранится только в памяти JavaScript, а не в localStorage или sessionStorage. Это защищает от XSS-атак, поскольку злоумышленник не сможет получить доступ к токену даже при успешном внедрении вредоносного кода.

Refresh token, в свою очередь, хранится в HttpOnly cookie, которая недоступна для JavaScript, что защищает от XSS, но делает токен уязвимым для CSRF. Поэтому для операций с refresh token всегда необходимо использовать CSRF-защиту, о которой мы говорили ранее.

Автоматическое обновление токенов



Чтобы пользователь не замечал процесса обновления токенов, я настраиваю HTTP-перехватчик, который перехватывает 401 ошибки и пытается обновить токен:

JavaScript
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
angular.module('app.auth').factory('authInterceptor', function($q, tokenService) {
    var isRefreshing = false;
    var refreshQueue = [];
    
    return {
        // Добавляем токен к исходящим запросам
        request: function(config) {
            var token = tokenService.getAccessToken();
            if (token) {
                config.headers.Authorization = 'Bearer ' + token;
            }
            return config;
        },
        
        // Обрабатываем 401 (Unauthorized) ошибки
        responseError: function(rejection) {
            if (rejection.status !== 401) {
                return $q.reject(rejection);
            }
            
            var deferred = $q.defer();
            
            // Если уже идет обновление, добавляем запрос в очередь
            if (isRefreshing) {
                refreshQueue.push({deferred: deferred, config: rejection.config});
                return deferred.promise;
            }
            
            isRefreshing = true;
            
            // Пытаемся обновить токен
            tokenService.refreshTokens()
                .then(function(newToken) {
                    // Повторяем исходный запрос с новым токеном
                    rejection.config.headers.Authorization = 'Bearer ' + newToken;
                    
                    // Обрабатываем очередь отложенных запросов
                    refreshQueue.forEach(function(item) {
                        item.config.headers.Authorization = 'Bearer ' + newToken;
                        $http(item.config).then(
                            function(response) { item.deferred.resolve(response); },
                            function(err) { item.deferred.reject(err); }
                        );
                    });
                    
                    refreshQueue = [];
                    deferred.resolve($http(rejection.config));
                })
                .catch(function(error) {
                    // Если не удалось обновить токен, отклоняем все запросы
                    refreshQueue.forEach(function(item) {
                        item.deferred.reject(error);
                    });
                    
                    refreshQueue = [];
                    deferred.reject(error);
                    
                    // Перенаправляем на страницу входа
                    $location.path('/login');
                })
                .finally(function() {
                    isRefreshing = false;
                });
            
            return deferred.promise;
        }
    };
});
Этот перехватчик не только обновляет токен при необходимости, но и повторяет все запросы, которые были отклонены из-за устаревшего токена. Пользователь даже не заметит, что происходило обновление токена.

Правильная работа с токенами аутентификации и сессиями — это баланс между безопасностью и удобством использования. Короткоживущие JWT в памяти и долгоживущие refresh токены в HttpOnly куках дают хороший компромисс, защищая от основных типов атак при сохранении плавного пользовательского опыта.

Реализация функции "Запомнить меня" и безопасное хранение учетных данных



Функция "Запомнить меня" — одна из тех мелочей, которые кажутся тривиальными, но при неправильной реализации могут превратиться в серьезную дыру в безопасности. Я не раз сталкивался с сайтами, где эта, казалось бы, простая функция позволяла злоумышленникам получить неавторизованный доступ к аккаунтам.

Что на самом деле означает "Запомнить меня"?



Когда пользователь ставит галочку "Запомнить меня", он ожидает, что при следующем посещении сайта ему не придется вводить учетные данные. Технически это означает, что мы должны сохранить некий идентификатор сессии на устройстве пользователя — обычно в виде долгоживущей куки. Но вот тут-то и скрывается дьявол в деталях. Наивный подход:

JavaScript
1
2
3
4
5
// НЕ ДЕЛАЙТЕ ТАК!!!
if (rememberMe) {
    localStorage.setItem('userId', user.id);
    localStorage.setItem('userEmail', user.email);
}
Это катастрофа с точки зрения безопасности! Любой скрипт на странице сможет прочитать эти данные.

Безопасная реализация в ASP.NET



Правильный подход основан на создании специального уникального токена для функции "Запомнить меня":

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
public class RememberMeService
{
    private readonly ApplicationDbContext _context;
    private readonly IConfiguration _config;
    
    public RememberMeService(ApplicationDbContext context, IConfiguration config)
    {
        _context = context;
        _config = config;
    }
    
    public string GenerateRememberMeToken(int userId)
    {
        var tokenBytes = new byte[64];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(tokenBytes);
        }
        
        var token = Convert.ToBase64String(tokenBytes);
        var selector = Guid.NewGuid().ToString();
        
        // Хешируем токен перед сохранением в базе
        var hashedToken = HashToken(token);
        
        // Сохраняем запись в базе данных
        _context.RememberMeTokens.Add(new RememberMeToken
        {
            Selector = selector,
            HashedToken = hashedToken,
            UserId = userId,
            ExpiryDate = DateTime.UtcNow.AddMonths(3) // Срок действия 3 месяца
        });
        
        _context.SaveChanges();
        
        // Возвращаем комбинацию селектора и токена
        return $"{selector}:{token}";
    }
    
    public int? ValidateRememberMeToken(string tokenString)
    {
        if (string.IsNullOrEmpty(tokenString) || !tokenString.Contains(":"))
            return null;
            
        var parts = tokenString.Split(':');
        var selector = parts[0];
        var token = parts[1];
        
        var storedToken = _context.RememberMeTokens
            .FirstOrDefault(t => t.Selector == selector && t.ExpiryDate > DateTime.UtcNow);
            
        if (storedToken == null)
            return null;
            
        // Проверяем токен
        if (VerifyToken(token, storedToken.HashedToken))
            return storedToken.UserId;
            
        return null;
    }
    
    private string HashToken(string token)
    {
        using (var sha256 = SHA256.Create())
        {
            var bytes = Encoding.UTF8.GetBytes(token + _config["RememberMeSecret"]);
            var hash = sha256.ComputeHash(bytes);
            return Convert.ToBase64String(hash);
        }
    }
    
    private bool VerifyToken(string token, string hashedToken)
    {
        return HashToken(token) == hashedToken;
    }
}
Ключевые моменты здесь:
1. Мы используем два значения: селектор и токен.
2. Селектор хранится в открытом виде и используется для поиска записи в базе.
3. Токен хешируется и только хеш хранится в базе.
4. Пользователю отправляется комбинация селектор:токен.

Почему такая сложность? Потому что прямой поиск по хешу в базе данных невозможен. Селектор позволяет найти нужную запись, а затем мы проверяем токен.

Интеграция с AngularJS



На стороне клиента мы устанавливаем и читаем куки:

JavaScript
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
angular.module('app.auth').factory('authService', function($http, $cookies) {
    var service = {
        login: login,
        logout: logout,
        autoLogin: autoLogin
    };
    
    return service;
    
    function login(credentials, rememberMe) {
        return $http.post('/api/auth/login', {
            email: credentials.email,
            password: credentials.password,
            rememberMe: rememberMe
        }).then(function(response) {
            // Если запрос успешен, сохраняем токен
            storeToken(response.data.token);
            return response.data;
        });
    }
    
    function autoLogin() {
        // Проверяем наличие Remember Me токена
        var rememberMeToken = $cookies.get('remember_me');
        
        if (!rememberMeToken) {
            return $q.reject('No token found');
        }
        
        return $http.post('/api/auth/auto-login', {
            token: rememberMeToken
        }).then(function(response) {
            storeToken(response.data.token);
            return response.data;
        });
    }
    
    function storeToken(token) {
        // Сохраняем токен в памяти приложения
        sessionStorage.setItem('auth_token', token);
    }
    
    function logout() {
        return $http.post('/api/auth/logout').then(function() {
            // Удаляем токены
            sessionStorage.removeItem('auth_token');
            $cookies.remove('remember_me');
        });
    }
});
В контроллере формы логина добавляем опцию "Запомнить меня":

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
angular.module('app.auth').controller('LoginController', function($scope, authService) {
    $scope.credentials = {
        email: '',
        password: ''
    };
    
    $scope.rememberMe = false;
    
    $scope.login = function() {
        authService.login($scope.credentials, $scope.rememberMe)
            .then(function(response) {
                // Успешный вход
            })
            .catch(function(error) {
                // Обработка ошибок
            });
    };
});
А в шаблоне формы:

HTML5
1
2
3
4
5
6
7
<div class="form-group">
    <div class="checkbox">
        <label>
            <input type="checkbox" ng-model="rememberMe"> Запомнить меня
        </label>
    </div>
</div>

Настройка безопасных Cookie



На стороне сервера очень важно правильно настроить параметры куки:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model)
{
    // Аутентификация...
    
    if (model.RememberMe)
    {
        var token = _rememberMeService.GenerateRememberMeToken(user.Id);
        
        Response.Cookies.Append("remember_me", token, new CookieOptions
        {
            HttpOnly = true,  // JavaScript не сможет прочитать
            Secure = true,    // Только по HTTPS
            SameSite = SameSiteMode.Strict,  // Защита от CSRF
            Expires = DateTimeOffset.UtcNow.AddMonths(3)
        });
    }
    
    // Дальнейшая логика...
}
Флаги HttpOnly и Secure критически важны для безопасности. Без них ваши куки могут быть украдены при XSS-атаке или перехвачены через незащищенное соединение.

В моей практике был случай, когда разработчик забыл поставить эти флаги, и это привело к массовой компрометации аккаунтов через уязвимость XSS в форуме сайта. После этого я всегда проверяю настройки куки дважды!

Функция "Запомнить меня" — это удобство, которое не должно идти в ущерб безопасности. При правильной реализации вы можете предоставить пользователям комфорт без компромиссов с безопасностью их данных.

Механизмы обновления токенов и их ротация



Когда я впервые столкнулся с необходимостью реализовать механизм обновления токенов, мне казалось, что это избыточная сложность. Зачем усложнять и без того непростую систему аутентификации? Однако после нескольких инцидентов безопасности в проектах, где токены имели долгий срок жизни, я изменил свое мнение. Правильная стратегия обновления и ротации токенов — это не просто "красивое" решение, а необходимый компонент безопасной системы аутентификации.

Почему короткоживущие токены требуют механизма обновления



Идеальный токен доступа должен жить ровно столько, сколько нужно для выполнения одной операции — не больше. Но в реальности такой подход привел бы к постоянным запросам авторизации и ужасному UX. Поэтому мы идем на компромисс:

1. Access токены живут относительно недолго (15-30 минут).
2. Refresh токены существуют дольше (дни или недели) и служат для получения новых access токенов.

Такая схема позволяет сочетать безопасность с удобством: даже если access токен будет перехвачен, злоумышленник сможет использовать его лишь ограниченное время.

Реализация механизма обновления токенов в ASP.NET



В моей практике я обычно создаю специальный сервис для управления refresh токенами:

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
public class TokenRotationService
{
    private readonly ApplicationDbContext _context;
    private readonly JwtService _jwtService;
    
    public TokenRotationService(ApplicationDbContext context, JwtService jwtService)
    {
        _context = context;
        _jwtService = jwtService;
    }
    
    public async Task<TokenResponse> RotateTokensAsync(string refreshToken)
    {
        // Проверяем, существует ли токен в базе
        var storedToken = await _context.RefreshTokens
            .Include(rt => rt.User)
            .FirstOrDefaultAsync(rt => rt.Token == refreshToken && !rt.IsRevoked);
        
        if (storedToken == null)
            throw new SecurityException("Недействительный refresh токен");
            
        if (storedToken.ExpiryDate < DateTime.UtcNow)
            throw new SecurityException("Истекший refresh токен");
        
        // Отзываем старый токен
        storedToken.IsRevoked = true;
        
        // Создаем новый refresh токен
        var newRefreshToken = await CreateRefreshTokenAsync(storedToken.User);
        
        // Создаем новый access токен
        var accessToken = _jwtService.GenerateToken(storedToken.User);
        
        await _context.SaveChangesAsync();
        
        return new TokenResponse
        {
            AccessToken = accessToken,
            RefreshToken = newRefreshToken.Token,
            ExpiresIn = 900 // 15 минут в секундах
        };
    }
    
    private async Task<RefreshToken> CreateRefreshTokenAsync(User user)
    {
        var token = new RefreshToken
        {
            Token = GenerateUniqueToken(),
            User = user,
            IssuedAt = DateTime.UtcNow,
            ExpiryDate = DateTime.UtcNow.AddDays(30),
            IsRevoked = false
        };
        
        _context.RefreshTokens.Add(token);
        
        return token;
    }
    
    private string GenerateUniqueToken()
    {
        // Генерируем случайный токен, пока не найдем уникальный
        while (true)
        {
            var tokenBytes = new byte[64];
            using (var rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(tokenBytes);
            }
            
            var token = Convert.ToBase64String(tokenBytes);
            
            if (!_context.RefreshTokens.Any(rt => rt.Token == token))
                return token;
        }
    }
}
В этом коде есть несколько важных аспектов:

1. Ротация токенов — мы не просто выдаем новый access токен, но и заменяем refresh токен, что критически важно для безопасности,
2. Отзыв старого токена — старый refresh токен помечается как отозванный, и его больше нельзя использовать,
3. Проверка срока действия — мы проверяем, не истек ли refresh токен.

Интеграция с AngularJS



На клиентской стороне необходимо реализовать автоматическое обновление токенов. Я обычно делаю это через HTTP-перехватчик:

JavaScript
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
angular.module('app.auth').factory('tokenRefreshInterceptor', function($q, $injector) {
    var isRefreshingToken = false;
    var deferredRequests = [];
    
    return {
        responseError: function(rejection) {
            // Если сервер вернул 401 Unauthorized
            if (rejection.status === 401) {
                var tokenService = $injector.get('tokenService');
                var $http = $injector.get('$http');
                
                // Если уже идет обновление токена, добавляем запрос в очередь
                if (isRefreshingToken) {
                    var deferred = $q.defer();
                    deferredRequests.push({ deferred: deferred, config: rejection.config });
                    return deferred.promise;
                }
                
                isRefreshingToken = true;
                
                // Пытаемся обновить токены
                return tokenService.refreshTokens()
                    .then(function(tokens) {
                        // Обновляем заголовок в исходном запросе
                        rejection.config.headers.Authorization = 'Bearer ' + tokens.accessToken;
                        
                        // Повторяем все отложенные запросы с новым токеном
                        deferredRequests.forEach(function(request) {
                            request.config.headers.Authorization = 'Bearer ' + tokens.accessToken;
                            request.deferred.resolve($http(request.config));
                        });
                        
                        deferredRequests = [];
                        
                        // Повторяем исходный запрос
                        return $http(rejection.config);
                    })
                    .catch(function(error) {
                        // Если не удалось обновить токен, отклоняем все запросы
                        deferredRequests.forEach(function(request) {
                            request.deferred.reject(error);
                        });
                        
                        deferredRequests = [];
                        
                        // Перенаправляем на страницу входа
                        var $location = $injector.get('$location');
                        $location.path('/login');
                        
                        return $q.reject(error);
                    })
                    .finally(function() {
                        isRefreshingToken = false;
                    });
            }
            
            return $q.reject(rejection);
        }
    };
});
Важный момент здесь — управление очередью запросов. Если несколько запросов одновременно получат 401, мы не хотим, чтобы каждый из них запускал обновление токенов. Вместо этого первый запрос инициирует обновление, а остальные ждут результата.

Безопасное хранение refresh токенов



Как я уже говорил в предыдущих главах, refresh токены должны храниться в HttpOnly куках для защиты от XSS-атак. Но даже в этом случае остается риск кражи через CSRF. Для дополнительной защиты я применяю следующие меры:

1. Привязка к IP — сохраняем IP-адрес при выдаче токена и проверяем его при обновлении:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public async Task<TokenResponse> RotateTokensAsync(string refreshToken, string ipAddress)
{
    var storedToken = await _context.RefreshTokens
        .FirstOrDefaultAsync(rt => rt.Token == refreshToken);
        
    // Проверяем IP-адрес, если он сохранен
    if (storedToken.IpAddress != null && storedToken.IpAddress != ipAddress)
    {
        // Возможная кража токена! Отзываем все токены пользователя
        await RevokeAllUserTokensAsync(storedToken.UserId);
        throw new SecurityException("Подозрительная активность. Все сессии завершены.");
    }
    
    // Остальная логика...
}
2. Ротация при каждом использовании — refresh токен можно использовать только один раз, после чего он заменяется новым. Это значительно усложняет использование украденного токена.
3. Семейства токенов — при отзыве одного токена из-за подозрительной активности отзываются все токены, выданные в рамках той же сессии:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public async Task RevokeTokenFamilyAsync(string refreshToken)
{
    var token = await _context.RefreshTokens
        .FirstOrDefaultAsync(rt => rt.Token == refreshToken);
        
    if (token == null) return;
    
    // Отзываем все токены с тем же FamilyId
    var familyTokens = await _context.RefreshTokens
        .Where(rt => rt.FamilyId == token.FamilyId)
        .ToListAsync();
        
    foreach (var familyToken in familyTokens)
    {
        familyToken.IsRevoked = true;
    }
    
    await _context.SaveChangesAsync();
}
Этот подход дает дополнительный уровень защиты: если злоумышленник перехватит refresh токен и попытается его использовать, легитимный пользователь не сможет обновить свой токен и заметит проблему. А система сможет отследить факт компрометации.

Механизмы обновления и ротации токенов — это не просто теоретическая предосторожность. В современных веб-приложениях, где пользователи ожидают оставаться авторизованными неделями или даже месяцами, это необходимое условие для обеспечения безопасности без ущерба для удобства.

Реализация двухфакторной аутентификации



Двухфакторная аутентификация (2FA) - это как второй замок на двери вашего приложения. Если первый взломан (логин и пароль скомпрометированы), второй все равно защитит данные пользователя. Я до сих пор удивляюсь, когда вижу проекты, где при бюджете в сотни тысяч долларов, никто даже не подумал о внедрении 2FA. Приходится объяснять клиентам, что дополнительный фактор аутентификации - это не блажь, а необходимость в современном мире, где утечки паролей стали обыденностью.

Типы второго фактора



Второй фактор аутентификации обычно относится к одной из трех категорий:
1. Что-то, что вы получаете - одноразовые коды через SMS или email.
2. Что-то, что у вас есть - аппаратные ключи или мобильные приложения-аутентификаторы.
3. Что-то, что вы собой представляете - биометрические данные (отпечаток пальца, скан лица).
В контексте веб-приложений на AngularJS и ASP.NET наиболее распространены первые два подхода. Давайте рассмотрим, как их реализовать.

Серверная часть: ASP.NET



Для начала необходимо добавить поддержку 2FA в наш бэкенд. Я обычно создаю специальную модель для хранения настроек 2FA:

C#
1
2
3
4
5
6
7
8
9
10
11
public class TwoFactorSettings
{
    public int UserId { get; set; }
    public bool IsEnabled { get; set; }
    public string SecretKey { get; set; }  // Секретный ключ для TOTP
    public string RecoveryCodesJson { get; set; }  // Резервные коды в JSON
    public DateTime LastUpdated { get; set; }
    
    // Навигационное свойство
    public User User { get; set; }
}
Затем сервис для работы с 2FA:

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
public class TwoFactorService
{
    private readonly ApplicationDbContext _context;
    private readonly UserManager<User> _userManager;
    
    public TwoFactorService(ApplicationDbContext context, UserManager<User> userManager)
    {
        _context = context;
        _userManager = userManager;
    }
    
    public async Task<string> GenerateSecretKeyAsync(int userId)
    {
        // Генерируем случайный секретный ключ
        byte[] secretBytes = new byte[20];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(secretBytes);
        }
        
        var secretKey = Base32Encode(secretBytes);
        
        // Сохраняем в базе
        var settings = await _context.TwoFactorSettings.FirstOrDefaultAsync(s => s.UserId == userId);
        if (settings == null)
        {
            settings = new TwoFactorSettings
            {
                UserId = userId,
                IsEnabled = false,
                SecretKey = secretKey,
                LastUpdated = DateTime.UtcNow
            };
            _context.TwoFactorSettings.Add(settings);
        }
        else
        {
            settings.SecretKey = secretKey;
            settings.LastUpdated = DateTime.UtcNow;
        }
        
        await _context.SaveChangesAsync();
        return secretKey;
    }
    
    public async Task<bool> ValidateCodeAsync(int userId, string code)
    {
        var settings = await _context.TwoFactorSettings.FirstOrDefaultAsync(s => s.UserId == userId);
        if (settings == null || !settings.IsEnabled)
            return false;
            
        // Получаем пользователя
        var user = await _userManager.FindByIdAsync(userId.ToString());
        if (user == null)
            return false;
            
        return await _userManager.VerifyTwoFactorTokenAsync(
            user, 
            TokenOptions.DefaultAuthenticatorProvider, 
            code);
    }
    
    // Метод для создания резервных кодов
    public async Task<List<string>> GenerateRecoveryCodesAsync(int userId, int count = 8)
    {
        var codes = new List<string>();
        for (int i = 0; i < count; i++)
        {
            // Генерируем 8-символьный код
            var code = GenerateRandomCode(8);
            codes.Add(code);
        }
        
        var settings = await _context.TwoFactorSettings.FirstOrDefaultAsync(s => s.UserId == userId);
        if (settings != null)
        {
            settings.RecoveryCodesJson = JsonSerializer.Serialize(codes);
            await _context.SaveChangesAsync();
        }
        
        return codes;
    }
    
    private string Base32Encode(byte[] data)
    {
        // Реализация Base32 кодирования
        // (упрощено для краткости)
        return Convert.ToBase64String(data).Replace("=", "").Replace("/", "").Replace("+", "");
    }
    
    private string GenerateRandomCode(int length)
    {
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        var random = new Random();
        return new string(Enumerable.Repeat(chars, length)
            .Select(s => s[random.Next(s.Length)]).ToArray());
    }
}
В контроллере добавляем эндпоинты для управления 2FA:

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
[Route("api/auth/2fa")]
public class TwoFactorController : Controller
{
    private readonly TwoFactorService _twoFactorService;
    
    public TwoFactorController(TwoFactorService twoFactorService)
    {
        _twoFactorService = twoFactorService;
    }
    
    [HttpGet("setup")]
    [Authorize]
    public async Task<IActionResult> Setup()
    {
        var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
        var secretKey = await _twoFactorService.GenerateSecretKeyAsync(userId);
        
        return Ok(new
        {
            SecretKey = secretKey,
            QrCodeUrl = $"otpauth://totp/MyApp:{User.Identity.Name}?secret={secretKey}&issuer=MyApp"
        });
    }
    
    [HttpPost("verify")]
    [Authorize]
    public async Task<IActionResult> Verify([FromBody] VerifyTwoFactorModel model)
    {
        var userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
        var isValid = await _twoFactorService.ValidateCodeAsync(userId, model.Code);
        
        if (!isValid)
            return BadRequest(new { Message = "Неверный код" });
            
        // Включаем 2FA для пользователя
        await _twoFactorService.EnableTwoFactorAsync(userId);
        
        // Генерируем резервные коды
        var recoveryCodes = await _twoFactorService.GenerateRecoveryCodesAsync(userId);
        
        return Ok(new
        {
            Success = true,
            RecoveryCodes = recoveryCodes
        });
    }
    
    [HttpPost("login")]
    public async Task<IActionResult> Login([FromBody] TwoFactorLoginModel model)
    {
        // Проверяем временный токен, полученный после первого этапа входа
        var userId = GetUserIdFromTemporaryToken(model.TempToken);
        if (!userId.HasValue)
            return Unauthorized();
            
        var isValid = await _twoFactorService.ValidateCodeAsync(userId.Value, model.Code);
        if (!isValid)
            return BadRequest(new { Message = "Неверный код аутентификации" });
            
        // Генерируем полноценные токены доступа и обновления
        var tokens = GenerateTokens(userId.Value);
        
        return Ok(tokens);
    }
    
    // Вспомогательные методы опущены для краткости
}

Клиентская часть: AngularJS



На стороне клиента необходимо реализовать две основные функции: настройку 2FA и процесс входа с 2FA. Начнем с сервиса:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
angular.module('app.auth').factory('twoFactorService', function($http) {
    return {
        // Инициализация настройки 2FA
        setup: function() {
            return $http.get('/api/auth/2fa/setup');
        },
        
        // Проверка и активация 2FA
        verify: function(code) {
            return $http.post('/api/auth/2fa/verify', { code: code });
        },
        
        // Вход с использованием 2FA
        login: function(tempToken, code) {
            return $http.post('/api/auth/2fa/login', {
                tempToken: tempToken,
                code: code
            });
        }
    };
});
Контроллер для настройки 2FA:

JavaScript
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
angular.module('app.auth').controller('TwoFactorSetupController', function($scope, twoFactorService, notifyService) {
    $scope.secretKey = '';
    $scope.qrCodeUrl = '';
    $scope.verificationCode = '';
    $scope.recoveryCodes = [];
    $scope.step = 1;
    
    // Инициализация настройки
    $scope.init = function() {
        twoFactorService.setup()
            .then(function(response) {
                $scope.secretKey = response.data.secretKey;
                $scope.qrCodeUrl = response.data.qrCodeUrl;
                $scope.step = 2;
            })
            .catch(function(error) {
                notifyService.error('Ошибка при настройке 2FA');
            });
    };
    
    // Проверка и активация
    $scope.verify = function() {
        if (!$scope.verificationCode) {
            notifyService.warning('Введите код подтверждения');
            return;
        }
        
        twoFactorService.verify($scope.verificationCode)
            .then(function(response) {
                $scope.recoveryCodes = response.data.recoveryCodes;
                $scope.step = 3;
                notifyService.success('Двухфакторная аутентификация успешно активирована');
            })
            .catch(function(error) {
                notifyService.error('Неверный код подтверждения');
            });
    };
});
И наконец, контроллер для входа с 2FA:

JavaScript
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
angular.module('app.auth').controller('TwoFactorLoginController', function($scope, $location, twoFactorService, authService, notifyService) {
    $scope.code = '';
    $scope.tempToken = '';
    $scope.isLoading = false;
    
    // Инициализация из параметров URL
    $scope.init = function() {
        $scope.tempToken = $location.search().token;
        if (!$scope.tempToken) {
            $location.path('/login');
        }
    };
    
    // Отправка кода 2FA
    $scope.submit = function() {
        if (!$scope.code) {
            return;
        }
        
        $scope.isLoading = true;
        
        twoFactorService.login($scope.tempToken, $scope.code)
            .then(function(response) {
                // Сохраняем полученные токены
                authService.setTokens(response.data);
                $location.path('/dashboard').search({});
            })
            .catch(function(error) {
                notifyService.error('Неверный код аутентификации');
            })
            .finally(function() {
                $scope.isLoading = false;
            });
    };
    
    // Инициализация при загрузке контроллера
    $scope.init();
});

Пользовательский опыт при 2FA



Одна из самых частых ошибок, которую я наблюдаю в реализациях 2FA, - это плохо продуманный пользовательский опыт. Пользователи воспринимают 2FA как раздражающее препятствие, если интерфейс неудобный. Вот несколько практик, которые я применяю:
1. Делаю четкие и понятные инструкции для настройки аутентификатора.
2. Предоставляю QR-код и текстовый ключ (на случай, если сканирование не работает).
3. Генерирую резервные коды и настаиваю, чтобы пользователь их сохранил.
4. Добавляю возможность запомнить устройство (для доверенных компьютеров).

В шаблоне для формы 2FA я обычно использую такую структуру:

HTML5
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
<div class="two-factor-form">
    <h3>Двухфакторная аутентификация</h3>
    <p>Для входа введите код из вашего приложения-аутентификатора</p>
    
    <form ng-submit="submit()">
        <div class="form-group">
            <input type="text" 
                   ng-model="code" 
                   class="form-control" 
                   placeholder="Введите 6-значный код" 
                   maxlength="6"
                   autofocus
                   pattern="[0-9]*"
                   inputmode="numeric">
        </div>
        
        <button type="submit" 
                class="btn btn-primary btn-block" 
                ng-disabled="isLoading">
            <span ng-if="isLoading"><i class="fa fa-spinner fa-spin"></i> Проверка...</span>
            <span ng-if="!isLoading">Подтвердить</span>
        </button>
    </form>
    
    <div class="recovery-option">
        <a href ng-click="showRecoveryForm = !showRecoveryForm">
            Использовать резервный код
        </a>
    </div>
    
    <div ng-if="showRecoveryForm">
        <form ng-submit="submitRecovery()">
            <!-- Форма для ввода резервного кода -->
        </form>
    </div>
</div>
Важно помнить, что двухфакторная аутентификация - это не просто галочка в списке требований безопасности. Это реальный барьер, который значительно затрудняет несанкционированный доступ к аккаунтам. Правильная реализация 2FA может стать решающим фактором в защите данных ваших пользователей, особенно в эпоху, когда утечки паролей происходят регулярно.

AngularJS + ASP.Net MVC
У кого был опыт работы? Мне сейчас кажется очень хорошей идеей отказаться полностью от Razor, Ajax...

Проект на angularjs с asp.net mvc
я новый в angularjs и пишу на ASP.NET.MVC.У меня есть слудвщие вопросы.Если я пишу на Angulatjs то...

.Net ASP MVC, REST, KnockoutJS/AngularJS, HTML5, CSS
Я программист (опыт 15 лет). На данный момент занимаюсь разработкой ERP систем. Я работаю с...

AngularJs и ASP.NET MVC5
Подскажите, как используют AngularJs вместе с ASP.NET. Для каких целей и какие плюсы и минусы....

Проект ASP.NET WebAPI + AngularJS. Подскажите, как составить логику, пожалуйста
Всем привет. Раньше кодил на чистом C#, теперь приступил к изучению ASP.NET. Дали задание: создать...

ASP.NET Core + AngularJs. Не работает метод success сервиса $http
Собственно, вот. Разбираюсь с работой Angular. Вроде все работает, но стала проблема с работой...

Как скрыть обращения от веб-сайта AngularJS к веб-сервисам ASP.NET WebAPI?
У меня есть веб-сайт, написанный на ASP.NET WebForms, который обращается к веб-службам, написанным...

Что нужно иметь виндам XP, чтобы работали ASP, не ASP.NET, а просто ASP?
Что нужно иметь виндам XP, чтобы работали ASP, не ASP.NET, а просто ASP? Или все уже есть? Я имею...

При создании проекта ASP.NET Aplicetion выскакивает сообщение Web server is not running ASP/NET version 1.1
При создании проекта ASP.NET Aplicetion выскакивает сообщение Web server is not running ASP/NET...

Перевод проекта с ASP.NET 1.0 на ASP.NET 2.0
Есть весьма большой проект, сделанный на ASP.NET 1.0 (code-behind), SQL 2000, запущен на сервере...

проблема при миграции с ASP.NET к ASP.NET 2.0
При конвертации ASP.NET сайта, по рекомендации Microsoft, установил WebApplicationProjectSetup.msi....

Не отображается страница при запуске ASP.NET приложения через ASP.NET Development Server
Добрый день. У меня возникла следующая проблема. Работаю на Visual Studio 2010. Создал новое...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Nekobox - outbounds[0].transport: unknown transport type: raw
damix 01.10.2026
Фикс ошибки Правым кликом по серверу -> отладочная информация -> edit Заменить "net": "raw", на "net": "tcp", Нажать кнопку reload.
Программный домашний кинотеатр
russiannick 27.09.2026
Сподобился на программный домашний кинотеатр. В качестве ЯВУ по традиции выбрал js. В помощники взял Яндекс-Алису. Было создано три зала на разные интересы. исторические и ретро сериал Хичкок. . .
Беседа с ИИ о программистах, недопускающих к созданию и правке кода генеративные ИИ и причины этого
zorxor 21.09.2026
Раньше я радовался или получал некоторые эмоции, пусть небольшие, но всё же, от самого процесса написания кода, рекомпиляции и запуска, видя постепенное развитие программы и прочее. А теперь лень. . .
Мобильное приложение ColorStep
pavlinmavlin 17.09.2026
Реализовал приложение Красный, Зеленый, Синий в Unity3d + c#. Название изменил на ColorStep. Приложение прошло модерацию и теперь доступно для скачивания. Делал его сам, шаг за шагом — и вот,. . .
Запрет дублирования строк в табличной части
Maks 13.09.2026
Реализация из решения ниже выполнена на нетиповом справочнике "Нормы ТО" с табличной часть "Виды ТО", разработанного в КА2, со следующими реквизитами: - ВидТО (СправочникСсылка. ВидыТО); - ВидГСМ. . .
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр.
Jin X 06.09.2026
Скрипты Tampermonkey для CyberForum, ChatGPT, Claude и пр. Работая с форумом и нейросетями в браузере часто хочется что-то подкорректировать или добавить какого-то функционала. Ниже прикреплён. . .
Программа опроса у.з. расходомера SLS-720F
Argus19 02.09.2026
Программа опроса у. з. расходомера SLS-720F Программа опрашивает один раз в минуту три ультразвуковых расходомера SLS-720F через интерфейс RS-485 по протоколу Modbus RTU. Опрашиваются регистры. . .
Hyper-V: Компьютер должен поддерживать доверенный платформенный модуль 2.0.
Maks 31.08.2026
При установке Windows 11 на виртуальную машину Hyper-V 2-го поколения вылезла такая ошибка: Решение: в параметрах виртуальной машины, в разделе "Безопасность" (Security) активировать флаг. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru