Протокол Model Context Protocol (MCP) стремительно развивается, и вопросы его безопасности становятся всё актуальнее. Чтобы упростить реализацию защиты MCP-серверов в проектах на Spring AI, был запущен инкубационный проект spring-ai-community/mcp-security. В новом переводе от команды Spring АйО рассмотрим, как защитить MCP-сервер с помощью OAuth2 или API-ключей, а также как развернуть собственный MCP-совместимый Spring Authorization Server.


Протокол Model Context Protocol, или MCP, захватил внимание всего AI-сообщества. Вопросы безопасности MCP развиваются стремительными темпами, и последняя версия спецификации получает всё большую поддержку со стороны экосистемы. Чтобы удовлетворить потребности пользователей Spring, мы запустили отдельный инкубационный проект на Github: spring-ai-community/mcp-security. На этой неделе мы опубликовали первые релизы, и теперь вы можете добавить их в свои приложения, основанные на Spring AI версии 1.1.x. В этом посте мы рассмотрим:

Защита MCP-серверов с помощью OAuth2

Согласно разделу Authorization спецификации MCP, MCP-серверы, доступные по HTTP, должны быть защищены с помощью токенов доступа OAuth 2. Любой запрос к MCP-серверу должен содержать заголовок Authorization: Bearer <access_token>, где access_token — это токен, полученный от сервера авторизации (например, Okta, Github и др.) от имени пользователя. MCP-сервер также обязан явно указать, каким серверам авторизации он доверяет, чтобы MCP-клиенты могли динамически обнаружить эти серверы, зарегистрироваться в них и получить токены. Мы подробнее обсудим серверы авторизации позже, но пока будем считать, что у вас уже настроен и запущен сервер авторизации по адресу <AUTH_SERVER_URL>, и мы подключим к нему наш MCP-сервер.

Сначала добавьте необходимые зависимости в ваш проект:

Maven:

<dependencies>
    <!-- Spring AI MCP starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <!-- MCP Security -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>mcp-server-security</artifactId>
        <version>0.0.3</version>
    </dependency>
    <!-- MCP Security dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    </dependency>
</dependencies>

Gradle:

implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
implementation("org.springaicommunity:mcp-server-security:0.0.3")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")

Убедитесь, что MCP-сервер включён в файле application.properties, и укажите URL вашего сервера авторизации:

spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE
# Choose any property name you'd like
# You MAY use the usual Spring well-known "spring.security.oauth2.resourceserver.jwt.issuer-uri".
authorization.server.url=<AUTH_SERVER_URL>

Мы добавим простой инструмент MCP, который приветствует пользователя на заданном языке (например, "english", "french" и т. д.) и по имени пользователя.

@Service
public class MyToolsService {

    @McpTool(name = "greeter", description = "A tool that greets you, in the selected language")
    public String greet(
            @ToolParam(description = "The language for the greeting (example: english, french, ...)") String language
    ) {
        if (!StringUtils.hasText(language)) {
            language = "";
        }
        var authentication = SecurityContextHolder.getContext().getAuthentication();
        var name = authentication.getName();
        return switch (language.toLowerCase()) {
            case "english" -> "Hello, %s!".formatted(name);
            case "french" -> "Salut %s!".formatted(name);
            default -> ("I don't understand language \"%s\". " +
                        "So I'm just going to say Hello %s!").formatted(language, name);
        };
    }
}

В этом примере инструмент будет извлекать имя пользователя из SecurityContext и формировать персонализированное приветствие. Имя пользователя будет получено из утверждения sub в JWT-токене доступа, используемом для аутентификации запроса.

И, наконец, добавим конфигурационный класс для настройки безопасности, например McpServerSecurityConfiguration:

@Configuration
@EnableWebSecurity
class McpServerSecurityConfiguration {

    @Value("${authorization.server.url}")
    private String authServerUrl;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                // принудительно требовать аутентификацию с токеном для КАЖДОГО запроса
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                // настроить OAuth2 на сервере MCP
                .with(
                        McpServerOAuth2Configurer.mcpServerOAuth2(),
                        (mcpAuthorization) -> {
                            // ОБЯЗАТЕЛЬНО: URI сервера авторизации (issuer URI)
                            mcpAuthorization.authorizationServer(this.authServerUrl);
                            // НЕОБЯЗАТЕЛЬНО: проверять требование aud в JWT-токене.
                            mcpAuthorization.validateAudienceClaim(true);
                        }
                )
                .build();
    }
}

Запустите приложение с помощью команды ./mvnw spring-boot:run или ./gradlew bootRun. Приложение должно стартовать на порту 8080. Если вы попытаетесь обратиться к MCP-серверу по адресу http://localhost:8080/mcp, вы получите заголовок WWW-Authenticate, указывающий на URL метаданных ресурса OAuth2:

curl -XPOST  -w '%{http_code}\n%header{www-authenticate}' http://localhost:8080/mcp
#
# Will print out:
#
# 401
# Bearer resource_metadata=http://localhost:8080/.well-known/oauth-protected-resource/mcp

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

curl http://localhost:8080/.well-known/oauth-protected-resource/mcp
#
# Will print out:
#
# {
#   "resource": "http://localhost:8080/mcp",
#   "authorization_servers": [
#     "<AUTH_SERVER_URL>"
#   ],
#   "resource_name": "Spring MCP Resource Server",
#   "bearer_methods_supported": [
#     "header"
#   ]
# }

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

npx @modelcontextprotocol/inspector@0.16.7

В пользовательском интерфейсе необходимо указать URL вашего сервера, а затем нажать кнопку «Open Auth Settings»:

В настройках авторизации выберите опцию «Quick OAuth Flow».

После этого вы будете перенаправлены на сервер авторизации. После входа в систему вы вернётесь обратно в MCP Inspector, где отобразится сообщение об успешной авторизации и первые несколько символов токена доступа. С этого момента вы сможете установить соединение и, в конечном итоге, вызвать наш инструмент «greeter»:

На скриншоте выше нужно выполнить следующие шаги по порядку:

  • Перейти на вкладку Tools

  • Нажать List tools

  • Выбрать инструмент greeter

  • Заполнить аргументы и вызвать инструмент

Таким образом, вы получите свой первый сервер MCP, соответствующий спецификации и защищённый с помощью OAuth2. Существуют и альтернативные реализации, например, вариант, при котором весь функционал MCP-сервера (например, «list tools») доступен публично, за исключением непосредственного вызова инструментов. Такой подход не соответствует спецификации, но может быть уместен в некоторых специфических сценариях. Подробнее об этом можно узнать в соответствующем разделе документации по mcp-security.

Разумеется, чтобы пользователи могли войти в систему, ваш MCP-сервер должен быть подключён к серверу авторизации, соответствующему требованиям MCP, таким как динамическая регистрация клиентов. Помимо множества готовых SaaS-решений, вы также можете создать собственный сервер авторизации с помощью Spring Authorization Server.

MCP-совместимый Spring Authorization Server

Чтобы создать MCP-совместимый сервер авторизации на базе Spring, создайте новый Spring-проект с включённым модулем Spring Authorization Server, а также добавьте MCP-специфическую настройку:

Maven


<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>mcp-authorization-server</artifactId>
    <version>0.0.3</version>
</dependency>

Gradle

implementation("org.springaicommunity:mcp-authorization-server:0.0.2")

Вы можете настроить сервер авторизации стандартным способом (см. справочную документацию). Ниже приведён пример файла application.yml для регистрации клиента по умолчанию и пользователя по умолчанию:

spring:
  application:
    name: sample-authorization-server
  security:
    oauth2:
      authorizationserver:
        client:
          default-client:
            token:
              access-token-time-to-live: 1h
            registration:
              client-id: "default-client-id"
              client-secret: "{noop}default-client-secret"
              client-authentication-methods:
                - "client_secret_basic"
                - "none"
              authorization-grant-types:
                - "authorization_code"
                - "client_credentials"
              redirect-uris:
                - "http://127.0.0.1:8080/authorize/oauth2/code/authserver"
                - "http://localhost:8080/authorize/oauth2/code/authserver"
                # mcp-inspector
                - "http://localhost:6274/oauth/callback"
    user:
      # A single user, named "user"
      name: user
      password: password

server:
  port: 9000
  servlet:
    session:
      cookie:
        # Override the default cookie name (JSESSIONID).
        # This allows running multiple Spring apps on localhost, and they'll each have their own cookie.
        # Otherwise, since the cookies do not take the port into account, they are confused.
        name: MCP_AUTHORIZATION_SERVER_SESSIONID

Это лишь пример, и, скорее всего, вы захотите создать собственную конфигурацию. В приведённой настройке будет зарегистрирован один пользователь (имя пользователя: user, пароль: password). Также будет настроен один OAuth2-клиент (default-client-id / default-client-secret). После этого вы можете активировать все возможности сервера авторизации с помощью стандартного API Spring Security — через security filter chain:


@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
            // все запросы должны быть аутентифицированы
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            // включить настройки сервера авторизации
            .with(McpAuthorizationServerConfigurer.mcpAuthorizationServer(), withDefaults())
            // включить вход с использованием формы для пользователя "user"/"password"
            .formLogin(withDefaults())
            .build();
}

С этой конфигурацией ваш Spring Authorization Server будет поддерживать динамическую регистрацию клиентов OAuth 2 (Dynamic Client Registration), а также указание ресурса (Resource Indicators) для OAuth 2. Подключение вашего MCP-сервера к этому серверу авторизации будет совместимо с большинством AI-инструментов, таких как Claude Desktop, Cursor или MCP Inspector.

За пределами OAuth 2: API-ключи

Хотя спецификация MCP требует использования OAuth 2 для обеспечения безопасности, во многих средах нет необходимой инфраструктуры для реализации такого подхода. Чтобы обеспечить работу в таких условиях, многие клиенты, включая сам MCP Inspector, позволяют передавать пользовательские заголовки при выполнении запросов. Это открывает возможность использования альтернативных механизмов аутентификации, включая защиту на основе API-ключей.

Проект MCP Security поддерживает API-ключи — и далее мы покажем, как это реализуется.

Сначала добавьте зависимости в ваш проект:


<dependencies>

    <!-- Spring AI MCP starter -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
    <!-- MCP Security -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>mcp-server-security</artifactId>
        <version>0.0.3</version>
    </dependency>
    <!-- MCP Security dependencies -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

</dependencies>

Gradle:

implementation("org.springframework.ai:spring-ai-starter-mcp-server-webmvc")
implementation("org.springaicommunity:mcp-server-security:0.0.3")
implementation("org.springframework.boot:spring-boot-starter-security")

Убедитесь, что MCP-сервер включён в файле application.properties:

spring.ai.mcp.server.name=my-cool-mcp-server
# Supported protocols: STREAMABLE, STATELESS
spring.ai.mcp.server.protocol=STREAMABLE

Сущности, аутентифицируемые с помощью API-ключа — такие как пользователи или сервисные аккаунты — представлены классом ApiKeyEntity. MCP-сервер проверяет определённый заголовок на наличие API-ключа, загружает соответствующую сущность и валидирует секрет. Вы можете реализовать собственный класс сущности и собственный репозиторий для выполнения специфических проверок безопасности.

После этого вы можете настроить безопасность вашего проекта стандартным способом через Spring Security:

@Configuration
@EnableWebSecurity
class McpServerConfiguration {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
                .with(
                        McpApiKeyConfigurer.mcpServerApiKey(),
                        (apiKey) -> apiKey.apiKeyRepository(apiKeyRepository())
                )
                .build();
    }

    private ApiKeyEntityRepository<ApiKeyEntityImpl> apiKeyRepository() {
        var apiKey = ApiKeyEntityImpl.builder()
                .name("test api key")
                .id("api01")
                .secret("mycustomapikey")
                .build();

        return new InMemoryApiKeyEntityRepository<>(List.of(apiKey));
    }

}

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

X-API-key: api01.mycustomapikey

X-API-key — это имя заголовка по умолчанию для передачи API-ключей, где значение заголовка имеет формат {id}.{secret}. Секретная часть (secret) хранится на стороне сервера в виде bcrypt-хеша. Конфигуратор mcpServerApiKey() предоставляет возможность изменить имя заголовка и даже использовать специализированные API для извлечения ключа из входящих HTTP-запросов.


Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.