mateclaw/mateclaw-server/src/main/java/vip/mate/config/SecurityConfig.java
matevip 1159dcdbf3 feat(a2a): add agent interoperability protocol support
Add an A2A JSON-RPC endpoint with authenticated message/send, message/stream, tasks/get, and tasks/cancel handling. Expose anonymous minimal Agent Cards while keeping the enabled-agent skills list behind existing Bearer authentication.

Bridge inbound calls into the existing agent runtime, add an in-memory task store with duplicate task rejection, JSON-RPC idempotency snapshots, terminal TTL cleanup, and SSE status/artifact event streaming with heartbeat comments.

Add the call_a2a_agent tool and peer adapter with Agent Card discovery fallback, blocking task polling through tasks/get, event-boundary SSE parsing, response caps, timeout limits, redirect refusal, and private-network SSRF protection.

Wire mateclaw.a2a configuration, document deployment settings in English and Chinese, mirror bundled docs, and cover task storage, JSON-RPC validation, card privacy, lifecycle/cancel behavior, SSE parsing, and outbound guardrails with focused tests.
2026-08-19 05:36:51 -04:00

141 lines
6.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package vip.mate.config;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.http.HttpMethod;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.DispatcherType;
import vip.mate.kbopen.auth.KbOpenApiAuthFilter;
/**
* Spring Security 配置
* <p>
* 注意BCryptPasswordEncoder 单独定义为静态内部配置,避免与 JwtAuthFilter 产生循环依赖
*
* @author MateClaw Team
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
private final KbOpenApiAuthFilter kbOpenApiAuthFilter;
/**
* SpringDoc Swagger UI / OpenAPI document paths. These serve the full REST
* surface (every endpoint plus request/response schemas), so they are gated
* explicitly instead of relying on the {@code .anyRequest().permitAll()}
* fallthrough. Whether they are public or admin-only is driven by
* {@code mateclaw.openapi.expose-ui} (see {@link #filterChain}).
*/
private static final String[] OPENAPI_PATHS = {
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs",
"/v3/api-docs/**",
"/v3/api-docs.yaml",
"/webjars/**"
};
/**
* 密码编码器独立配置(打破 SecurityConfig → JwtAuthFilter → AuthService → BCryptPasswordEncoder 循环)
*/
@Configuration
static class PasswordEncoderConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
/**
* Configure the security filter chain.
*
* @param exposeOpenApiUi when {@code true} the Swagger UI / OpenAPI document
* paths are public; when {@code false} they require a global admin
* ({@code ROLE_ADMIN}). Defaults to {@code false} (locked down) when the
* property is absent — the base {@code application.yml} enables it for
* local dev while the production database profiles keep it off.
*/
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
@Value("${mateclaw.openapi.expose-ui:false}") boolean exposeOpenApiUi) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers
// 允许同源 frame 嵌入Electron 桌面应用、H2 Console 均需要)
.frameOptions(frame -> frame.sameOrigin())
)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> {
// REQUEST dispatches are authenticated below. Async SSE error/completion
// redispatches can run after the response is committed and no longer carry
// the JWT; challenging them produces a second AccessDeniedException.
auth.dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.ERROR).permitAll();
// GET /settings/language stays anonymous (first-paint i18n). PUT
// requires login + admin (see @RequireGlobalAdmin on the controller).
auth.requestMatchers(HttpMethod.GET, "/api/v1/settings/language").permitAll()
// 公开 API 接口
.requestMatchers(
"/api/v1/auth/login",
"/api/v1/auth/sso/**",
"/api/v1/agents/*/chat/stream",
"/api/v1/chat/stream",
"/api/v1/chat/*/stop",
"/api/v1/setup/**",
"/api/v1/channels/webhook/**",
"/api/v1/channels/webchat/**",
// KB Open API: authenticated by KbOpenApiAuthFilter (API key),
// not JWT — must be permitAll so the filter is the sole gatekeeper (R1).
"/api/v1/open/kb/**",
"/api/a2a/card",
"/.well-known/agent-card.json",
"/api/v1/talk/ws",
// Desktop local-tool tunnel — the handshake interceptor
// authenticates the ?token= query param itself, so the
// upgrade request is opened to the filter chain like talk/ws.
"/api/v1/desktop/ws",
// RFC-045: tool-generated files served via unguessable UUID; entries
// expire after GeneratedFileCache.TTL (7 days) — delayed access (e.g. an
// IM-delivered link opened later) is intentional, the UUID is the guard.
"/api/v1/files/generated/**"
).permitAll();
// Swagger UI / OpenAPI document — explicit rule rather than the
// permitAll() fallthrough. Public for local dev, admin-only in
// production, driven by mateclaw.openapi.expose-ui.
if (exposeOpenApiUi) {
auth.requestMatchers(OPENAPI_PATHS).permitAll();
} else {
auth.requestMatchers(OPENAPI_PATHS).hasRole("ADMIN");
}
// 所有其他 API 接口需要认证
auth.requestMatchers("/api/**").authenticated()
// 非 API 请求前端路由、静态资源、H2 Console 等)全部放行
.anyRequest().permitAll();
})
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"Token expired or invalid\",\"data\":null}");
})
)
.addFilterBefore(kbOpenApiAuthFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}