Форма логина на AngularJS с ASP.NET, часть 1
Форма логина на AngularJS с ASP.NET, часть 2
Форма логина на AngularJS с ASP.NET, часть 3
Форма логина на AngularJS с ASP.NET, часть 4
Кэширование данных пользователей и оптимизация запросов
Когда ваше приложение начинает расти, каждый запрос к базе данных становится на вес золота. Особенно это касается данных авторизации — ведь к ним обращаются при каждом действии пользователя! В одном из моих проектов именно проверка авторизации стала узким местом, которое тормозило всё приложение. Решение? Правильно настроенное кэширование.
Зачем кэшировать данные пользователей
Подумайте сами: когда пользователь делает запрос к защищенному ресурсу, система должна:
1. Проверить валидность токена,
2. Загрузить данные пользователя из базы,
3. Проверить права доступа,
4. Зарегистрировать факт доступа.
И всё это — для каждого запроса! В высоконагруженных системах это может привести к тысячам лишних обращений к базе данных в минуту.
Реализация кэширования в ASP.NET
ASP.NET предоставляет несколько встроенных механизмов кэширования. Для данных авторизации я обычно использую IMemoryCache:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| public class CachedUserRepository : IUserRepository
{
private readonly IUserRepository _innerRepository;
private readonly IMemoryCache _cache;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(10);
public CachedUserRepository(IUserRepository innerRepository, IMemoryCache cache)
{
_innerRepository = innerRepository;
_cache = cache;
}
public async Task<User> GetByEmailAsync(string email)
{
string cacheKey = $"User_{email}";
if (!_cache.TryGetValue(cacheKey, out User user))
{
user = await _innerRepository.GetByEmailAsync(email);
if (user != null)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(_cacheDuration);
_cache.Set(cacheKey, user, cacheOptions);
}
}
return user;
}
// Реализация других методов с инвалидацией кэша
public async Task UpdateAsync(User user)
{
await _innerRepository.UpdateAsync(user);
// Инвалидируем кэш при обновлении
string cacheKey = $"User_{user.Email}";
_cache.Remove(cacheKey);
}
} |
|
Важный момент здесь — инвалидация кэша. Если пользователь изменил пароль или его аккаунт заблокировали, кэш должен быть немедленно обновлен. Иначе пользователь сможет продолжать работать, несмотря на блокировку!
Распределенное кэширование
Для систем с несколькими серверами in-memory кэширование не подойдет — каждый сервер будет иметь свою копию кэша. В таких случаях я использую Redis:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| public void ConfigureServices(IServiceCollection services)
{
// Настройка распределенного кэша
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
options.InstanceName = "AuthApp_";
});
// Регистрация сервисов
services.AddScoped<IUserRepository, UserRepository>();
services.Decorate<IUserRepository, CachedUserRepository>();
} |
|
Когда я внедрял распределенное кэширование в финансовом приложении, мы смогли уменьшить нагрузку на базу данных на 70%! Это позволило справиться с пиковыми нагрузками без добавления новых серверов.
Кэширование токенов и сессий
Помимо данных пользователей, часто требуется кэшировать информацию о токенах и сессиях:
| 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 TokenCacheService : ITokenService
{
private readonly IDistributedCache _cache;
public TokenCacheService(IDistributedCache cache)
{
_cache = cache;
}
public async Task<bool> ValidateTokenAsync(string token)
{
var cacheResult = await _cache.GetStringAsync($"Token_{token}");
if (cacheResult != null)
{
return true; // Токен в кэше, значит он валидный
}
// Если токена нет в кэше, проверяем его валидность
// и добавляем в кэш, если он валидный
return false;
}
} |
|
Оптимизация запросов для авторизации
Кэширование — это лишь часть решения. Не менее важно оптимизировать сами запросы:
1. Используйте проекции — загружайте только нужные данные:
| C# | 1
2
3
4
5
6
7
8
9
| var userAuth = await _context.Users
.Where(u => u.Email == email)
.Select(u => new {
u.Id,
u.PasswordHash,
u.IsActive,
Roles = u.UserRoles.Select(ur => ur.Role.Name)
})
.FirstOrDefaultAsync(); |
|
2. Избегайте N+1 запросов — когда вам нужно загрузить связанные данные для нескольких пользователей, используйте Include:
| C# | 1
2
3
4
5
| var users = await _context.Users
.Include(u => u.UserRoles)
.ThenInclude(ur => ur.Role)
.Where(u => u.IsActive)
.ToListAsync(); |
|
3. Используйте индексы — особенно на полях, по которым выполняется поиск:
| C# | 1
2
3
4
5
| modelBuilder.Entity<User>()
.HasIndex(u => u.Email);
modelBuilder.Entity<User>()
.HasIndex(u => u.LastLoginDate); |
|
Помню случай, когда у клиента была огромная база пользователей (более миллиона записей). Простое добавление индекса на поле Email ускорило авторизацию в 50 раз!
Правильно настроенное кэширование и оптимизированные запросы — это не просто технические улучшения. Это напрямую влияет на пользовательский опыт. Никто не любит ждать, особенно когда речь идет о таком базовом действии, как вход в систему. Инвестируйте время в оптимизацию, и ваши пользователи будут вам благодарны.
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 большая ли разница между ними? Начал во всю осваивать технологию,теперь хочу с книжкой посидеть и вдумчиво перебрать всё то что...
Реализация middleware для обработки запросов аутентификации
Middleware - это, пожалуй, один из самых недооцененных инструментов в ASP.NET при работе с аутентификацией. Долгое время я сам допускал ошибку, пытаясь впихнуть всю логику проверки токенов и сессий в контроллеры или атрибуты, пока не осознал мощь правильно настроенного промежуточного ПО.
Middleware в ASP.NET - это компоненты, формирующие конвейер обработки HTTP-запросов. Каждый запрос проходит через этот конвейер, прежде чем достигнет контроллера. И именно здесь мы можем перехватить запрос, проверить наличие и валидность токена аутентификации, и решить, пропускать ли его дальше.
Создание кастомного middleware для аутентификации
Хотя ASP.NET Core предоставляет готовые компоненты для аутентификации, иногда требуется собственная реализация с уникальной логикой. Вот как я обычно создаю middleware для проверки 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
| public class JwtAuthMiddleware
{
private readonly RequestDelegate _next;
private readonly ITokenService _tokenService;
public JwtAuthMiddleware(RequestDelegate next, ITokenService tokenService)
{
_next = next;
_tokenService = tokenService;
}
public async Task InvokeAsync(HttpContext context)
{
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (token != null)
{
try
{
var userId = _tokenService.ValidateToken(token);
if (userId != null)
{
// Устанавливаем ClaimsIdentity пользователя
context.User = new ClaimsPrincipal(
new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, userId)
}, "jwt"));
}
}
catch (Exception ex)
{
// Логирование ошибки, но продолжаем без аутентификации
Debug.WriteLine($"Token validation failed: {ex.Message}");
}
}
await _next(context);
}
} |
|
Обратите внимание, что даже при неудачной валидации токена мы не блокируем запрос, а просто не устанавливаем идентичность пользователя. Решение о том, пропускать ли неаутентифицированные запросы, будет принимать следующий компонент конвейера или контроллер с атрибутом [Authorize].
Регистрация middleware в конвейере
Чтобы добавить middleware в конвейер обработки запросов, нужно зарегистрировать его в методе Configure класса Startup:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Другие компоненты middleware...
// Добавляем наш middleware для аутентификации
app.UseMiddleware<JwtAuthMiddleware>();
// Стандартный middleware аутентификации и авторизации
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
} |
|
Порядок регистрации middleware критически важен! Ваш кастомный middleware должен быть размещен перед UseAuthentication() и UseAuthorization(), чтобы он мог установить идентичность пользователя до того, как стандартные компоненты авторизации начнут работу.
Middleware для защиты от CSRF-атак
Когда я работал над одним финансовым приложением, нам требовалась особая защита от CSRF-атак для операций с деньгами. Я реализовал это через 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
| 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 (HttpMethods.IsPost(context.Request.Method) ||
HttpMethods.IsPut(context.Request.Method) ||
HttpMethods.IsDelete(context.Request.Method))
{
try
{
await _antiforgery.ValidateRequestAsync(context);
}
catch (AntiforgeryValidationException)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("CSRF token validation failed.");
return;
}
}
await _next(context);
}
} |
|
Middleware для ограничения частоты запросов
Еще одна важная задача - защита от брутфорс-атак путем ограничения частоты запросов к API авторизации:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class RateLimitMiddleware
{
private static readonly Dictionary<string, Queue<DateTime>> RequestTimes = new Dictionary<string, Queue<DateTime>>();
private readonly RequestDelegate _next;
private readonly int _maxRequests;
private readonly TimeSpan _timeWindow;
public RateLimitMiddleware(RequestDelegate next, int maxRequests = 5, int timeWindowSeconds = 60)
{
_next = next;
_maxRequests = maxRequests;
_timeWindow = TimeSpan.FromSeconds(timeWindowSeconds);
}
public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.GetEndpoint()?.DisplayName;
if (endpoint != null && endpoint.Contains("Login"))
{
var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
var key = $"{clientIp}_{endpoint}";
if (!RequestTimes.ContainsKey(key))
{
RequestTimes[key] = new Queue<DateTime>();
}
// Очищаем устаревшие запросы
while (RequestTimes[key].Count > 0 &&
DateTime.Now - RequestTimes[key].Peek() > _timeWindow)
{
RequestTimes[key].Dequeue();
}
if (RequestTimes[key].Count >= _maxRequests)
{
context.Response.StatusCode = 429; // Too Many Requests
await context.Response.WriteAsync("Rate limit exceeded. Try again later.");
return;
}
RequestTimes[key].Enqueue(DateTime.Now);
}
await _next(context);
}
} |
|
Middleware - невероятно мощный инструмент для централизованной обработки аутентификации. Размещая логику проверки токенов и защиты от атак на уровне middleware, вы делаете ее единообразной для всего приложения и избегаете дублирования кода в контроллерах. Это тот редкий случай, когда повышение безопасности идет рука об руку с улучшением архитектуры!
Валидация на стороне клиента и сервера: синхронизация правил
В веб-разработки я постоянно сталкиваюсь с одной коварной проблемой, которая грозит обернуться серьезными багами в продакшене: рассинхронизация правил валидации между клиентом и сервером. Эта проблема возникает, когда, например, AngularJS требует на фронте пароль минимум из 8 символов, а ASP.NET на бэкенде - из 6. И пользователь с 7-символьным паролем сначала радуется успешной регистрации, а потом недоумевает, почему не может войти.
Еще хуже ситуация в обратном направлении: когда фронт разрешает форматы, которые бэкенд отклоняет. Я видел, как это приводило к массовым обращениям в поддержку и ошибочному мнению, что "сайт сломан".
Почему нужна двойная валидация?
Некоторые разработчики наивно полагают, что валидации на клиенте достаточно. Давайте раз и навсегда проясним ситуацию: клиентская валидация существует исключительно для удобства пользователя. С точки зрения безопасности она бесполезна, потому что любой запрос можно сформировать в обход браузера.
Серверная валидация - это последний рубеж обороны ваших данных. Но при этом клиентская валидация критично важна для UX - никто не хочет заполнять форму, отправлять ее и только потом узнавать, что данные некорректны.
Валидация в AngularJS
AngularJS предоставляет мощный инструментарий для валидации форм:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <form name="myForm" novalidate ng-submit="LoginForm()">
<input type="email"
ng-model="UserModel.Email"
name="UserEmail"
ng-class="Submited?'ng-dirty':''"
required autofocus />
<span style="color:red"
ng-show="(myForm.UserEmail.$dirty||Submited)&&myForm.UserEmail.$error.required">
Введите email
</span>
<span style="color:red"
ng-show="myForm.UserEmail.$error.email">
Email некорректен
</span>
</form> |
|
Тут используются встроенные валидаторы required и проверка типа email. AngularJS также позволяет создавать собственные директивы валидации:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
| angular.module('myApp').directive('passwordStrength', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
ngModel.$validators.passwordStrength = function(value) {
// Должен содержать хотя бы одну цифру и одну заглавную букву
var strongRegex = /^(?=.*[A-Z])(?=.*\d).+$/;
return strongRegex.test(value);
};
}
};
}); |
|
Валидация в ASP.NET
На серверной стороне у нас есть атрибуты валидации и ModelState:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| public class LoginModel
{
[Required(ErrorMessage = "Email не может быть пустым")]
[EmailAddress(ErrorMessage = "Некорректный формат email")]
public string Email { get; set; }
[Required(ErrorMessage = "Пароль не может быть пустым")]
[MinLength(8, ErrorMessage = "Пароль должен содержать минимум 8 символов")]
[RegularExpression(@"^(?=.*[A-Z])(?=.*\d).+$",
ErrorMessage = "Пароль должен содержать хотя бы одну цифру и одну заглавную букву")]
public string Password { get; set; }
} |
|
А потом в контроллере:
| C# | 1
2
3
4
5
6
7
8
9
10
| [HttpPost]
public IActionResult Login(LoginModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Логика авторизации
} |
|
Как синхронизировать правила?
Проблема в том, что эти правила живут в разных местах, на разных языках программирования, и изменение одного не приводит к автоматическому обновлению другого. Вот несколько стратегий, которые я применяю:
1. Документирование правил валидации - банально, но эффективно. Отдельный документ с перечнем всех полей и правил валидации к ним.
2. Централизация правил - вынести константы в отдельные файлы:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // validation-constants.js
var VALIDATION = {
PASSWORD_MIN_LENGTH: 8,
PASSWORD_REGEX: /^(?=.*[A-Z])(?=.*\d).+$/
};
[/CSHARP]
[CSHARP]
// ValidationConstants.cs
public static class ValidationConstants
{
public const int PasswordMinLength = 8;
public const string PasswordRegex = @"^(?=.*[A-Z])(?=.*\d).+$";
} |
|
3. Генерация клиентского кода из серверных моделей - есть инструменты типа AutoMapper или ручная генерация JavaScript на основе C# моделей.
4. API для получения правил валидации - бэкенд предоставляет эндпоинт, который возвращает все правила в формате JSON, фронтенд их загружает и применяет.
У последнего подхода есть интересный побочный эффект - он позволяет менять правила валидации без редеплоя фронтенда, что может быть полезно для A/B-тестирования или быстрой реакции на изменения требований.
Обработка ошибок валидации
Даже при идеально синхронизированных правилах всегда найдется пользователь, который обойдет клиентскую валидацию (намеренно или случайно). Поэтому важно корректно обрабатывать ошибки серверной валидации:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| loginService.login = function(credentials) {
return $http.post('/api/auth/login', credentials)
.catch(function(response) {
if (response.status === 400 && response.data.modelState) {
// Преобразуем ошибки валидации с сервера в формат для отображения
var errors = [];
angular.forEach(response.data.modelState, function(messages, field) {
angular.forEach(messages, function(message) {
errors.push({ field: field, message: message });
});
});
return $q.reject(errors);
}
return $q.reject(response);
});
}; |
|
А затем в контроллере:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| $scope.login = function() {
loginService.login($scope.credentials)
.then(function(result) {
// Успешный вход
})
.catch(function(errors) {
if (Array.isArray(errors)) {
// Это ошибки валидации, показываем их в форме
$scope.serverErrors = errors;
} else {
// Другая ошибка
$scope.errorMessage = "Произошла ошибка. Попробуйте позже.";
}
});
}; |
|
Синхронизация правил валидации между клиентом и сервером может показаться мелочью, но, поверьте моему опыту, это та область, где маленькие несостыковки приводят к большим проблемам. Особенно в проектах, где фронтенд и бэкенд разрабатываются разными командами.
Создание Angular-модулей для аутентификации
Когда я только начинал работать с AngularJS, я наивно полагал, что для небольшой формы логина достаточно просто добавить немного кода в общий скрипт приложения. Ох, как же я ошибался! Именно с появлением требований по расширению функциональности аутентификации я понял, насколько важна модульная структура.
В AngularJS модули — это не просто способ организации кода, а полноценный механизм инкапсуляции и управления зависимостями. Правильно организованный модуль аутентификации позволяет легко расширять, тестировать и поддерживать код, связанный с авторизацией пользователей.
Архитектура модуля аутентификации
Начнем с создания базового модуля:
| JavaScript | 1
2
3
4
5
6
7
8
9
| (function() {
'use strict';
// Создаем модуль аутентификации с зависимостями
angular.module('app.auth', [
'ngRoute', // Для маршрутизации
'ngStorage' // Для хранения токена в localStorage
]);
})(); |
|
Я всегда использую паттерн IIFE (Immediately Invoked Function Expression), чтобы избежать загрязнения глобального пространства имён. Это крайне важно для крупных приложений, где разные модули могут случайно перезаписать переменные друг друга.
Структура каталогов для модуля аутентификации
Для поддерживаемости кода критически важна правильная структура файлов. Обычно я организую её так:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| /app
/auth
/services
auth.service.js // Основная логика аутентификации
token.service.js // Работа с токенами и сессиями
/controllers
login.controller.js
register.controller.js
password-reset.controller.js
/directives
password-strength.directive.js
/views
login.html
register.html
password-reset.html
auth.module.js // Определение модуля
auth.routes.js // Конфигурация маршрутов
auth.interceptor.js // HTTP-перехватчик для добавления токенов |
|
Такая структура позволяет быстро находить нужные компоненты и интуитивно понятна для новых разработчиков в команде.
Сервис аутентификации
Ядро нашего модуля — сервис аутентификации. Он инкапсулирует всю логику взаимодействия с серверным API:
| 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
| (function() {
'use strict';
angular
.module('app.auth')
.factory('authService', authService);
authService.$inject = ['$http', '$q', 'tokenService'];
function authService($http, $q, tokenService) {
var service = {
login: login,
logout: logout,
register: register,
isAuthenticated: isAuthenticated,
getCurrentUser: getCurrentUser
};
return service;
function login(credentials) {
return $http.post('/api/auth/token', credentials)
.then(function(response) {
tokenService.setToken(response.data.token);
return response.data;
});
}
function logout() {
return $http.delete('/api/auth/session')
.finally(function() {
tokenService.removeToken();
});
}
function register(user) {
return $http.post('/api/users', user);
}
function isAuthenticated() {
return tokenService.getToken() !== null;
}
function getCurrentUser() {
if (!isAuthenticated()) {
return $q.reject('Пользователь не авторизован');
}
return $http.get('/api/users/me')
.then(function(response) {
return response.data;
});
}
}
})(); |
|
Заметьте, что я разделил ответственность: authService отвечает за бизнес-логику аутентификации, а хранение токенов вынесено в отдельный tokenService. Это следование принципу единственной ответственности (Single Responsibility Principle) из SOLID.
Сервис для работы с токенами
| 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
| (function() {
'use strict';
angular
.module('app.auth')
.factory('tokenService', tokenService);
tokenService.$inject = ['$localStorage'];
function tokenService($localStorage) {
var TOKEN_KEY = 'auth_token';
var service = {
getToken: getToken,
setToken: setToken,
removeToken: removeToken
};
return service;
function getToken() {
return $localStorage[TOKEN_KEY];
}
function setToken(token) {
$localStorage[TOKEN_KEY] = token;
}
function removeToken() {
delete $localStorage[TOKEN_KEY];
}
}
})(); |
|
Я помню один проект, где токен хранился в обычной переменной JavaScript без использования localStorage или sessionStorage. После каждого обновления страницы пользователи вылетали из системы! Мне пришлось потратить день, чтобы переписать эту часть кода и прекратить пытки пользователей.
Перехватчик HTTP-запросов
Для автоматического добавления токена к исходящим запросам и обработки ошибок авторизации создадим 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
| (function() {
'use strict';
angular
.module('app.auth')
.factory('authInterceptor', authInterceptor)
.config(configFunction);
authInterceptor.$inject = ['$q', 'tokenService', '$location'];
configFunction.$inject = ['$httpProvider'];
function authInterceptor($q, tokenService, $location) {
return {
request: function(config) {
config.headers = config.headers || {};
var token = tokenService.getToken();
if (token) {
config.headers.Authorization = 'Bearer ' + token;
}
return config;
},
responseError: function(rejection) {
if (rejection.status === 401) {
// Если сервер вернул 401, удаляем токен и перенаправляем на логин
tokenService.removeToken();
$location.path('/login');
}
return $q.reject(rejection);
}
};
}
function configFunction($httpProvider) {
$httpProvider.interceptors.push('authInterceptor');
}
})(); |
|
Этот перехватчик решает сразу две задачи: добавляет токен ко всем запросам и обрабатывает ситуацию, когда токен становится недействительным. Удивительно, сколько раз я встречал проекты, где эти 20 строк кода отсутствовали, а вместо этого токен добавлялся вручную в каждом запросе!
Контроллер формы входа
Теперь создадим контроллер для формы входа:
| 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
| (function() {
'use strict';
angular
.module('app.auth')
.controller('LoginController', LoginController);
LoginController.$inject = ['$scope', '$location', 'authService'];
function LoginController($scope, $location, authService) {
$scope.UserModel = {
Email: '',
Password: ''
};
$scope.Submited = false;
$scope.IsLoggedIn = false;
$scope.IsFormValid = false;
$scope.msg = "";
// Отслеживаем валидность формы
$scope.$watch("myForm.$valid", function(TrueOrFalse) {
$scope.IsFormValid = TrueOrFalse;
});
$scope.LoginForm = function() {
$scope.Submited = true;
if ($scope.IsFormValid) {
authService.login($scope.UserModel)
.then(function(response) {
if (response.Email != null) {
$scope.IsLoggedIn = true;
$scope.msg = "Вы успешно вошли, " + response.FullName;
// Перенаправление на защищенную страницу
$location.path('/dashboard');
} else {
alert("Неверные учетные данные!");
}
})
.catch(function(error) {
$scope.errorMessage = "Ошибка при входе. Пожалуйста, попробуйте еще раз.";
});
}
};
}
})(); |
|
В реальных проектах я обычно использую подход с controllerAs вместо $scope, так как это делает код более читаемым и помагает избежать проблем с наследованием областей видимости.
Защита маршрутов
Для защиты маршрутов от неавторизованного доступа настроим маршрутизацию с проверкой аутентификации:
| 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
| (function() {
'use strict';
angular
.module('app.auth')
.config(routeConfig);
routeConfig.$inject = ['$routeProvider'];
function routeConfig($routeProvider) {
$routeProvider
.when('/login', {
templateUrl: 'app/auth/views/login.html',
controller: 'LoginController'
})
.when('/dashboard', {
templateUrl: 'app/dashboard/dashboard.html',
controller: 'DashboardController',
resolve: {
authCheck: ['authService', function(authService) {
return authService.getCurrentUser();
}]
}
});
}
})(); |
|
Параметр resolve — это мощный механизм, который позволяет выполнить асинхронные операции перед загрузкой представления. Если getCurrentUser() вернет ошибку (т.е. пользователь не аутентифицирован), маршрут не загрузится.
Вместе с этим нужно добавить обработчик события $routeChangeError:
| JavaScript | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| (function() {
'use strict';
angular
.module('app.auth')
.run(runBlock);
runBlock.$inject = ['$rootScope', '$location'];
function runBlock($rootScope, $location) {
$rootScope.$on('$routeChangeError', function(event, current, previous, rejection) {
if (rejection === 'Пользователь не авторизован') {
$location.path('/login');
}
});
}
})(); |
|
Теперь, когда пользователь попытается получить доступ к защищенному маршруту без аутентификации, он будет автоматически перенаправлен на страницу входа.
Модульный подход к организации кода аутентификации в AngularJS не только делает код более чистым и понятным, но и значительно упрощает его тестирование. Вы можете легко создать юнит-тесты для каждого компонента, используя Angular Mocks, и быть уверенными в надежности своей системы авторизации.
Построение клиентской логики: сервисы и контроллеры
В мире фронтенд-разработки на 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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
| angular.module('app.auth').factory('authService', function($http, $q, localStorageService) {
// Приватные переменные сервиса
var currentUser = null;
// Публичное API сервиса
var service = {
login: login,
logout: logout,
isAuthenticated: isAuthenticated,
getCurrentUser: getCurrentUser,
resetPassword: resetPassword,
refreshToken: refreshToken
};
return service;
// Реализация методов
function login(credentials) {
return $http.post('/api/auth/token', credentials)
.then(function(response) {
// Сохраняем токен в localStorage
localStorageService.set('auth_token', response.data.token);
// Сохраняем время истечения токена
var expirationDate = new Date();
expirationDate.setMinutes(expirationDate.getMinutes() + 30); // 30 минут
localStorageService.set('token_expires', expirationDate.getTime());
// Запрашиваем данные пользователя
return getUserProfile();
})
.catch(function(error) {
// Централизованная обработка ошибок
return handleAuthError(error, 'Ошибка при входе в систему');
});
}
function getUserProfile() {
return $http.get('/api/users/me')
.then(function(response) {
currentUser = response.data;
return currentUser;
});
}
function logout() {
// Отправляем запрос на сервер для инвалидации токена
var token = localStorageService.get('auth_token');
if (token) {
// Не ждем ответа - даже если запрос не удастся, мы все равно
// удалим токен на клиенте
$http.delete('/api/auth/token');
// Очищаем локальное хранилище
localStorageService.remove('auth_token');
localStorageService.remove('token_expires');
currentUser = null;
}
return $q.resolve();
}
function isAuthenticated() {
var token = localStorageService.get('auth_token');
var expires = localStorageService.get('token_expires');
if (!token || !expires) {
return false;
}
// Проверяем, не истек ли токен
return new Date().getTime() < expires;
}
function getCurrentUser() {
if (!isAuthenticated()) {
return $q.reject('Пользователь не авторизован');
}
if (currentUser) {
return $q.resolve(currentUser);
}
// Если у нас есть токен, но нет данных пользователя, запрашиваем их
return getUserProfile();
}
function refreshToken() {
if (!isAuthenticated()) {
return $q.reject('Невозможно обновить токен: пользователь не авторизован');
}
return $http.post('/api/auth/token/refresh')
.then(function(response) {
localStorageService.set('auth_token', response.data.token);
var expirationDate = new Date();
expirationDate.setMinutes(expirationDate.getMinutes() + 30);
localStorageService.set('token_expires', expirationDate.getTime());
return response.data;
});
}
function resetPassword(email) {
return $http.post('/api/auth/reset-password', { email: email })
.catch(function(error) {
return handleAuthError(error, 'Ошибка при сбросе пароля');
});
}
// Приватный метод для обработки ошибок
function handleAuthError(error, defaultMessage) {
var errorMessage = defaultMessage;
if (error.data && error.data.message) {
errorMessage = error.data.message;
} else if (error.status === 401) {
errorMessage = 'Неверные учетные данные';
} else if (error.status === 403) {
errorMessage = 'Доступ запрещен';
} else if (error.status === 429) {
errorMessage = 'Слишком много попыток. Попробуйте позже.';
}
return $q.reject({ message: errorMessage, originalError: error });
}
}); |
|
В этом сервисе я реализовал несколько важных паттернов:
1. Разделение публичного API и приватных методов — только нужные методы экспортируются в service, остальные доступны только внутри.
2. Централизованная обработка ошибок — функция handleAuthError унифицирует формат ошибок.
3. Кэширование данных пользователя — переменная currentUser хранит профиль, чтобы не делать лишние запросы.
4. Автоматическое обновление токена — метод refreshToken позволяет продлить сессию без повторного входа.
Раньше я делал ошибку, храня слишком много логики в контроллерах. Это приводило к дублированию кода, когда одни и те же операции с токенами нужно было выполнять в разных частях приложения. Вынесение этой логики в сервис решило проблему раз и навсегда.
Контроллеры — связующее звено с пользовательским интерфейсом
Контроллеры в 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
| angular.module('app.auth').controller('LoginController', function($scope, $location, authService, notifyService) {
// Модель для привязки к форме
$scope.credentials = {
email: '',
password: ''
};
// Флаги состояния
$scope.isLoading = false;
$scope.errorMessage = null;
$scope.rememberMe = false;
// Отслеживаем состояние формы для валидации
$scope.submitted = false;
// Метод для отправки формы
$scope.login = function() {
$scope.submitted = true;
$scope.errorMessage = null;
// Проверяем валидность формы
if ($scope.loginForm.$invalid) {
return;
}
$scope.isLoading = true;
authService.login($scope.credentials)
.then(function(user) {
notifyService.success('Добро пожаловать, ' + user.displayName + '!');
// Если пользователь был перенаправлен на страницу логина,
// возвращаем его на исходную страницу
var redirectUrl = $location.search().redirect || '/dashboard';
$location.path(redirectUrl).search('redirect', null);
})
.catch(function(error) {
$scope.errorMessage = error.message;
})
.finally(function() {
$scope.isLoading = false;
});
};
// Метод для сброса пароля
$scope.forgotPassword = function() {
var email = $scope.credentials.email;
if (!email) {
$scope.errorMessage = 'Введите email для сброса пароля';
return;
}
$scope.isLoading = true;
authService.resetPassword(email)
.then(function() {
notifyService.info('Инструкции по сбросу пароля отправлены на ' + email);
})
.catch(function(error) {
$scope.errorMessage = error.message;
})
.finally(function() {
$scope.isLoading = false;
});
};
}); |
|
Обратите внимание, как контроллер делегирует всю логику работы с API сервису authService. Он фокусируется только на:
1. Управлении состоянием формы (submitted, isLoading),
2. Обработке ввода пользователя,
3. Отображении уведомлений и ошибок,
4. Навигации после успешного входа
Взаимодействие между сервисами
Часто в реальных проектах нужно, чтобы разные сервисы взаимодействовали между собой. Например, сервис для работы с уведомлениями:
| 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.common').factory('notifyService', function($rootScope) {
var service = {
success: showSuccess,
error: showError,
info: showInfo,
warning: showWarning
};
return service;
function showSuccess(message) {
show({ type: 'success', message: message });
}
function showError(message) {
show({ type: 'error', message: message });
}
function showInfo(message) {
show({ type: 'info', message: message });
}
function showWarning(message) {
show({ type: 'warning', message: message });
}
function show(notification) {
// Отправляем событие, которое будет перехвачено директивой уведомлений
$rootScope.$broadcast('notify', notification);
}
}); |
|
Этот сервис используется для отображения уведомлений пользователю. В реальных проектах я обычно интегрирую его с библиотеками типа Toastr для красивых всплывающих сообщений.
Еще один пример — сервис для отслеживания состояния аутентификации:
| 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
| angular.module('app.auth').factory('authStateService', function($rootScope, authService) {
// Храним состояние аутентификации
var isAuthenticated = authService.isAuthenticated();
var currentUser = null;
// Инициализация: получаем текущего пользователя
if (isAuthenticated) {
authService.getCurrentUser()
.then(function(user) {
currentUser = user;
broadcastAuthChange();
});
}
// При смене маршрута проверяем состояние аутентификации
$rootScope.$on('$routeChangeStart', function() {
var newState = authService.isAuthenticated();
if (newState !== isAuthenticated) {
isAuthenticated = newState;
if (isAuthenticated) {
authService.getCurrentUser()
.then(function(user) {
currentUser = user;
broadcastAuthChange();
});
} else {
currentUser = null;
broadcastAuthChange();
}
}
});
// Уведомляем компоненты об изменении состояния
function broadcastAuthChange() {
$rootScope.$broadcast('auth:stateChanged', {
isAuthenticated: isAuthenticated,
currentUser: currentUser
});
}
return {
isAuthenticated: function() { return isAuthenticated; },
getCurrentUser: function() { return currentUser; }
};
}); |
|
Этот сервис отслеживает изменения состояния аутентификации и уведомляет об этом другие компоненты через события. Это очень удобно, когда у вас есть, например, шапка сайта, которая должна показывать имя пользователя или кнопку входа в зависимости от состояния аутентификации.
Не забывайте, что сервисы — это синглтоны, поэтому они идеально подходят для хранения общего состояния и централизации бизнес-логики. Правильное разделение ответственности между сервисами и контроллерами — это тот фундамент, на котором строится масштабируемое и поддерживаемое приложение.
Создание переиспользуемых компонентов формы входа
За годы разработки я не раз сталкивался с ситуацией, когда приходилось создавать почти идентичные формы аутентификации для разных проектов или даже в рамках одного приложения. Поначалу я просто копировал готовый код, внося небольшие изменения. Но со временем понял: такой подход ведет к кошмару сопровождения. Стоит внести изменение в одну форму — и приходится вручную обновлять все остальные.
Решение? Создание переиспользуемых компонентов. В 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
| angular.module('app.auth').directive('authInput', function() {
return {
restrict: 'E',
replace: true,
scope: {
model: '=ngModel',
type: '@',
label: '@',
required: '=?',
minLength: '@?',
placeholder: '@?',
name: '@'
},
templateUrl: 'app/auth/directives/auth-input.html',
link: function(scope, element, attrs, ctrl) {
// Устанавливаем значения по умолчанию
scope.required = scope.required !== false;
scope.type = scope.type || 'text';
scope.minLength = scope.minLength || 0;
// Уникальный ID для связи label с input
scope.inputId = 'input_' + scope.name + '_' + Math.random().toString(36).substr(2, 9);
}
};
}); |
|
Шаблон для этой директивы (auth-input.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
| <div class="form-group" ng-class="{'has-error': form[name].$invalid && (form[name].$dirty || form.$submitted)}">
<label for="{{inputId}}" class="control-label">{{label}}</label>
<input type="{{type}}"
class="form-control"
id="{{inputId}}"
name="{{name}}"
ng-model="model"
ng-required="required"
ng-minlength="minLength"
placeholder="{{placeholder}}" />
<div class="help-block" ng-if="form[name].$error.required && (form[name].$dirty || form.$submitted)">
Поле "{{label}}" обязательно для заполнения
</div>
<div class="help-block" ng-if="form[name].$error.minlength && form[name].$dirty">
Поле "{{label}}" должно содержать минимум {{minLength}} символов
</div>
<div class="help-block" ng-if="form[name].$error.email && form[name].$dirty">
Введите корректный email
</div>
</div> |
|
Теперь мы можем использовать эту директиву для создания любых полей ввода в форме логина:
| HTML5 | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| <form name="loginForm" ng-submit="login()" novalidate>
<auth-input ng-model="credentials.email"
type="email"
label="Email"
name="email"
required="true"></auth-input>
<auth-input ng-model="credentials.password"
type="password"
label="Пароль"
name="password"
min-length="8"
required="true"></auth-input>
<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> |
|
Директива для полной формы логина
Пойдем дальше и создадим директиву для всей формы входа:
| 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
| angular.module('app.auth').directive('loginForm', function() {
return {
restrict: 'E',
scope: {
onSuccess: '&',
redirectUrl: '@?'
},
templateUrl: 'app/auth/directives/login-form.html',
controller: function($scope, $location, authService, notifyService) {
$scope.credentials = {
email: '',
password: ''
};
$scope.isLoading = false;
$scope.errorMessage = null;
$scope.login = function() {
if ($scope.loginForm.$invalid) {
return;
}
$scope.isLoading = true;
$scope.errorMessage = null;
authService.login($scope.credentials)
.then(function(user) {
if ($scope.onSuccess) {
$scope.onSuccess({user: user});
} else if ($scope.redirectUrl) {
$location.path($scope.redirectUrl);
}
})
.catch(function(error) {
$scope.errorMessage = error.message;
})
.finally(function() {
$scope.isLoading = false;
});
};
}
};
}); |
|
Теперь достаточно одной строки, чтобы добавить форму логина на любую страницу:
| HTML5 | 1
| <login-form redirect-url="/dashboard"></login-form> |
|
Или с кастомным обработчиком успешного входа:
| HTML5 | 1
| <login-form on-success="handleSuccessfulLogin(user)"></login-form> |
|
Компонент для формы входа (AngularJS 1.5+)
В более поздних версиях 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
| angular.module('app.auth').component('loginForm', {
templateUrl: 'app/auth/components/login-form.html',
bindings: {
onSuccess: '&',
redirectUrl: '@?'
},
controller: function($location, authService) {
var ctrl = this;
ctrl.credentials = {
email: '',
password: ''
};
ctrl.isLoading = false;
ctrl.errorMessage = null;
ctrl.login = function() {
if (ctrl.loginForm.$invalid) {
return;
}
ctrl.isLoading = true;
ctrl.errorMessage = null;
authService.login(ctrl.credentials)
.then(function(user) {
if (ctrl.onSuccess) {
ctrl.onSuccess({user: user});
} else if (ctrl.redirectUrl) {
$location.path(ctrl.redirectUrl);
}
})
.catch(function(error) {
ctrl.errorMessage = error.message;
})
.finally(function() {
ctrl.isLoading = false;
});
};
}
}); |
|
Я активно использую этот подход на текущих проектах. Компоненты легче поддерживать, они имеют более предсказуемый жизненный цикл и лучше вписываются в современную архитектуру фронтенда.
Преимущества переиспользуемых компонентов
Кроме очевидного сокращения дублирования кода, переиспользуемые компоненты дают нам:
1. Единообразие пользовательского опыта — все формы в приложении выглядят и ведут себя одинаково;
2. Централизованные изменения — нужно обновить логику валидации? Меняете в одном месте, работает везде;
3. Упрощенное тестирование — можно написать тесты для компонента один раз и быть уверенным, что он работает правильно во всех местах использования;
4. Ускорение разработки — новые формы создаются буквально за минуты.
Последний пункт я особенно ценю. В одном из проектов нам пришлось срочно добавить форму восстановления пароля. Благодаря переиспользуемым компонентам, мне понадобилось всего 20 минут для создания полностью функциональной формы с валидацией и обработкой ошибок.
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. Создал новое...
|