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

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

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

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

Интеграция с внешними провайдерами OAuth



Помню, как несколько лет назад мой клиент возмутился: "Зачем нам эта кнопка 'Войти через Google'? У нас серьезный бизнес-сервис, а не какая-нибудь социальная сеть!" Сегодня этот же клиент благодарит меня за настойчивость - оказалось, что больше 70% их пользователей предпочитают именно такой способ входа. И это неудивительно: люди устали создавать и запоминать десятки паролей для каждого сервиса.

OAuth - это стандарт авторизации, который позволяет пользователям предоставлять сторонним приложениям доступ к своим ресурсам без передачи учетных данных. По сути, это делегированная авторизация, когда пользователь говорит: "Я доверяю Google, и я разрешаю этому приложению использовать мои данные из Google".

Преимущества OAuth-авторизации



Интеграция с внешними провайдерами дает ряд существенных преимуществ:

1. Упрощение процесса регистрации и входа - пользователю не нужно заполнять длинные формы и придумывать очередной пароль;
2. Повышение конверсии - меньше трения при регистрации означает больше зарегистрированных пользователей;
3. Делегирование безопасности - такие гиганты как Google и Facebook вкладывают огромные ресурсы в безопасность аутентификации;
4. Доступ к дополнительной информации - с согласия пользователя можно получить его имя, фото, email и другие данные.

Настройка ASP.NET для работы с OAuth



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

Bash
1
2
3
4
Install-Package Microsoft.Owin.Security.OAuth
Install-Package Microsoft.Owin.Security.Google
Install-Package Microsoft.Owin.Security.Facebook
Install-Package Microsoft.Owin.Security.MicrosoftAccount
Затем настраиваем провайдеры в Startup.Auth.cs:

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
public void ConfigureAuth(IAppBuilder app)
{
    // Основные настройки аутентификации
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/Account/Login"),
        Provider = new CookieAuthenticationProvider
        {
            OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                validateInterval: TimeSpan.FromMinutes(30),
                regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
        }
    });
    
    // Настройка Google-аутентификации
    app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
    {
        ClientId = ConfigurationManager.AppSettings["Google:ClientId"],
        ClientSecret = ConfigurationManager.AppSettings["Google:ClientSecret"],
        CallbackPath = new PathString("/signin-google"),
        Provider = new GoogleOAuth2AuthenticationProvider
        {
            OnAuthenticated = context =>
            {
                // Сохраняем access token для использования в API-запросах
                context.Identity.AddClaim(new Claim("GoogleAccessToken", context.AccessToken));
                return Task.FromResult(0);
            }
        }
    });
    
    // Аналогично для Facebook, GitHub, Microsoft и других провайдеров
}
Ключевой момент здесь - правильная настройка ClientId и ClientSecret. Эти параметры вы получаете при регистрации вашего приложения в консоли разработчика соответствующего провайдера. Например, для Google это Google Cloud Console, для Facebook - Facebook Developers.

Когда я впервые настраивал OAuth, я совершил распространенную ошибку - хардкодил секреты прямо в коде. Это плохая практика! Храните эти чувствительные данные в конфигурации приложения или еще лучше - в секретах, доступных только на продакшн-сервере.

Контроллер для обработки OAuth-запросов



Теперь нам нужен контроллер, который будет инициировать OAuth-аутентификацию и обрабатывать ответы:

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
[AllowAnonymous]
public class AccountController : Controller
{
    private ApplicationSignInManager _signInManager;
    private ApplicationUserManager _userManager;
    
    // Конструктор и свойства опущены для краткости
    
    // Инициирует OAuth-аутентификацию
    [HttpPost]
    [AllowAnonymous]
    public ActionResult ExternalLogin(string provider, string returnUrl)
    {
        // Запрашиваем редирект на внешний провайдер
        return new ChallengeResult(provider, 
            Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
    }
    
    // Обрабатывает ответ от внешнего провайдера
    [AllowAnonymous]
    public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
    {
        var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
        if (loginInfo == null)
        {
            return RedirectToAction("Login");
        }
        
        // Пытаемся войти с внешними учетными данными
        var result = await _signInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
        switch (result)
        {
            case SignInStatus.Success:
                return RedirectToLocal(returnUrl);
            case SignInStatus.LockedOut:
                return View("Lockout");
            case SignInStatus.RequiresVerification:
                return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
            default:
                // Если пользователь не имеет аккаунта, предлагаем создать его
                ViewBag.ReturnUrl = returnUrl;
                ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
                return View("ExternalLoginConfirmation", 
                    new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
        }
    }
    
    // Другие методы опущены для краткости
}
Класс ChallengeResult - это специальный ActionResult, который инициирует процесс OAuth-аутентификации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private class ChallengeResult : HttpUnauthorizedResult
{
    public ChallengeResult(string provider, string redirectUri)
    {
        LoginProvider = provider;
        RedirectUri = redirectUri;
    }
    
    public string LoginProvider { get; set; }
    public string RedirectUri { get; set; }
    
    public override void ExecuteResult(ControllerContext context)
    {
        var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
        context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
    }
}

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



На стороне AngularJS интеграция с OAuth немного сложнее, поскольку SPA-приложения обычно не обрабатывают перенаправления так, как это делают традиционные веб-приложения. Есть два основных подхода:
1. Традиционный подход с полной перезагрузкой страницы
2. SPA-подход с использованием всплывающего окна

Я предпочитаю второй вариант, так как он дает лучший пользовательский опыт. Вот как это можно реализовать:

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
angular.module('app.auth').factory('oauthService', function($q, $window, $http) {
    return {
        login: function(provider) {
            var deferred = $q.defer();
            
            // Открываем всплывающее окно для авторизации
            var width = 600, height = 600;
            var left = (screen.width/2) - (width/2);
            var top = (screen.height/2) - (height/2);
            
            var popup = $window.open('/api/auth/external-login?provider=' + provider, 
                'oauth', 
                'width=' + width + ',height=' + height + ',top=' + top + ',left=' + left);
            
            // Функция для проверки статуса авторизации
            var checkPopup = setInterval(function() {
                try {
                    // Если окно перенаправлено на наш домен
                    if (popup.location.hostname === window.location.hostname) {
                        if (popup.location.search.indexOf('success=true') !== -1) {
                            clearInterval(checkPopup);
                            popup.close();
                            
                            // Получаем информацию о текущем пользователе
                            $http.get('/api/auth/current-user')
                                .then(function(response) {
                                    deferred.resolve(response.data);
                                })
                                .catch(function(error) {
                                    deferred.reject(error);
                                });
                        }
                        else if (popup.location.search.indexOf('error=true') !== -1) {
                            clearInterval(checkPopup);
                            popup.close();
                            deferred.reject('Ошибка авторизации');
                        }
                    }
                } catch (e) {
                    // Доступ к location запрещен из-за Same Origin Policy
                    // Это нормально, пока пользователь на стороннем сайте
                }
                
                // Если окно закрыто пользователем
                if (popup.closed) {
                    clearInterval(checkPopup);
                    deferred.reject('Авторизация отменена');
                }
            }, 500);
            
            return deferred.promise;
        }
    };
});
А вот контроллер для кнопок входа через социальные сети:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
angular.module('app.auth').controller('SocialLoginController', function($scope, oauthService, notifyService) {
    $scope.isLoading = false;
    
    $scope.loginWith = function(provider) {
        $scope.isLoading = true;
        
        oauthService.login(provider)
            .then(function(user) {
                notifyService.success('Успешный вход');
                // Обновляем состояние авторизации в приложении
                // ...
            })
            .catch(function(error) {
                notifyService.error('Не удалось войти: ' + error);
            })
            .finally(function() {
                $scope.isLoading = false;
            });
    };
});
И шаблон с кнопками:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
<div class="social-login" ng-controller="SocialLoginController">
    <button class="btn btn-google" ng-click="loginWith('Google')" ng-disabled="isLoading">
        <i class="fa fa-google"></i> Войти через Google
    </button>
    
    <button class="btn btn-facebook" ng-click="loginWith('Facebook')" ng-disabled="isLoading">
        <i class="fa fa-facebook"></i> Войти через Facebook
    </button>
    
    <button class="btn btn-github" ng-click="loginWith('GitHub')" ng-disabled="isLoading">
        <i class="fa fa-github"></i> Войти через GitHub
    </button>
</div>

Связывание аккаунтов



Один из самых сложных аспектов OAuth-интеграции - это правильное связывание внешних аккаунтов с существующими учетными записями в вашей системе. Я обычно использую следующий подход:
1. Если email из внешнего провайдера совпадает с существующим в системе - предлагаю пользователю связать аккаунты
2. Если email не найден - создаю новую учетную запись
Это требует дополнительного 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
[HttpPost("link-account")]
public async Task<IActionResult> LinkExternalAccount([FromBody] LinkAccountModel model)
{
    var user = await _userManager.FindByNameAsync(model.Email);
    if (user == null)
    {
        return BadRequest(new { Message = "Пользователь не найден" });
    }
    
    // Проверяем пароль
    var isPasswordValid = await _userManager.CheckPasswordAsync(user, model.Password);
    if (!isPasswordValid)
    {
        return BadRequest(new { Message = "Неверный пароль" });
    }
    
    // Получаем информацию о внешнем логине из сессии
    var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
    if (loginInfo == null)
    {
        return BadRequest(new { Message = "Информация о внешнем логине не найдена" });
    }
    
    // Связываем аккаунты
    var result = await _userManager.AddLoginAsync(user.Id, loginInfo.Login);
    if (!result.Succeeded)
    {
        return BadRequest(new { Message = "Не удалось связать аккаунты", Errors = result.Errors });
    }
    
    // Выполняем вход
    await _signInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
    
    return Ok(new { Success = true });
}
Интеграция с внешними провайдерами OAuth может значительно упростить процесс регистрации и входа для ваших пользователей. Однако она также добавляет сложности в архитектуру приложения и требует тщательного тестирования. Я рекомендую начинать с одного провайдера (обычно Google, как самого популярного) и добавлять другие постепенно, по мере необходимости.

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 большая ли разница между ними?
Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...


Мониторинг и логирование попыток входа



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

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

Что нужно логировать при попытках входа



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

1. Дата и время попытки,
2. IP-адрес пользователя,
3. User-Agent браузера,
4. Введенный логин/email (но никогда не пароль!),
5. Результат попытки (успех/неудача),
6. Причина неудачи (неверный пароль, аккаунт заблокирован и т.д.),
7. Был ли использован второй фактор аутентификации.

Для более детального анализа можно добавить:

8. Идентификатор сессии,
9. Геолокацию IP-адреса,
10. Отпечаток устройства (device fingerprint),
11. Информацию о предыдущей успешной авторизации.

Реализация логирования в 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
public class LoginAttempt
{
  public int Id { get; set; }
  
  public int? UserId { get; set; }  // null если пользователь не найден
  
  [Required]
  [MaxLength(256)]
  public string Email { get; set; }  // Введенный email
  
  public DateTime AttemptDate { get; set; }
  
  [Required]
  [MaxLength(50)]
  public string IpAddress { get; set; }
  
  [MaxLength(500)]
  public string UserAgent { get; set; }
  
  public bool Succeeded { get; set; }
  
  [MaxLength(50)]
  public string FailureReason { get; set; }  // Причина неудачи
  
  [MaxLength(100)]
  public string Country { get; set; }  // Страна по IP
  
  [MaxLength(100)]
  public string City { get; set; }  // Город по IP
  
  public User User { get; set; }  // Навигационное свойство
}
Теперь создадим сервис для работы с логами:

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
public interface ILoginAttemptService
{
  Task LogAttemptAsync(string email, string ipAddress, string userAgent, bool succeeded, string failureReason = null);
  Task<List<LoginAttempt>> GetRecentAttemptsForUserAsync(int userId, int count = 10);
  Task<List<LoginAttempt>> GetFailedAttemptsAsync(string ipAddress, TimeSpan period);
  Task<bool> ExceedsThresholdAsync(string ipAddress, int maxAttempts, TimeSpan period);
}
 
public class LoginAttemptService : ILoginAttemptService
{
  private readonly ApplicationDbContext _context;
  private readonly IUserService _userService;
  private readonly IGeoLocationService _geoLocationService;
  
  public LoginAttemptService(
      ApplicationDbContext context,
      IUserService userService,
      IGeoLocationService geoLocationService)
  {
      _context = context;
      _userService = userService;
      _geoLocationService = geoLocationService;
  }
  
  public async Task LogAttemptAsync(string email, string ipAddress, string userAgent, bool succeeded, string failureReason = null)
  {
      // Находим пользователя, если есть
      var user = await _userService.FindByEmailAsync(email);
      
      // Получаем геолокацию
      var geoInfo = await _geoLocationService.GetLocationAsync(ipAddress);
      
      var attempt = new LoginAttempt
      {
          UserId = user?.Id,
          Email = email,
          AttemptDate = DateTime.UtcNow,
          IpAddress = ipAddress,
          UserAgent = userAgent,
          Succeeded = succeeded,
          FailureReason = failureReason,
          Country = geoInfo?.Country,
          City = geoInfo?.City
      };
      
      _context.LoginAttempts.Add(attempt);
      await _context.SaveChangesAsync();
  }
  
  public async Task<List<LoginAttempt>> GetRecentAttemptsForUserAsync(int userId, int count = 10)
  {
      return await _context.LoginAttempts
          .Where(la => la.UserId == userId)
          .OrderByDescending(la => la.AttemptDate)
          .Take(count)
          .ToListAsync();
  }
  
  public async Task<List<LoginAttempt>> GetFailedAttemptsAsync(string ipAddress, TimeSpan period)
  {
      var cutoffTime = DateTime.UtcNow.Subtract(period);
      
      return await _context.LoginAttempts
          .Where(la => la.IpAddress == ipAddress && 
                      !la.Succeeded && 
                      la.AttemptDate >= cutoffTime)
          .OrderByDescending(la => la.AttemptDate)
          .ToListAsync();
  }
  
  public async Task<bool> ExceedsThresholdAsync(string ipAddress, int maxAttempts, TimeSpan period)
  {
      var attempts = await GetFailedAttemptsAsync(ipAddress, period);
      return attempts.Count >= maxAttempts;
  }
}
Интегрируем логирование в контроллер авторизации:

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
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
  try
  {
      // Получаем IP-адрес и User-Agent
      var ipAddress = HttpContext.Connection.RemoteIpAddress.ToString();
      var userAgent = HttpContext.Request.Headers["User-Agent"].ToString();
      
      // Проверяем, не превышен ли лимит попыток
      if (await _loginAttemptService.ExceedsThresholdAsync(ipAddress, 5, TimeSpan.FromMinutes(15)))
      {
          await _loginAttemptService.LogAttemptAsync(
              model.Email, ipAddress, userAgent, false, "Rate limit exceeded");
              
          return StatusCode(429, new { Message = "Слишком много попыток входа. Попробуйте позже." });
      }
      
      // Пытаемся аутентифицировать пользователя
      var result = await _userService.AuthenticateAsync(model.Email, model.Password);
      
      if (result.Succeeded)
      {
          // Логируем успешную попытку
          await _loginAttemptService.LogAttemptAsync(
              model.Email, ipAddress, userAgent, true);
              
          // Генерируем токены и возвращаем результат
          // ...
      }
      else
      {
          // Логируем неудачную попытку
          await _loginAttemptService.LogAttemptAsync(
              model.Email, ipAddress, userAgent, false, result.FailureReason);
              
          return Unauthorized(new { Message = "Неверные учетные данные" });
      }
  }
  catch (Exception ex)
  {
      _logger.LogError(ex, "Ошибка при обработке запроса на вход");
      return StatusCode(500, new { Message = "Внутренняя ошибка сервера" });
  }
}

Мониторинг подозрительной активности



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

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
public class LoginSecurityMonitor : BackgroundService
{
  private readonly IServiceProvider _serviceProvider;
  private readonly ILogger<LoginSecurityMonitor> _logger;
  
  public LoginSecurityMonitor(
      IServiceProvider serviceProvider,
      ILogger<LoginSecurityMonitor> logger)
  {
      _serviceProvider = serviceProvider;
      _logger = logger;
  }
  
  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
      while (!stoppingToken.IsCancellationRequested)
      {
          try
          {
              using (var scope = _serviceProvider.CreateScope())
              {
                  var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                  var notificationService = scope.ServiceProvider.GetRequiredService<INotificationService>();
                  
                  // Проверяем подозрительные IP-адреса с множественными неудачными попытками
                  await CheckBruteForceAttacksAsync(dbContext, notificationService);
                  
                  // Проверяем необычную активность для пользователей
                  await CheckUnusualActivityAsync(dbContext, notificationService);
              }
          }
          catch (Exception ex)
          {
              _logger.LogError(ex, "Ошибка при мониторинге безопасности входа");
          }
          
          // Запускаем проверку каждые 5 минут
          await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
      }
  }
  
  private async Task CheckBruteForceAttacksAsync(
      ApplicationDbContext dbContext, 
      INotificationService notificationService)
  {
      // Группируем неудачные попытки по IP и считаем их
      var suspiciousIps = await dbContext.LoginAttempts
          .Where(la => !la.Succeeded && la.AttemptDate >= DateTime.UtcNow.AddHours(-1))
          .GroupBy(la => la.IpAddress)
          .Select(g => new { IpAddress = g.Key, Count = g.Count() })
          .Where(x => x.Count >= 10) // Порог: 10 попыток в час
          .ToListAsync();
          
      foreach (var ip in suspiciousIps)
      {
          _logger.LogWarning($"Обнаружена возможная брутфорс-атака с IP {ip.IpAddress}, {ip.Count} попыток за последний час");
          
          // Отправляем уведомление администратору
          await notificationService.NotifyAdminsAsync(
              "Обнаружена подозрительная активность", 
              $"Возможная брутфорс-атака с IP {ip.IpAddress}, {ip.Count} неудачных попыток за последний час.");
              
          // Здесь можно добавить автоматическую блокировку IP
      }
  }
  
  private async Task CheckUnusualActivityAsync(
      ApplicationDbContext dbContext, 
      INotificationService notificationService)
  {
      // Ищем успешные входы из необычных мест
      var suspiciousLogins = await dbContext.LoginAttempts
          .Where(la => la.Succeeded && la.AttemptDate >= DateTime.UtcNow.AddDays(-1))
          .Join(
              dbContext.Users,
              la => la.UserId,
              u => u.Id,
              (la, u) => new { LoginAttempt = la, User = u }
          )
          .Where(x => x.User.LastLoginCountry != null && 
                      x.User.LastLoginCountry != x.LoginAttempt.Country)
          .ToListAsync();
          
      foreach (var login in suspiciousLogins)
      {
          _logger.LogWarning(
              $"Необычный вход для пользователя {login.User.Email}: " +
              $"обычная страна {login.User.LastLoginCountry}, " +
              $"текущий вход из {login.LoginAttempt.Country}"
          );
          
          // Уведомляем пользователя
          await notificationService.NotifyUserAsync(
              login.User.Id,
              "Обнаружен вход из необычного места",
              $"Мы заметили вход в ваш аккаунт из {login.LoginAttempt.Country}. Если это были не вы, немедленно смените пароль."
          );
      }
  }
}
В продакшене я часто дополняю эту систему интеграцией с SIEM-решениями или отправкой уведомлений в Slack или другие системы мониторинга, которые использует команда безопасности.

Система уведомлений об аномальной активности



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

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

Типы аномалий, требующие уведомлений



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

1. Вход из нового места — пользователь обычно входит из Москвы, но внезапно появляется логин из Бразилии.
2. Вход в необычное время — если сотрудник всегда работает с 9 до 18, а тут вход в 3 часа ночи.
3. Множественные неудачные попытки — более 5-10 неудачных попыток входа за короткий промежуток времени.
4. Необычные паттерны использования — например, вход сразу из двух разных стран за короткий период.
5. Попытки доступа к чувствительной информации — после входа в систему пользователь пытается получить доступ к ресурсам, которые он обычно не использует.

Реализация сервиса уведомлений



Начнем с создания интерфейса для сервиса уведомлений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface INotificationService
{
    Task NotifyUserAsync(int userId, string subject, string message);
    Task NotifyAdminsAsync(string subject, string message);
    Task NotifySecurityTeamAsync(string subject, string message, AlertLevel level);
}
 
public enum AlertLevel
{
    Low,
    Medium,
    High,
    Critical
}
Теперь реализуем этот интерфейс:

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
public class NotificationService : INotificationService
{
    private readonly ApplicationDbContext _context;
    private readonly IEmailService _emailService;
    private readonly ISmsService _smsService;
    private readonly IWebPushService _pushService;
    private readonly ILogger<NotificationService> _logger;
    
    public NotificationService(
        ApplicationDbContext context,
        IEmailService emailService,
        ISmsService smsService,
        IWebPushService pushService,
        ILogger<NotificationService> logger)
    {
        _context = context;
        _emailService = emailService;
        _smsService = smsService;
        _pushService = pushService;
        _logger = logger;
    }
    
    public async Task NotifyUserAsync(int userId, string subject, string message)
    {
        var user = await _context.Users.FindAsync(userId);
        if (user == null)
        {
            _logger.LogWarning($"Попытка уведомить несуществующего пользователя: {userId}");
            return;
        }
        
        // Сохраняем уведомление в базе
        var notification = new UserNotification
        {
            UserId = userId,
            Subject = subject,
            Message = message,
            CreatedAt = DateTime.UtcNow,
            IsRead = false
        };
        
        _context.UserNotifications.Add(notification);
        await _context.SaveChangesAsync();
        
        // Отправляем email
        if (!string.IsNullOrEmpty(user.Email))
        {
            await _emailService.SendAsync(user.Email, subject, message);
        }
        
        // Отправляем SMS для критичных уведомлений
        if (!string.IsNullOrEmpty(user.PhoneNumber) && subject.Contains("Подозрительный вход"))
        {
            await _smsService.SendAsync(user.PhoneNumber, $"Обнаружена подозрительная активность в вашем аккаунте. Проверьте почту для деталей.");
        }
        
        // Отправляем push-уведомление, если пользователь подписан
        if (user.PushSubscription != null)
        {
            await _pushService.SendAsync(user.PushSubscription, subject, message);
        }
    }
    
    // Реализация других методов опущена для краткости
}

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



На стороне клиента нам нужен способ отображения уведомлений. Для этого создадим сервис и директиву в 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
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
angular.module('app.notifications', [])
.factory('notificationService', function($http, $interval) {
    var service = {};
    var notifications = [];
    
    // Получение уведомлений с сервера
    service.fetchNotifications = function() {
        return $http.get('/api/notifications')
            .then(function(response) {
                notifications = response.data;
                return notifications;
            });
    };
    
    // Отметить уведомление как прочитанное
    service.markAsRead = function(notificationId) {
        return $http.post('/api/notifications/' + notificationId + '/read')
            .then(function() {
                var index = notifications.findIndex(n => n.id === notificationId);
                if (index !== -1) {
                    notifications[index].isRead = true;
                }
            });
    };
    
    // Запускаем периодическую проверку новых уведомлений
    $interval(service.fetchNotifications, 60000); // Каждую минуту
    
    // Инициализация
    service.fetchNotifications();
    
    return service;
})
.directive('notificationBell', function(notificationService) {
    return {
        restrict: 'E',
        template: 
            '<div class="notification-bell">' +
                '<i class="fa fa-bell"></i>' +
                '<span class="badge" ng-if="unreadCount > 0">{{unreadCount}}</span>' +
                '<div class="notification-dropdown" ng-if="showDropdown">' +
                    '<div class="notification-item" ng-repeat="notification in notifications" ng-class="{\'read\': notification.isRead}">' +
                        '<div class="notification-title">{{notification.subject}}</div>' +
                        '<div class="notification-message">{{notification.message}}</div>' +
                        '<div class="notification-time">{{notification.createdAt | date:\'short\'}}</div>' +
                        '<button ng-if="!notification.isRead" ng-click="markAsRead(notification.id)">Отметить как прочитанное</button>' +
                    '</div>' +
                '</div>' +
            '</div>',
        link: function(scope, element) {
            scope.notifications = [];
            scope.unreadCount = 0;
            scope.showDropdown = false;
            
            // Обновляем список уведомлений
            function updateNotifications() {
                notificationService.fetchNotifications()
                    .then(function(data) {
                        scope.notifications = data;
                        scope.unreadCount = data.filter(n => !n.isRead).length;
                    });
            }
            
            // Отмечаем уведомление как прочитанное
            scope.markAsRead = function(id) {
                notificationService.markAsRead(id);
            };
            
            // Открываем/закрываем выпадающий список
            element.find('i').on('click', function() {
                scope.$apply(function() {
                    scope.showDropdown = !scope.showDropdown;
                });
            });
            
            // Инициализация
            updateNotifications();
        }
    };
});

Триггеры для отправки уведомлений



Теперь нам нужно интегрировать эту систему уведомлений с нашим мониторингом безопасности. Для этого расширим класс LoginSecurityMonitor:

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
private async Task CheckUnusualLocationAsync(
    ApplicationDbContext dbContext, 
    INotificationService notificationService)
{
    var lastDay = DateTime.UtcNow.AddDays(-1);
    
    // Находим входы из необычных локаций
    var unusualLogins = await dbContext.LoginAttempts
        .Where(la => la.Succeeded && la.AttemptDate >= lastDay)
        .Join(dbContext.Users, la => la.UserId, u => u.Id, (la, u) => new { LoginAttempt = la, User = u })
        .Where(x => x.User.LastLoginCountry != null && 
                    x.User.LastLoginCountry != x.LoginAttempt.Country)
        .ToListAsync();
    
    foreach (var item in unusualLogins)
    {
        // Уведомляем пользователя
        await notificationService.NotifyUserAsync(
            item.User.Id,
            "Подозрительный вход в ваш аккаунт",
            $"Мы обнаружили вход в ваш аккаунт из необычного места: {item.LoginAttempt.Country}, {item.LoginAttempt.City}. " +
            $"Если это были не вы, немедленно смените пароль и обратитесь в службу поддержки."
        );
        
        // Уведомляем службу безопасности
        await notificationService.NotifySecurityTeamAsync(
            "Вход из необычной локации",
            $"Пользователь {item.User.Email} вошел из необычного места: {item.LoginAttempt.Country}, {item.LoginAttempt.City}. " +
            $"Обычная локация: {item.User.LastLoginCountry}.",
            AlertLevel.Medium
        );
    }
}
Система уведомлений — это не просто удобство, а критически важный компонент безопасности. Она позволяет оперативно реагировать на угрозы и дает пользователям чувство контроля над своими аккаунтами. В современных веб-приложениях ее реализация должна быть приоритетом, особенно если вы имеете дело с чувствительными данными или финансовыми операциями.

Практическое внедрение и обработка ошибок авторизации



Даже самая тщательно спроектированная система авторизации может превратиться в кошмар для пользователей, если ошибки в ней обрабатываются неправильно. Я не раз сталкивался с ситуациями, когда качественно написанный код с надежной архитектурой имел ужасный UX из-за непонятных сообщений об ошибках или отсутствия информативной обратной связи.

Работая с одним крупным e-commerce проектом, я наблюдал, как пользователи буквально уходили с сайта из-за невнятных сообщений типа "Error code: AUTH_ERR_0023" при попытке авторизации. Анализ данных показал, что мы теряли почти 30% конверсии на форме входа! Правильная обработка ошибок — это не просто технический вопрос, а прямой путь к кошельку клиента.

Типы ошибок авторизации и их правильная обработка



Давайте рассмотрим основные типы ошибок, с которыми мы сталкиваемся при авторизации:
1. Ошибки валидации формы — неверный формат email, слишком короткий пароль и т.д.
2. Ошибки аутентификации — неверные учетные данные.
3. Ошибки состояния аккаунта — заблокированный аккаунт, требуется подтверждение email.
4. Технические ошибки — сбои в соединении, ошибки сервера.
5. Ошибки безопасности — подозрительная активность, превышен лимит попыток входа.
Для каждого типа ошибок нужен свой подход к обработке. Вот как я реализую это в 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
78
79
80
81
82
83
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
    try
    {
        // Валидация модели
        if (!ModelState.IsValid)
        {
            return BadRequest(new ApiErrorResponse
            {
                ErrorType = "ValidationError",
                Message = "Данные формы содержат ошибки",
                Details = ModelState.GetErrorMessages() // Вспомогательный метод для сбора всех ошибок
            });
        }
        
        // Проверка на блокировку по IP
        if (await _securityService.IsIpBlockedAsync(HttpContext.Connection.RemoteIpAddress.ToString()))
        {
            return StatusCode(429, new ApiErrorResponse
            {
                ErrorType = "RateLimitExceeded",
                Message = "Слишком много попыток входа. Попробуйте позже.",
                Details = new { RetryAfter = 15 } // Минут до снятия блокировки
            });
        }
        
        // Попытка аутентификации
        var result = await _authService.AuthenticateAsync(model.Email, model.Password);
        
        if (!result.Succeeded)
        {
            // Разные сообщения в зависимости от причины ошибки
            switch (result.FailureReason)
            {
                case AuthFailureReason.InvalidCredentials:
                    return Unauthorized(new ApiErrorResponse
                    {
                        ErrorType = "InvalidCredentials",
                        Message = "Неверный email или пароль"
                    });
                    
                case AuthFailureReason.AccountLocked:
                    return Forbidden(new ApiErrorResponse
                    {
                        ErrorType = "AccountLocked",
                        Message = "Ваш аккаунт временно заблокирован",
                        Details = new { UnlockTime = result.AccountUnlockTime }
                    });
                    
                case AuthFailureReason.EmailNotConfirmed:
                    // Переотправляем ссылку подтверждения
                    await _emailService.SendConfirmationAsync(model.Email);
                    
                    return Unauthorized(new ApiErrorResponse
                    {
                        ErrorType = "EmailNotConfirmed",
                        Message = "Необходимо подтвердить email. Мы отправили новую ссылку для подтверждения."
                    });
                    
                default:
                    return Unauthorized(new ApiErrorResponse
                    {
                        ErrorType = "AuthenticationFailed",
                        Message = "Не удалось выполнить вход"
                    });
            }
        }
        
        // Успешная аутентификация - генерируем токены и т.д.
        // ...
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Ошибка при обработке запроса на вход");
        
        return StatusCode(500, new ApiErrorResponse
        {
            ErrorType = "ServerError",
            Message = "Произошла внутренняя ошибка сервера"
        });
    }
}
Обратите внимание, как я использую структурированный ответ ApiErrorResponse вместо просто строки с сообщением. Это делает обработку на клиенте более предсказуемой и гибкой.

Клиентская обработка ошибок в AngularJS



На стороне 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
angular.module('app.auth').factory('authService', function($http, $q) {
  return {
    login: function(credentials) {
      return $http.post('/api/auth/login', credentials)
        .then(function(response) {
          return response.data;
        })
        .catch(function(response) {
          // Преобразуем ошибку в более удобный формат
          var error = {
            type: response.data?.errorType || 'UnknownError',
            message: response.data?.message || 'Произошла неизвестная ошибка',
            details: response.data?.details || {},
            status: response.status
          };
          
          // Специальная обработка в зависимости от типа ошибки
          switch (error.type) {
            case 'RateLimitExceeded':
              error.retryAfter = error.details.retryAfter;
              break;
              
            case 'ValidationError':
              // Преобразуем ошибки валидации в формат, удобный для привязки к форме
              error.validationErrors = {};
              angular.forEach(error.details, function(errors, field) {
                error.validationErrors[field] = errors.join(', ');
              });
              break;
          }
          
          return $q.reject(error);
        });
    }
  };
});
А в контроллере формы:

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
$scope.login = function() {
  $scope.isLoading = true;
  $scope.errorMessage = null;
  $scope.fieldErrors = {};
  
  authService.login($scope.credentials)
    .then(function(result) {
      // Обработка успешного входа
    })
    .catch(function(error) {
      // Отображаем ошибку в зависимости от ее типа
      switch (error.type) {
        case 'ValidationError':
          $scope.fieldErrors = error.validationErrors;
          break;
          
        case 'RateLimitExceeded':
          $scope.errorMessage = error.message;
          $scope.lockoutTime = error.retryAfter;
          // Запускаем таймер обратного отсчета
          startLockoutTimer(error.retryAfter);
          break;
          
        case 'EmailNotConfirmed':
          $scope.errorMessage = error.message;
          $scope.showResendButton = true;
          break;
          
        default:
          $scope.errorMessage = error.message;
      }
    })
    .finally(function() {
      $scope.isLoading = false;
    });
};
 
function startLockoutTimer(minutes) {
  // Реализация таймера обратного отсчета
}

Юзабилити: как представить ошибки пользователю



Правильное отображение ошибок — это искусство. Вот мои принципы:

1. Используйте понятный язык — никаких технических кодов или жаргона;
2. Подсказывайте решение — не просто "ошибка", а "что делать дальше";
3. Визуально выделяйте ошибки — цветом, иконками, но без перебора;
4. Располагайте сообщения рядом с проблемой — ошибки валидации поля должны быть рядом с этим полем;
5. Группируйте ошибки — если их много, не разбрасывайте по всей форме;

Вот пример шаблона для отображения ошибок:

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
37
38
39
40
41
42
43
44
45
46
47
48
<form name="loginForm" ng-submit="login()">
  <!-- Общая ошибка -->
  <div class="alert alert-danger" ng-if="errorMessage">
    <i class="fa fa-exclamation-circle"></i> {{errorMessage}}
    
    <div ng-if="lockoutTime">
      Повторите попытку через <span class="countdown">{{remainingTime}}</span>
    </div>
    
    <button class="btn btn-link" 
            ng-if="showResendButton" 
            ng-click="resendConfirmation()"
            ng-disabled="isResending">
      <span ng-if="!isResending">Отправить письмо повторно</span>
      <span ng-if="isResending">Отправка...</span>
    </button>
  </div>
  
  <!-- Поле Email -->
  <div class="form-group" ng-class="{'has-error': loginForm.email.$invalid && (loginForm.email.$dirty || loginForm.$submitted) || fieldErrors.email}">
    <label for="email">Email</label>
    <input type="email" 
           id="email" 
           name="email" 
           class="form-control" 
           ng-model="credentials.email" 
           required>
           
    <div class="help-block" ng-if="loginForm.email.$error.required && (loginForm.email.$dirty || loginForm.$submitted)">
      Введите email
    </div>
    
    <div class="help-block" ng-if="loginForm.email.$error.email && loginForm.email.$dirty">
      Введите корректный email
    </div>
    
    <div class="help-block" ng-if="fieldErrors.email">
      {{fieldErrors.email}}
    </div>
  </div>
  
  <!-- Аналогично для поля пароля -->
  
  <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>
</form>

Отладка проблем с авторизацией



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

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
// В AuthService
public async Task<AuthResult> AuthenticateAsync(string email, string password)
{
    try
    {
        _logger.LogInformation("Начало аутентификации для {Email}", email);
        
        var user = await _userManager.FindByEmailAsync(email);
        if (user == null)
        {
            _logger.LogWarning("Пользователь не найден: {Email}", email);
            return AuthResult.Failed(AuthFailureReason.InvalidCredentials);
        }
        
        // Другие проверки с логированием
        // ...
        
        _logger.LogInformation("Успешная аутентификация: {Email} (UserId: {UserId})", email, user.Id);
        return AuthResult.Success(user);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Ошибка при аутентификации {Email}", email);
        throw;
    }
}
Для диагностики проблем с клиентской стороной добавляю детальное логирование 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, $log) {
    return {
      request: function(config) {
        $log.debug('HTTP Request', config.method, config.url, config);
        return config;
      },
      
      requestError: function(rejection) {
        $log.error('HTTP Request Error', rejection);
        return $q.reject(rejection);
      },
      
      response: function(response) {
        $log.debug('HTTP Response', response.status, response.config.url, response);
        return response;
      },
      
      responseError: function(rejection) {
        $log.error('HTTP Response Error', rejection.status, rejection.config.url, rejection);
        return $q.reject(rejection);
      }
    };
  });
});
Правильно реализованная обработка ошибок авторизации не только улучшает пользовательский опыт, но и значительно упрощает диагностику и решение проблем. Помните: хорошая система — не та, в которой не бывает ошибок, а та, которая обрабатывает их максимально прозрачно и удобно для всех участников процесса.

Тестирование и отладка интеграции AngularJS с ASP.NET



Как-то мне пришлось разбираться с проблемой, когда пользователи жаловались на "странные сбои" при входе в систему. Оказалось, что разработчики совершенно не тестировали сценарий, при котором пользователь заходит с двух устройств одновременно. В итоге токены обновлялись некорректно, вызывая каскад неожиданных ошибок. Этот кейс научил меня тому, что тестирование авторизации должно быть таким же скрупулезным, как тестирование бизнес-логики приложения.

Юнит-тестирование компонентов авторизации



Начнем с самого базового — юнит-тестов для отдельных компонентов. Для AngularJS я использую связку Karma + Jasmine:

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
describe('LoginController', function() {
  var $controller, $rootScope, $httpBackend, authService, $location;
  
  beforeEach(module('app.auth'));
  
  beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_, _authService_, _$location_) {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
    $httpBackend = _$httpBackend_;
    authService = _authService_;
    $location = _$location_;
  }));
  
  it('должен отправлять учетные данные на сервер при успешной валидации', function() {
    var $scope = $rootScope.$new();
    var controller = $controller('LoginController', { $scope: $scope });
    
    $scope.credentials = {
      email: 'test@example.com',
      password: 'Password123!'
    };
    
    $scope.loginForm = { $valid: true };
    
    $httpBackend.expectPOST('/api/auth/login', {
      email: 'test@example.com',
      password: 'Password123!'
    }).respond(200, { token: 'fake-token', userName: 'Test User' });
    
    $scope.login();
    $httpBackend.flush();
    
    expect($location.path()).toBe('/dashboard');
  });
  
  it('должен показывать ошибку при неверных учетных данных', function() {
    var $scope = $rootScope.$new();
    var controller = $controller('LoginController', { $scope: $scope });
    
    $scope.credentials = {
      email: 'test@example.com',
      password: 'WrongPassword'
    };
    
    $scope.loginForm = { $valid: true };
    
    $httpBackend.expectPOST('/api/auth/login').respond(401, { 
      errorType: 'InvalidCredentials',
      message: 'Неверный email или пароль' 
    });
    
    $scope.login();
    $httpBackend.flush();
    
    expect($scope.errorMessage).toBe('Неверный email или пароль');
    expect($location.path()).not.toBe('/dashboard');
  });
});
На стороне ASP.NET я тестирую контроллеры и сервисы с помощью xUnit:

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
public class AuthControllerTests
{
  private readonly Mock<IUserService> _userServiceMock;
  private readonly Mock<IJwtService> _jwtServiceMock;
  private readonly Mock<ILogger<AuthController>> _loggerMock;
  private readonly AuthController _controller;
  
  public AuthControllerTests()
  {
    _userServiceMock = new Mock<IUserService>();
    _jwtServiceMock = new Mock<IJwtService>();
    _loggerMock = new Mock<ILogger<AuthController>>();
    
    _controller = new AuthController(
      _userServiceMock.Object,
      _jwtServiceMock.Object,
      _loggerMock.Object
    );
    
    // Настраиваем HttpContext для контроллера
    var httpContext = new DefaultHttpContext();
    httpContext.Connection.RemoteIpAddress = IPAddress.Parse("127.0.0.1");
    _controller.ControllerContext = new ControllerContext
    {
      HttpContext = httpContext
    };
  }
  
  [Fact]
  public async Task Login_WithValidCredentials_ReturnsToken()
  {
    // Arrange
    var model = new LoginModel
    {
      Email = "test@example.com",
      Password = "Password123!"
    };
    
    var user = new User { Id = 1, Email = model.Email, FullName = "Test User" };
    
    _userServiceMock
      .Setup(x => x.AuthenticateAsync(model.Email, model.Password))
      .ReturnsAsync(AuthResult.Success(user));
      
    _jwtServiceMock
      .Setup(x => x.GenerateToken(user))
      .Returns("fake-token");
    
    // Act
    var result = await _controller.Login(model) as OkObjectResult;
    
    // Assert
    Assert.NotNull(result);
    Assert.Equal(200, result.StatusCode);
    
    var value = result.Value as dynamic;
    Assert.Equal("fake-token", value.token);
    Assert.Equal("Test User", value.userName);
    
    // Проверяем, что логирование было вызвано
    _loggerMock.Verify(
      x => x.Log(
        LogLevel.Information,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Успешная аутентификация")),
        null,
        It.IsAny<Func<It.IsAnyType, Exception, string>>()),
      Times.Once);
  }
  
  [Fact]
  public async Task Login_WithInvalidCredentials_ReturnsUnauthorized()
  {
    // Arrange
    var model = new LoginModel
    {
      Email = "test@example.com",
      Password = "WrongPassword"
    };
    
    _userServiceMock
      .Setup(x => x.AuthenticateAsync(model.Email, model.Password))
      .ReturnsAsync(AuthResult.Failed(AuthFailureReason.InvalidCredentials));
    
    // Act
    var result = await _controller.Login(model) as UnauthorizedObjectResult;
    
    // Assert
    Assert.NotNull(result);
    Assert.Equal(401, result.StatusCode);
    
    var value = result.Value as dynamic;
    Assert.Equal("InvalidCredentials", value.errorType);
    
    // Проверяем логирование
    _loggerMock.Verify(
      x => x.Log(
        LogLevel.Warning,
        It.IsAny<EventId>(),
        It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Неудачная попытка входа")),
        null,
        It.IsAny<Func<It.IsAnyType, Exception, string>>()),
      Times.Once);
  }
}

Интеграционное тестирование



Юнит-тесты хороши, но они не проверяют взаимодействие компонентов. Для этого нужны интеграционные тесты, которые эмулируют реальные сценарии использования:

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 AuthIntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
  private readonly WebApplicationFactory<Startup> _factory;
  private readonly HttpClient _client;
  
  public AuthIntegrationTests(WebApplicationFactory<Startup> factory)
  {
    _factory = factory.WithWebHostBuilder(builder =>
    {
      builder.ConfigureServices(services =>
      {
        // Заменяем реальные сервисы на тестовые реализации
        services.AddSingleton<IUserService, TestUserService>();
        services.AddSingleton<IJwtService, TestJwtService>();
      });
    });
    
    _client = _factory.CreateClient();
  }
  
  [Fact]
  public async Task FullLoginFlow_WithValidCredentials_SucceedsAndAccessesProtectedResource()
  {
    // 1. Попытка доступа к защищенному ресурсу без аутентификации
    var unauthResponse = await _client.GetAsync("/api/protected-resource");
    Assert.Equal(HttpStatusCode.Unauthorized, unauthResponse.StatusCode);
    
    // 2. Успешный логин
    var loginResponse = await _client.PostAsJsonAsync("/api/auth/login", new
    {
      Email = "test@example.com",
      Password = "Password123!"
    });
    
    Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
    
    var loginResult = await loginResponse.Content.ReadFromJsonAsync<LoginResult>();
    Assert.NotNull(loginResult.Token);
    
    // 3. Доступ к защищенному ресурсу с токеном
    _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResult.Token);
    
    var authResponse = await _client.GetAsync("/api/protected-resource");
    Assert.Equal(HttpStatusCode.OK, authResponse.StatusCode);
    
    // 4. Проверяем, что ответ содержит ожидаемые данные
    var content = await authResponse.Content.ReadAsStringAsync();
    Assert.Contains("protected data", content.ToLower());
  }
}
На фронтенде для интеграционного тестирования я использую Protractor:

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
describe('Форма авторизации', function() {
  beforeEach(function() {
    browser.get('http://localhost:4200/login');
  });
  
  it('должна успешно авторизовать пользователя с правильными данными', function() {
    element(by.model('credentials.email')).sendKeys('test@example.com');
    element(by.model('credentials.password')).sendKeys('Password123!');
    element(by.buttonText('Войти')).click();
    
    // Проверяем, что произошло перенаправление на дашборд
    expect(browser.getCurrentUrl()).toContain('/dashboard');
    
    // Проверяем, что отображается имя пользователя
    var userNameElement = element(by.css('.user-info .name'));
    expect(userNameElement.getText()).toBe('Test User');
  });
  
  it('должна показывать ошибку при неверном пароле', function() {
    element(by.model('credentials.email')).sendKeys('test@example.com');
    element(by.model('credentials.password')).sendKeys('WrongPassword');
    element(by.buttonText('Войти')).click();
    
    // Проверяем, что отображается сообщение об ошибке
    var errorElement = element(by.css('.alert-danger'));
    expect(errorElement.isDisplayed()).toBe(true);
    expect(errorElement.getText()).toContain('Неверный email или пароль');
    
    // Проверяем, что мы остались на странице логина
    expect(browser.getCurrentUrl()).toContain('/login');
  });
});

Отладка распространенных проблем



Несмотря на тщательное тестирование, некоторые проблемы всё равно проявляются только в боевом окружении. Вот несколько распространенных проблем и методов их отладки:

1. Проблемы с CORS — используйте инструменты разработчика в браузере (вкладка Network), чтобы проверить заголовки запросов и ответов. Часто проблема в неправильных заголовках или в различиях между окружениями разработки и продакшн.
2. Токен не отправляется с запросами — проверьте, правильно ли настроен HTTP-перехватчик и добавляется ли заголовок Authorization. Для отладки добавьте консольный вывод перед каждым запросом:

JavaScript
1
2
3
4
5
request: function(config) {
  console.log('Отправляется запрос:', config.url);
  console.log('Заголовки:', config.headers);
  return config;
}
3. Ошибки валидации JWT — проверьте время на сервере и клиенте, особенно в разных временных зонах. Несинхронизированные часы могут привести к тому, что токен будет считаться устаревшим.
4. Проблемы с обновлением токенов — добавьте подробное логирование процесса обновления:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public async Task<TokenResponse> RefreshTokensAsync(string refreshToken)
{
  _logger.LogInformation("Начало обновления токена: {RefreshToken}", refreshToken.Substring(0, 10) + "...");
  
  try
  {
    // Логика обновления
    
    _logger.LogInformation("Токен успешно обновлен");
    return tokenResponse;
  }
  catch (Exception ex)
  {
    _logger.LogError(ex, "Ошибка при обновлении токена");
    throw;
  }
}
5. Потеря состояния сессии — проверьте настройки куки, особенно параметры SameSite и Secure. В новых версиях браузеров требования к этим параметрам были ужесточены.

Инструменты, которые я считаю незаменимыми при отладке интеграции:
Fiddler или Charles Proxy — для перехвата и анализа HTTP-трафика между клиентом и сервером,
JWT.io — для декодирования и проверки JWT-токенов,
Postman — для тестирования API без необходимости использовать фронтенд,
Browser DevTools — особенно вкладки Network и Application для анализа запросов и хранилища.

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

Приложения с авторизацией



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

Структура решения



Давайте начнем с общей структуры нашего приложения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/AuthApp
  /AuthApp.API        # ASP.NET Web API проект
    /Controllers      # API контроллеры
    /Models           # Модели данных
    /Services         # Сервисы для бизнес-логики
    /Infrastructure   # Middleware, конфигурация, вспомогательные классы
  
  /AuthApp.Web        # AngularJS SPA проект
    /app
      /auth           # Модуль аутентификации
        /controllers  # Контроллеры для форм логина/регистрации
        /services     # Сервисы для работы с API аутентификации
        /directives   # Директивы для форм и компонентов
        /views        # Представления (HTML-шаблоны)
      /dashboard      # Защищенная часть приложения
      /common         # Общие компоненты
Такая структура обеспечивает четкое разделение ответственности и облегчает сопровождение кода в долгосрочной перспективе.

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



Начнем с серверной части. Вот ключевые файлы, которые нам понадобятся:

User.cs - модель пользователя



C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class User
{
    public int Id { get; set; }
    
    [Required, EmailAddress, MaxLength(100)]
    public string Email { get; set; }
    
    [Required]
    public string PasswordHash { get; set; }
    
    public string PasswordSalt { get; set; }
    
    [MaxLength(100)]
    public string FullName { get; set; }
    
    public bool IsActive { get; set; } = true;
    
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    
    public DateTime? LastLoginDate { get; set; }
}

AuthController.cs - контроллер авторизации



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
[Route("api/auth")]
[ApiController]
public class AuthController : ControllerBase
{
    private readonly IUserService _userService;
    private readonly IJwtService _jwtService;
    private readonly ILoginAttemptService _loginAttemptService;
    private readonly ILogger<AuthController> _logger;
    
    public AuthController(
        IUserService userService,
        IJwtService jwtService,
        ILoginAttemptService loginAttemptService,
        ILogger<AuthController> logger)
    {
        _userService = userService;
        _jwtService = jwtService;
        _loginAttemptService = loginAttemptService;
        _logger = logger;
    }
    
    [HttpPost("login")]
    public async Task<IActionResult> Login(LoginModel model)
    {
        if (!ModelState.IsValid)
            return BadRequest(ModelState);
            
        var ipAddress = HttpContext.Connection.RemoteIpAddress.ToString();
        var userAgent = Request.Headers["User-Agent"].ToString();
        
        // Проверка на блокировку по превышению лимита попыток
        if (await _loginAttemptService.ExceedsThresholdAsync(ipAddress, 5, TimeSpan.FromMinutes(15)))
        {
            await _loginAttemptService.LogAttemptAsync(model.Email, ipAddress, userAgent, false, "Rate limit exceeded");
            return StatusCode(429, new { message = "Слишком много попыток входа. Попробуйте позже." });
        }
        
        // Аутентификация
        var result = await _userService.AuthenticateAsync(model.Email, model.Password);
        
        if (!result.Succeeded)
        {
            await _loginAttemptService.LogAttemptAsync(model.Email, ipAddress, userAgent, false, result.FailureReason.ToString());
            return Unauthorized(new { message = "Неверный email или пароль" });
        }
        
        // Создаем токены
        var token = _jwtService.GenerateToken(result.User);
        var refreshToken = await _jwtService.GenerateRefreshTokenAsync(result.User, ipAddress);
        
        // Обновляем информацию о входе
        result.User.LastLoginDate = DateTime.UtcNow;
        await _userService.UpdateUserAsync(result.User);
        
        // Логируем успешный вход
        await _loginAttemptService.LogAttemptAsync(model.Email, ipAddress, userAgent, true);
        
        // Устанавливаем refresh токен в HttpOnly cookie
        Response.Cookies.Append("refresh_token", refreshToken, new CookieOptions
        {
            HttpOnly = true,
            Secure = true,
            SameSite = SameSiteMode.Strict,
            Expires = DateTimeOffset.UtcNow.AddDays(30)
        });
        
        return Ok(new
        {
            token,
            user = new { id = result.User.Id, email = result.User.Email, fullName = result.User.FullName }
        });
    }
    
    [HttpPost("refresh")]
    public async Task<IActionResult> RefreshToken()
    {
        var refreshToken = Request.Cookies["refresh_token"];
        if (string.IsNullOrEmpty(refreshToken))
            return Unauthorized(new { message = "Отсутствует refresh токен" });
            
        var ipAddress = HttpContext.Connection.RemoteIpAddress.ToString();
        
        try
        {
            var result = await _jwtService.RefreshTokenAsync(refreshToken, ipAddress);
            
            // Устанавливаем новый refresh токен
            Response.Cookies.Append("refresh_token", result.RefreshToken, new CookieOptions
            {
                HttpOnly = true,
                Secure = true,
                SameSite = SameSiteMode.Strict,
                Expires = DateTimeOffset.UtcNow.AddDays(30)
            });
            
            return Ok(new { token = result.AccessToken });
        }
        catch (SecurityException ex)
        {
            _logger.LogWarning(ex, "Ошибка при обновлении токена");
            return Unauthorized(new { message = ex.Message });
        }
    }
    
    [HttpPost("logout")]
    [Authorize]
    public async Task<IActionResult> Logout()
    {
        var refreshToken = Request.Cookies["refresh_token"];
        if (!string.IsNullOrEmpty(refreshToken))
        {
            await _jwtService.RevokeRefreshTokenAsync(refreshToken);
        }
        
        Response.Cookies.Delete("refresh_token");
        
        return Ok(new { message = "Выход выполнен успешно" });
    }
}

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



Теперь реализуем клиентскую часть на AngularJS:

auth.module.js



JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function() {
    'use strict';
    
    angular.module('app.auth', ['ngRoute', 'ngCookies'])
        .config(configFunction);
        
    configFunction.$inject = ['$routeProvider'];
    
    function configFunction($routeProvider) {
        $routeProvider
            .when('/login', {
                templateUrl: 'app/auth/views/login.html',
                controller: 'LoginController',
                controllerAs: 'vm'
            })
            .when('/register', {
                templateUrl: 'app/auth/views/register.html',
                controller: 'RegisterController',
                controllerAs: 'vm'
            });
    }
})();

auth.service.js



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
69
70
71
72
73
74
75
76
77
78
79
(function() {
    'use strict';
    
    angular
        .module('app.auth')
        .factory('authService', authService);
        
    authService.$inject = ['$http', '$q', '$window', 'tokenService'];
    
    function authService($http, $q, $window, tokenService) {
        var service = {
            login: login,
            logout: logout,
            register: register,
            isAuthenticated: isAuthenticated,
            getCurrentUser: getCurrentUser
        };
        
        return service;
        
        function login(credentials) {
            return $http.post('/api/auth/login', credentials)
                .then(function(response) {
                    tokenService.setToken(response.data.token);
                    $window.localStorage.setItem('currentUser', JSON.stringify(response.data.user));
                    return response.data;
                })
                .catch(function(error) {
                    // Преобразуем ошибку для удобной обработки в контроллере
                    if (error.status === 429) {
                        return $q.reject({
                            type: 'RateLimitExceeded',
                            message: error.data.message
                        });
                    }
                    
                    if (error.status === 401) {
                        return $q.reject({
                            type: 'InvalidCredentials',
                            message: 'Неверный email или пароль'
                        });
                    }
                    
                    return $q.reject({
                        type: 'UnknownError',
                        message: 'Произошла ошибка при входе. Попробуйте позже.'
                    });
                });
        }
        
        function logout() {
            return $http.post('/api/auth/logout')
                .then(function() {
                    tokenService.removeToken();
                    $window.localStorage.removeItem('currentUser');
                })
                .catch(function() {
                    // Даже при ошибке на сервере очищаем локальное хранилище
                    tokenService.removeToken();
                    $window.localStorage.removeItem('currentUser');
                    
                    return $q.resolve(); // Не показываем ошибку пользователю
                });
        }
        
        function register(user) {
            return $http.post('/api/auth/register', user);
        }
        
        function isAuthenticated() {
            return tokenService.getToken() !== null;
        }
        
        function getCurrentUser() {
            var userStr = $window.localStorage.getItem('currentUser');
            return userStr ? JSON.parse(userStr) : null;
        }
    }
})();

token.service.js



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
(function() {
    'use strict';
    
    angular
        .module('app.auth')
        .factory('tokenService', tokenService);
        
    tokenService.$inject = ['$window', '$http'];
    
    function tokenService($window, $http) {
        var service = {
            getToken: getToken,
            setToken: setToken,
            removeToken: removeToken,
            refreshToken: refreshToken
        };
        
        var TOKEN_KEY = 'access_token';
        
        return service;
        
        function getToken() {
            return $window.localStorage.getItem(TOKEN_KEY);
        }
        
        function setToken(token) {
            $window.localStorage.setItem(TOKEN_KEY, token);
        }
        
        function removeToken() {
            $window.localStorage.removeItem(TOKEN_KEY);
        }
        
        function refreshToken() {
            return $http.post('/api/auth/refresh')
                .then(function(response) {
                    setToken(response.data.token);
                    return response.data.token;
                });
        }
    }
})();

login.html



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
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
<div class="login-container">
    <div class="panel">
        <div class="panel-heading">
            <h3 class="panel-title">Вход в систему</h3>
        </div>
        <div class="panel-body">
            <form name="loginForm" ng-submit="vm.login()" novalidate>
                <!-- Сообщение об ошибке -->
                <div class="alert alert-danger" ng-if="vm.errorMessage">
                    <i class="fa fa-exclamation-circle"></i> {{vm.errorMessage}}
                </div>
                
                <!-- Email -->
                <div class="form-group" ng-class="{'has-error': loginForm.email.$invalid && (loginForm.email.$dirty || loginForm.$submitted)}">
                    <label for="email">Email</label>
                    <input type="email" 
                           id="email" 
                           name="email" 
                           class="form-control" 
                           ng-model="vm.credentials.email" 
                           required 
                           autofocus>
                    <div class="help-block" ng-if="loginForm.email.$error.required && (loginForm.email.$dirty || loginForm.$submitted)">
                        Введите email
                    </div>
                    <div class="help-block" ng-if="loginForm.email.$error.email && loginForm.email.$dirty">
                        Введите корректный email
                    </div>
                </div>
                
                <!-- Пароль -->
                <div class="form-group" ng-class="{'has-error': loginForm.password.$invalid && (loginForm.password.$dirty || loginForm.$submitted)}">
                    <label for="password">Пароль</label>
                    <input type="password" 
                           id="password" 
                           name="password" 
                           class="form-control" 
                           ng-model="vm.credentials.password" 
                           required>
                    <div class="help-block" ng-if="loginForm.password.$error.required && (loginForm.password.$dirty || loginForm.$submitted)">
                        Введите пароль
                    </div>
                </div>
                
                <!-- Запомнить меня -->
                <div class="form-group">
                    <div class="checkbox">
                        <label>
                            <input type="checkbox" ng-model="vm.credentials.rememberMe"> Запомнить меня
                        </label>
                    </div>
                </div>
                
                <!-- Кнопка входа -->
                <button type="submit" class="btn btn-primary btn-block" ng-disabled="vm.isLoading">
                    <span ng-if="vm.isLoading"><i class="fa fa-spinner fa-spin"></i> Вход...</span>
                    <span ng-if="!vm.isLoading">Войти</span>
                </button>
            </form>
            
            <div class="social-login">
                <p class="text-center">Или войдите через:</p>
                <div class="row">
                    <div class="col-xs-6">
                        <button class="btn btn-block btn-google" ng-click="vm.loginWithGoogle()">
                            <i class="fa fa-google"></i> Google
                        </button>
                    </div>
                    <div class="col-xs-6">
                        <button class="btn btn-block btn-facebook" ng-click="vm.loginWithFacebook()">
                            <i class="fa fa-facebook"></i> Facebook
                        </button>
                    </div>
                </div>
            </div>
            
            <div class="text-center register-link">
                <p>Нет аккаунта? <a href="#/register">Зарегистрируйтесь</a></p>
                <p><a href="#/forgot-password">Забыли пароль?</a></p>
            </div>
        </div>
    </div>
</div>

login.controller.js



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
(function() {
    'use strict';
    
    angular
        .module('app.auth')
        .controller('LoginController', LoginController);
        
    LoginController.$inject = ['$location', 'authService', 'notifyService'];
    
    function LoginController($location, authService, notifyService) {
        var vm = this;
        
        vm.credentials = {
            email: '',
            password: '',
            rememberMe: false
        };
        
        vm.isLoading = false;
        vm.errorMessage = null;
        
        vm.login = login;
        vm.loginWithGoogle = loginWithGoogle;
        vm.loginWithFacebook = loginWithFacebook;
        
        function login() {
            vm.isLoading = true;
            vm.errorMessage = null;
            
            authService.login(vm.credentials)
                .then(function(data) {
                    notifyService.success('Добро пожаловать, ' + data.user.fullName + '!');
                    $location.path('/dashboard');
                })
                .catch(function(error) {
                    vm.errorMessage = error.message;
                })
                .finally(function() {
                    vm.isLoading = false;
                });
        }
        
        function loginWithGoogle() {
            // Реализация OAuth авторизации через Google
            authService.loginWithProvider('google')
                .then(function(data) {
                    notifyService.success('Добро пожаловать, ' + data.user.fullName + '!');
                    $location.path('/dashboard');
                })
                .catch(function(error) {
                    vm.errorMessage = error.message;
                });
        }
        
        function loginWithFacebook() {
            // Реализация OAuth авторизации через Facebook
            authService.loginWithProvider('facebook')
                .then(function(data) {
                    notifyService.success('Добро пожаловать, ' + data.user.fullName + '!');
                    $location.path('/dashboard');
                })
                .catch(function(error) {
                    vm.errorMessage = error.message;
                });
        }
    }
})();
Вышеприведенный код представляет собой полноценную систему авторизации, которая включает все ключевые компоненты, рассмотренные в предыдущих главах:
1. Безопасное хранение паролей на сервере,
2. Токены JWT для аутентификации,
3. Механизм обновления токенов,
4. Логирование попыток входа,
5. Защита от брутфорс-атак через ограничение попыток,
6. Валидация форм на клиенте и сервере,
7. Поддержка OAuth-авторизации через внешние провайдеры,
8. Функция "Запомнить меня".

Такая система обеспечивает надежную защиту учетных данных пользователей и в то же время предоставляет удобный пользовательский интерфейс для входа в систему.

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

Производительность, масштабируемость и альтернативные подходы



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

Узкие места производительности в системах авторизации



В одном из моих проектов мы столкнулись с серьезным замедлением работы системы после того, как количество пользователей перевалило за 50 тысяч. Основные проблемы возникли в трёх местах:
1. Проверка хешей паролей — особенно если вы используете алгоритмы с высокой вычислительной сложностью (как и должно быть).
2. Валидация JWT-токенов — при большом количестве одновременных запросов.
3. Запросы к базе данных — особенно при неоптимизированных индексах.

Для решения первой проблемы мы применили асинхронную обработку запросов на вход и вертикальное масштабирование серверов. Вторую проблему решили кэшированием проверенных токенов в Redis. С третьей справились, оптимизировав индексы и перейдя на денормализованную структуру данных для часто запрашиваемой информации.

Стратегии кэширования для системы авторизации



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

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
// Кэширование проверенных токенов
public async Task<ClaimsPrincipal> ValidateTokenAsync(string token)
{
    // Сначала проверяем в кэше
    string cacheKey = $"ValidToken:{ComputeHash(token)}";
    
    if (_cache.TryGetValue(cacheKey, out ClaimsPrincipal principal))
    {
        return principal;
    }
    
    // Если в кэше нет, выполняем полную валидацию
    principal = await PerformFullValidationAsync(token);
    
    if (principal != null)
    {
        // Кэшируем результат на время меньше срока действия токена
        var tokenExpiryTime = GetTokenExpiryTime(principal);
        var cacheOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(tokenExpiryTime - DateTime.UtcNow - TimeSpan.FromMinutes(1));
            
        _cache.Set(cacheKey, principal, cacheOptions);
    }
    
    return principal;
}
Этот подход позволяет значительно снизить нагрузку на CPU при валидации токенов, но обратите внимание, что я никогда не кэширую результаты неудачной аутентификации — это может привести к тому, что недействительный токен будет ошибочно отвергаться даже после исправления проблемы.

Горизонтальное масштабирование системы авторизации



Когда вертикального масштабирования становится недостаточно, приходит время для горизонтального расширения. Здесь критически важно правильно спроектировать хранение сессий и токенов:
1. Централизованное хранилище токенов — используйте Redis или другие распределенные кэши.
2. Бессессионная архитектура — по возможности используйте JWT-токены и избегайте хранения состояния на сервере.
3. Репликация данных пользователей — обеспечьте быстрый доступ к основным данным на всех нодах.
В одном банковском проекте мы столкнулись с необходимостью обрабатывать более 1000 запросов авторизации в секунду. Решением стало разделение системы на микросервисы, где отдельный Authentication Service занимался только задачами авторизации и выдачи токенов. Этот сервис масштабировался независимо от остальной системы и имел собственный кэш токенов.

Альтернативные подходы к авторизации



Хотя JWT стал почти стандартом для современных веб-приложений, существуют и другие подходы, которые могут лучше подойти для конкретных сценариев:
1. Сессии на основе Redis — более безопасны, чем JWT, если правильно реализованы,
2. Токены на основе базы данных — обеспечивают немедленный отзыв токенов,
3. OAuth 2.0 с Authorization Code Flow — идеален для распределенных систем,
4. PASETO токены — более безопасная альтернатива JWT
Я долгое время был приверженцем JWT, но после участия в проекте, где мы столкнулись с необходимостью немедленного отзыва токенов, я оценил преимущества подхода с использованием Redis для хранения ссылок на сессии.

Что дальше? Тренды и эволюция



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

1. Беспарольная аутентификация — использование WebAuthn и FIDO2.
2. Контекстная аутентификация — учет поведения пользователя, местоположения и устройства.
3. Самосуверенная идентификация — с использованием блокчейн-технологий.
4. Единые стандарты для Web и мобильных приложений — упрощение разработки.

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

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
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru