↑ ↓ 移动 · Enter 打开 · Esc 关闭点按一个结果

架构
skill · design 类 · opus

security-architect

设计谁可以调用什么:机制、权限、filter chain 和失败响应。

skill · designopus写入 32-seguranca.md仅限 docs/use-cases/UC-*/**
§ 它写出的文件

32-seguranca.md,逐块说明

一份真实的分文件:bank-app 中的 UC-006-anonymize-customer,为节选。把鼠标移到某一块上或点击它,可以看到 skill 在这里固定了什么。文件的各个章节也是标签:点击即可跳转。

bank-app/docs/use-cases/UC-006-anonymize-customer/32-seguranca.md

UC-006 · Security

Partial of docs/use-cases/UC-006-anonymize-customer/. Owner: security-architect. Inherits the endpoints from 30-rest.md and the domain from 10-dominio.md — doesn't redecide them. Contains no code. Form references in .claude/skills/security-architect/templates/. Rules applied, never reproduced: @.claude/rules/authorization.md, @.claude/rules/api-rest.md § Errors, @.claude/rules/transport-security.md § HSTS.
FieldValue
Mother spec00-caso-de-uso.md — Access: only callers with the DATA_PROTECTION_OFFICER role; JWT issued by the bank's identity provider
Why this partial existsAccess names a role, and the project has no SecurityFilterChain yet — first secured case, so this partial also owns the project-wide setup and every endpoint already on disk
Stackservlet (spring-boot-starter-webmvc in pom.xml, Boot 4.1.1)
Wiring packageinfrastructure.config.security (blueprint packages.map, entry infrastructure.config)
Divergences from other partialsnone. 00-caso-de-uso.md listed "caller not identified, or without the role" as one situation; here it is two statuses, 401 and 403 (§ 6)

## 1 · Access per endpoint

MethodPathoperationIdRuleEnforced inMeta-annotationNo credentialWithout authorityWith authority
DELETE/api/v1/customers/{customerId}anonymizeCustomerrole DATA_PROTECTION_OFFICERchain: request rule DELETE /api/v1/customers/* → hasRole(Roles.DATA_PROTECTION_OFFICER)—401403204
POST/api/v1/customerscreateCustomer (UC-001)publicchain: permitAll—201—201
GET/api/v1/customers/{customerId}getCustomer (UC-003)authenticated (changed, user's decision)chain: deny by default (anyRequest().authenticated())—401— (no role)200

The role check is a request rule in the chain, not method security: one endpoint, one role, matched by method and path. No @PreAuthorize in the project yet, so no MethodSecurityConfig and no method-security denial to translate — a later case that needs one adds them. A 403 is not replaced by 404: the resource's existence is not sensitive (the DELETE answers 204 for an unknown id anyway, 30-rest.md).

Public rows of the project after this case (each with its reason):

PathReason
POST /api/v1/customersregistration of a new customer — UC-001's decision, kept by the user in this run. Protected only by Idempotency-Key and its own validation
/v3/api-docs/, /swagger-ui/, /swagger-ui.htmlOpenAPI document and UI — public by the rule's default; the user kept it
/actuator/health/**, /actuator/infoliveness and readiness probes of the platform; details only for OPERATOR

Request rules, specific to general: docs permitAll → health and info permitAll → /actuator/** hasRole(OPERATOR) → POST /api/v1/customers permitAll → DELETE /api/v1/customers/* hasRole(DATA_PROTECTION_OFFICER) → anyRequest().authenticated().

## 2 · Mechanism

DecisionValueWhy
MechanismOAuth2 resource server, signed token (JWT)references/mechanism-decision-matrix.md § 2 row 3: the bank's identity provider issues signed tokens (00-caso-de-uso.md, Access)
Inherited?no — first secured caseno security starter in pom.xml, no chain on disk, no earlier 32-seguranca.md
ExemplarsSecurityConfig.java.example + JwtResourceServerConfig.java.example—

Rejected, one line each: opaque token — the provider issues JWTs, and introspection adds a call per request. Users owned by the application — the bank's provider owns the accounts. API key — no machine caller in this case.

## 3 · Authorities

FactValueSource
Roles in the tokenflat top-level roles array → prefix ROLE_asked in this run
Groups in the tokennot usedno Access names a group
Scopesscope → prefix SCOPE_ (framework default, kept)—
Audience expectedbank-appasked in this run
Principal namesubdecided — the subject is the stable id; no actor travels in this case's command
Role catalogRoles.DATA_PROTECTION_OFFICER, Roles.OPERATOR — NEW, in infrastructure.config.securityfirst roles of the project

## 4 · Project-wide setup

ComponentStateExemplarDecision
SecurityConfig + SecurityAccessPropertiesNEWSecurityConfig.java.examplestateless; CSRF off (bearer token, no cookie authenticates); default headers kept — HSTS included; request rules of § 1; deny by default
JwtResourceServerConfig + TokenAuthoritiesProperties + ClaimPathAuthoritiesConverterNEWJwtResourceServerConfig.java.exampleissuer and audience validated by Boot's issuer-uri + audiences; converter reads roles → ROLE_, scope → SCOPE_; no JwtDecoder bean of our own
ProblemDetailAuthenticationEntryPoint, ProblemDetailAccessDeniedHandlerNEWProblemDetailSecurityHandlers.java.example401 with WWW-Authenticate: Bearer, 403, Problem Details with traceId
SecurityExceptionHandlernot neededSecurityExceptionHandler.java.exampleno method security → no AccessDeniedException reaches MVC; added by the first case with @PreAuthorize
MethodSecurityConfignot neededMethodSecurityAnnotations.java.examplesame reason
RolesNEWMethodSecurityAnnotations.java.example (constants only)DATA_PROTECTION_OFFICER, OPERATOR
SecurityAuditListenerNEWSecurityAuditListener.java.exampleWARN line + security.authentication.failures{reason}, security.authorization.denied{kind}; never the token
OpenApiSecurityConfigNEWOpenApiSecurityConfig.java.examplebearer scheme, required globally; CreateCustomerOpenApiDocs overrides with an empty requirement (public)
HstsHeaderFilterREUSE — becomes inert—@ConditionalOnMissingClass(SecurityFilterChain) drops it when the starter arrives; Spring Security's HstsHeaderWriter is the single writer, same value max-age=31536000 ; includeSubDomains, secure requests only (@.claude/rules/transport-security.md § HSTS). The chain must not call headers().disable() nor httpStrictTransportSecurity().disable()
CORSNEWSecurityConfig.java.exampleorigins from app.security.cors.allowed-origins (CORS_ALLOWED_ORIGINS), empty → no mapping; methods GET, POST, DELETE; headers Authorization, Content-Type, Idempotency-Key; no credentials (bearer, not cookies)
ActuatorCHANGE—/actuator/metrics and health details → OPERATOR (@.claude/rules/authorization.md § Default access; user confirmed); exposure list unchanged (health,info,metrics)

## 5 · Ownership

none — Access is a role, not the owner. No actor travels in AnonymizeCustomerCommand.

## 6 · Failure responses

SituationStatusHeaderBody
no token, malformed, expired, wrong issuer or audience — any protected path401WWW-Authenticate: BearerProblem Details, no reason in detail
token without ROLE_DATA_PROTECTION_OFFICER on anonymizeCustomer403—Problem Details
token without ROLE_OPERATOR on /actuator/metrics or other non-public actuator403—Problem Details

No errorCode on 401/403: they are not domain errors. The 401 and 403 rows of 30-rest.md § 3 are these.

## 7 · Schema requirements

none — the identity provider holds the credentials.

## 8 · Declared dependencies

DependencyVersionWhy
org.springframework.boot:spring-boot-starter-security(managed by the Boot parent)filter chain
org.springframework.boot:spring-boot-starter-security-oauth2-resource-server(managed by the Boot parent)JWT decoding and validation — the Boot 4 name (Initializr id oauth2-resource-server)

The test support (spring-boot-starter-security-test, spring-boot-starter-security-oauth2-resource-server-test) is declared by 40-testes.md § 5.

## 9 · Configuration

PropertyValueSecret?
spring.security.oauth2.resourceserver.jwt.issuer-uri${OAUTH2_ISSUER_URI:http://localhost:8180/realms/bank}no — the default points at the local Keycloak deferred in § 11
spring.security.oauth2.resourceserver.jwt.audiences${OAUTH2_AUDIENCE:bank-app}no
app.security.api-docs-publictrueno
app.security.actuator-roleOPERATORno
app.security.cors.allowed-origins${CORS_ALLOWED_ORIGINS:} — comma-separated, empty by defaultno
app.security.token.principal-claimsubno
app.security.token.roles-claimsrolesno
app.security.token.scopes-claimsscopeno
management.endpoint.health.rolesOPERATOR (show-details: when-authorized already set)no

One spring: root and one app: root in application.yml — merged, never a second root. Shape in templates/application-security.yml.example. OAUTH2_ISSUER_URI, OAUTH2_AUDIENCE and CORS_ALLOWED_ORIGINS go into .env.example with their defaults. With issuer-uri, Boot resolves the provider's keys lazily: the application starts without a reachable provider, and the first protected call answers 401 until one exists. The test profile keeps the chain on and uses the test support's jwt() request post-processor — never a live provider.

## 10 · Contract test cases

EndpointCaseExpected
anonymizeCustomerno token401, WWW-Authenticate present, use case never called
anonymizeCustomertoken with ROLE_OPERATOR only403, use case never called
anonymizeCustomertoken with ROLE_DATA_PROTECTION_OFFICER204
createCustomer (UC-001, impact)no token201 — public proven public
getCustomer (UC-003, impact)no token · any token401 · 200
/actuator/healthno token200
/v3/api-docsno token200
/actuator/metricsno token · token without ROLE_OPERATOR · token with it401 · 403 · 200
CORS preflight OPTIONS /api/v1/customers/{id}Origin listed · Origin not listedallowed headers present · 403
HSTSsecure request (X-Forwarded-Proto: https from a trusted proxy)Strict-Transport-Security: max-age=31536000 ; includeSubDomains, exactly one header
ClaimPathAuthoritiesConvertertoken with roles: ["DATA_PROTECTION_OFFICER"]authority ROLE_DATA_PROTECTION_OFFICER; no roles claim → no role authority

## 11 · Deferred and requirements to other partials

DecidedMissingNormIntended owner
the local issuer default is http://localhost:8180/realms/banka Keycloak service in docker-compose.yml with a bank realm, audience bank-app, a flat roles claim mapper, and test users holding DATA_PROTECTION_OFFICER and OPERATOR — until it exists, a local ./mvnw spring-boot:run answers 401 on every protected call@.claude/rules/authorization.md § Mechanism (a provider the tests never call, a local one for the developer)backlog — user's decision ("local Keycloak later"); consolidation assigns the BL-NN

Requirements to other partials: none — no actor, no credential store.

## Design patterns

none — one claims source per kind of authority, no growing branch over authentication types.

## Impact on approved use cases

CaseEndpointBeforeAfterDecision
UC-001-create-customerPOST /api/v1/customerspublic (no chain existed)public — permitAll with its reasonkept public — asked in this run
UC-001-create-customerCreateCustomerOpenApiDocsno security requirementempty @SecurityRequirements — overrides the global bearer requirementthe document must say the operation is public
UC-003-get-customerGET /api/v1/customers/{customerId}public — accepted risk: anyone with an id read personal dataauthenticated (any valid token, no role)protected from now on — asked in this run. UC-003's accepted risk narrows to any authenticated caller of the bank's provider
UC-003-get-customerGetCustomerOpenApiDocsno 401gains a 401 @ApiResponsethe operation can now answer it
UC-001, UC-003CustomerControllerTestran with no filter chainimports SecurityConfig and JwtResourceServerConfig; getCustomer cases send a jwt(); createCustomer cases send none; new cases of § 10listed for test-architect
allActuator /actuator/metricspublic (no chain existed)OPERATOR only@.claude/rules/authorization.md § Default access, user confirmed

## Implementation order

  1. The dependencies of § 8
  2. Roles, ProblemDetailAuthenticationEntryPoint, ProblemDetailAccessDeniedHandler
  3. JwtResourceServerConfig (+ properties, converter), then SecurityConfig (+ properties), SecurityAuditListener, OpenApiSecurityConfig
  4. OpenAPI changes on CreateCustomerOpenApiDocs and GetCustomerOpenApiDocs — after the REST block
  5. Properties in application.yml, variables in .env.example
  6. Tests — 40-testes.md
在 GitHub 上打开完整文件 →
§ 行为

它读什么、写什么、拒绝什么

skill 自己的契约,取自 SKILL.md,保留原文。

Class

design — the territory is skill_classes.design in @.claude/schemas/extensions.json, and ArchHook.java guard enforces it. Writes inside the use case folder and nothing else; a SecurityConfig written under src/ is refused with exit 2.

Reads

docs/use-cases/UC-NNN-<slug>/00-use-case.md, 10-domain.md, and 30-rest.md or 31-graphql.md (mandatory — stops without the one the trigger names), earlier cases' 32-security.md, pom.xml (or the Gradle build), src/main/resources/application*.yml, the security wiring on disk, the active blueprint's packages.map, @.claude/rules/authorization.md, @.claude/rules/api-rest.md, @.claude/rules/api-graphql.md (§ Access, when the trigger names GraphQL), @.claude/rules/architecture-ddd.md, @.claude/rules/logging.md, @.claude/rules/observability.md, and references/.

Writes

docs/use-cases/UC-NNN-<slug>/32-security.md. Nothing else.

Owns

the project's authentication mechanism, the claims-to-authorities mapping and the role and group catalog, each endpoint's access rule and where it is enforced, the permitAll list and its reasons, the project-wide filter chain, the 401 and 403 responses, CORS, and the OpenAPI and Actuator exposure.

Does not decide

the use case boundary or its Access fact (00-use-case.md), the actor field, the owner field or the ownership invariant (10-domain.md), endpoints and domain error rows (30-rest.md), tables and columns (20-persistence.md), or the test shape (40-tests.md). Does not write Java, pom.xml or application.yml — the executor does, from this partial. Doesn't touch .claude/rules/**. Does not design reactive (WebFlux) security.

来源:SKILL.md § Contract(nerviz e4cae27)。

§ guard 与 hook

guard 如何处理它

design 是这个 skill 的类。ArchHook 在 skill 被调用时开启一个阶段,并按该类的领地检查每次写入。右侧文字是 hook 自己的输出。

design 类的 write_allow

  • ✓docs/use-cases/UC-*/**
  • ✓docs/use-cases/BACKLOG.md
  • src/**:迁移和类归执行器
  • docker-compose.yml:另一个类的文件
  • 已批准的用例:其 spec 已冻结

拒绝以退出码 2 结束,消息回到模型。

PreToolUse  Skill(security-architect)
  audit call                       exit 0
  guard call                       exit 0
# a skill of another class replaces the phase
# write_allow: docs/use-cases/UC-*/**, docs/use-cases/BACKLOG.md
§ 编排

它在轨道上的位置

每张卡片是 /new-feature 的一步;箭头带着一步交给下一步的分文件。虚线的步骤仅在用例需要时运行。

/new-feature-implement 之后谁读这份分文件

批准之后,/new-feature-implement 让执行器构建代码。spec 按引用整合,与分文件不一致时以 spec 为准;分文件按章节读取,用于细节。

32-seguranca.mddocs/use-cases/UC-NNN-…/
按章节读取,用于细节
java-spring-boot-developeragent · executor
写出代码
src/**把一个用例设计成规格,不写代码
← 返回链路