SmartMVC
Все документы
Руководство
Возможности
Аутентификация
Пример
Справочник
  • 简体中文
  • English
  • 日本語
  • Русский
GitHub
Все документы
Руководство
Возможности
Аутентификация
Пример
Справочник
  • 简体中文
  • English
  • 日本語
  • Русский
GitHub
  • Аутентификация

    • Основы аутентификации и авторизации
    • Собственная аутентификация и текущий пользователь

Собственная аутентификация и текущий пользователь

В этом разделе мы реализуем аутентификацию с Bearer-токеном, а затем разберём роли, разрешения на запрос и жизненный цикл CurrentAuth.

1. Определите пользователя приложения

public record AppUser(
        String id,
        String username,
        String displayName
) {
}

SmartMVC не требует, чтобы пользователь реализовывал специальный интерфейс. Можно использовать уже существующую сущность или DTO приложения.

2. Реализуйте AuthInterceptor

@Component
public class TokenAuthInterceptor implements AuthInterceptor<AppUser> {

    private final TokenService tokenService;

    public TokenAuthInterceptor(TokenService tokenService) {
        this.tokenService = tokenService;
    }

    @Override
    public AuthPrincipal<AppUser> authenticate(
            String token,
            HttpServletRequest request) {
        AppUser user = tokenService.findUser(token);
        if (user == null) {
            return null;
        }

        return new AuthPrincipal<>(
                user.id(),
                user,
                Set.of("admin"),
                Set.of("user:read", "GET:/api/users/**"),
                Map.of("tenantId", "tenant-001")
        );
    }
}

Результат null означает, что аутентификация не удалась. SmartMVC выбросит UnauthorizedException и вернёт HTTP 401.

Как только приложение регистрирует этот бин, стандартный PermitAllAuthInterceptor автоматически перестаёт применяться.

3. Настройте расположение учётных данных

spring:
  smart:
    mvc:
      auth:
        authorization-header: Authorization
        token-prefix: Bearer

Для запроса с заголовком:

Authorization: Bearer eyJhbGciOi...

метод authenticate получает токен уже без префикса.

Если учётные данные хранятся в Cookie, Session или другом месте, можно переопределить метод:

@Override
public String resolveToken(
        HttpServletRequest request,
        AuthConfig config) {
    Cookie[] cookies = request.getCookies();
    if (cookies == null) {
        return null;
    }
    return Arrays.stream(cookies)
            .filter(cookie -> "session_token".equals(cookie.getName()))
            .map(Cookie::getValue)
            .findFirst()
            .orElse(null);
}

AuthPrincipal<T>

AuthPrincipal, возвращаемый после успешной аутентификации, содержит:

ЗначениеНазначение
idСтабильный идентификатор пользователя; не может быть пустым
userОбъект пользователя приложения; может быть null
rolesНабор ролей
permissionsНабор именованных разрешений и разрешений на запросы
attributesДополнительные атрибуты: арендатор, способ аутентификации и другие данные

Наборы и Map защитно копируются и становятся неизменяемыми. Это не позволяет случайно изменить данные пользователя после завершения аутентификации.

Использование CurrentAuth в бизнес-коде

@Service
public class ProfileService {

    private final CurrentAuth currentAuth;

    public ProfileService(CurrentAuth currentAuth) {
        this.currentAuth = currentAuth;
    }

    public ProfileView currentProfile() {
        AppUser user = currentAuth.getUser(AppUser.class);
        return ProfileView.from(user);
    }
}

Часто используемые методы:

currentAuth.isAuthenticated();
currentAuth.getPrincipal();
currentAuth.requirePrincipal();
currentAuth.getUserId();
currentAuth.getUser(AppUser.class);
currentAuth.getRoles();
currentAuth.getPermissions();
currentAuth.hasRole("admin");
currentAuth.hasPermission("GET", "/api/users/1001");

Если текущий пользователь не установлен, requirePrincipal() выбрасывает IllegalStateException. Метод удобен в коде, который выполняется только внутри защищённого запроса.

Безопасно ли это при параллельных запросах

CurrentAuth — синглтон под управлением Spring, но пользователь не хранится в поле этого объекта. Все запросы делегируются AuthContext, основанному на ThreadLocal:

  • синхронные Servlet-запросы изолированы друг от друга;
  • перед началом запроса старый контекст очищается;
  • после завершения запроса контекст очищается снова, чтобы при повторном использовании потока из пула в нём не остались данные другого пользователя.

Контекст не переносится автоматически в @Async, самостоятельно созданные потоки, задачи пула или реактивные цепочки. При переходе между потоками явно передавайте необходимые данные: ID пользователя, роли или неизменяемый снимок AuthPrincipal.

Асинхронная обработка Spring MVC

Callable, DeferredResult, WebAsyncTask и StreamingResponseBody также переходят границу Servlet-потока запроса. Текущий перехватчик аутентификации очищает пользователя в синхронном обратном вызове afterCompletion и не обрабатывает переход afterConcurrentHandlingStarted. Поэтому описанная выше гарантия автоматической очистки действует только для синхронных Servlet-запросов: асинхронная задача не наследует CurrentAuth, а контекст исходного потока не очищается автоматически в момент начала асинхронной обработки.

При использовании асинхронного возвращаемого типа Spring MVC скопируйте минимальный неизменяемый набор данных пользователя до выхода из синхронного потока и организуйте очистку в рамках асинхронного жизненного цикла. Другой вариант — предоставить собственный перехватчик, который явно обрабатывает переход в асинхронный режим. Не полагайтесь на CurrentAuth непосредственно внутри асинхронного callback.

Выражения разрешений на запрос

Включите проверку разрешения уровня запроса:

spring.smart.mvc.auth.check-request-permission: true

Разрешение записывается в формате METHOD:/path/pattern:

ВыражениеЧто оно разрешает
GET:/api/users/**GET-запросы на этот путь и все вложенные уровни
POST:/api/users/*POST-запросы на один вложенный сегмент
*:/api/public/**Любой HTTP-метод
*:*Любой метод и любой путь

В шаблоне пути поддерживаются:

  • ** — несколько сегментов пути;
  • * — любое количество символов внутри одного сегмента;
  • ? — один символ.

HTTP-метод сопоставляется без учёта регистра.

Собственная логика авторизации

Если разрешение нужно проверять в реальном времени во внешнем сервисе или приложение использует совсем другую модель авторизации, переопределите authorize:

@Override
public boolean authorize(
        AuthPrincipal<AppUser> principal,
        Auth requirement,
        HttpServletRequest request,
        AuthConfig config) {
    return permissionService.isAllowed(
            principal.getId(),
            request.getMethod(),
            request.getRequestURI()
    );
}

При необходимости также можно переопределить bind, clear и resolveRequestPath. Обычно стоит менять только ту часть, поведение которой действительно отличается, а остальной жизненный цикл оставить стандартной реализации.

Рекомендуемая конфигурация

spring:
  smart:
    mvc:
      auth:
        enabled: true
        mode: GLOBAL
        authorization-header: Authorization
        token-prefix: Bearer
        check-request-permission: true
        exclude-paths:
          - /actuator/health

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

Изменить эту страницу на GitHub
Последнее обновление: 07.08.2026, 08:33
Назад
Основы аутентификации и авторизации
Далее
Пример входа и проверки доступа