backend-claude-code 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +42 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +35 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +32 -0
- package/.mcp.json +19 -0
- package/CLAUDE.md +126 -0
- package/README.md +142 -0
- package/agents/code-reviewer.md +84 -0
- package/agents/database-reviewer.md +91 -0
- package/agents/java-build-resolver.md +127 -0
- package/agents/java-performance-reviewer.md +262 -0
- package/agents/planner.md +99 -0
- package/agents/security-reviewer.md +119 -0
- package/agents/tdd-guide.md +189 -0
- package/bin/cli.js +144 -0
- package/commands/db-migrate.md +134 -0
- package/commands/dev-build.md +72 -0
- package/commands/dev-coverage.md +73 -0
- package/commands/dev-fix.md +75 -0
- package/commands/dev-plan.md +501 -0
- package/commands/dev-review.md +144 -0
- package/commands/dev-run.md +385 -0
- package/commands/dev-test.md +89 -0
- package/commands/dev-verify.md +95 -0
- package/commands/dev.md +45 -0
- package/commands/git-commit.md +112 -0
- package/commands/git-issue.md +74 -0
- package/commands/git-pr.md +184 -0
- package/commands/git-push.md +28 -0
- package/package.json +24 -0
- package/rules/architecture.md +33 -0
- package/rules/coding-style.md +113 -0
- package/rules/controller-patterns.md +63 -0
- package/rules/dto-patterns.md +76 -0
- package/rules/entity-patterns.md +70 -0
- package/rules/error-handling.md +56 -0
- package/rules/hooks.md +73 -0
- package/rules/repository-patterns.md +75 -0
- package/rules/security.md +101 -0
- package/rules/service-patterns.md +70 -0
- package/rules/testing.md +174 -0
- package/skills/api-design/SKILL.md +523 -0
- package/skills/architecture-decision-records/SKILL.md +179 -0
- package/skills/database-migrations/SKILL.md +429 -0
- package/skills/hexagonal-architecture/SKILL.md +276 -0
- package/skills/java-coding-standards/SKILL.md +236 -0
- package/skills/jpa-patterns/SKILL.md +218 -0
- package/skills/postgres-patterns/SKILL.md +147 -0
- package/skills/springboot-patterns/SKILL.md +255 -0
- package/skills/springboot-security/SKILL.md +241 -0
- package/skills/springboot-tdd/SKILL.md +236 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: springboot-patterns
|
|
3
|
+
description: Spring Boot architecture patterns, REST API design, layered services, data access, caching, async processing, and logging. Use for Java Spring Boot backend work.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Spring Boot Development Patterns
|
|
8
|
+
|
|
9
|
+
Spring Boot 아키텍처 및 API 패턴 — 확장 가능한 프로덕션 등급 서비스.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- Spring MVC 또는 WebFlux로 REST API 구축
|
|
14
|
+
- controller → service → repository 레이어 구조화
|
|
15
|
+
- Spring Data JPA, 캐싱, 비동기 처리 설정
|
|
16
|
+
- 검증, 예외 처리, 페이지네이션 추가
|
|
17
|
+
- dev/staging/production 프로파일 설정
|
|
18
|
+
- Spring Events 또는 Kafka를 사용한 이벤트 기반 패턴 구현
|
|
19
|
+
|
|
20
|
+
## REST API Structure
|
|
21
|
+
|
|
22
|
+
```java
|
|
23
|
+
@RestController
|
|
24
|
+
@RequestMapping("/api/orders")
|
|
25
|
+
@Validated
|
|
26
|
+
class OrderController {
|
|
27
|
+
private final OrderService orderService;
|
|
28
|
+
|
|
29
|
+
OrderController(OrderService orderService) {
|
|
30
|
+
this.orderService = orderService;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@GetMapping
|
|
34
|
+
ResponseEntity<Page<OrderResponse>> list(
|
|
35
|
+
@RequestParam(defaultValue = "0") int page,
|
|
36
|
+
@RequestParam(defaultValue = "20") int size) {
|
|
37
|
+
return ResponseEntity.ok(
|
|
38
|
+
orderService.list(PageRequest.of(page, size)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@PostMapping
|
|
42
|
+
ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
|
|
43
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
44
|
+
.body(orderService.create(request));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@GetMapping("/{id}")
|
|
48
|
+
ResponseEntity<OrderResponse> findById(@PathVariable Long id) {
|
|
49
|
+
return ResponseEntity.ok(orderService.findById(id));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@DeleteMapping("/{id}")
|
|
53
|
+
ResponseEntity<Void> delete(@PathVariable Long id) {
|
|
54
|
+
orderService.delete(id);
|
|
55
|
+
return ResponseEntity.noContent().build();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Repository Pattern (Spring Data JPA)
|
|
61
|
+
|
|
62
|
+
```java
|
|
63
|
+
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
|
|
64
|
+
@Query("select o from OrderEntity o where o.status = :status order by o.createdAt desc")
|
|
65
|
+
Page<OrderEntity> findByStatus(@Param("status") OrderStatus status, Pageable pageable);
|
|
66
|
+
|
|
67
|
+
Optional<OrderEntity> findByOrderNumber(String orderNumber);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Service Layer with Transactions
|
|
72
|
+
|
|
73
|
+
```java
|
|
74
|
+
@Service
|
|
75
|
+
public class OrderService {
|
|
76
|
+
private final OrderRepository repo;
|
|
77
|
+
|
|
78
|
+
public OrderService(OrderRepository repo) {
|
|
79
|
+
this.repo = repo;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Transactional
|
|
83
|
+
public OrderResponse create(CreateOrderRequest request) {
|
|
84
|
+
OrderEntity entity = OrderEntity.from(request);
|
|
85
|
+
OrderEntity saved = repo.save(entity);
|
|
86
|
+
return OrderResponse.from(saved);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Transactional(readOnly = true)
|
|
90
|
+
public OrderResponse findById(Long id) {
|
|
91
|
+
return repo.findById(id)
|
|
92
|
+
.map(OrderResponse::from)
|
|
93
|
+
.orElseThrow(() -> new OrderNotFoundException(id));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Transactional(readOnly = true)
|
|
97
|
+
public Page<OrderResponse> list(Pageable pageable) {
|
|
98
|
+
return repo.findAll(pageable).map(OrderResponse::from);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Transactional
|
|
102
|
+
public void delete(Long id) {
|
|
103
|
+
if (!repo.existsById(id)) {
|
|
104
|
+
throw new OrderNotFoundException(id);
|
|
105
|
+
}
|
|
106
|
+
repo.deleteById(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## DTOs and Validation
|
|
112
|
+
|
|
113
|
+
```java
|
|
114
|
+
public record CreateOrderRequest(
|
|
115
|
+
@NotBlank @Size(max = 100) String customerName,
|
|
116
|
+
@NotNull @Positive BigDecimal amount,
|
|
117
|
+
@NotNull @FutureOrPresent LocalDate deliveryDate) {}
|
|
118
|
+
|
|
119
|
+
public record OrderResponse(Long id, String customerName, BigDecimal amount, OrderStatus status) {
|
|
120
|
+
static OrderResponse from(OrderEntity entity) {
|
|
121
|
+
return new OrderResponse(
|
|
122
|
+
entity.getId(), entity.getCustomerName(),
|
|
123
|
+
entity.getAmount(), entity.getStatus());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Exception Handling
|
|
129
|
+
|
|
130
|
+
```java
|
|
131
|
+
@RestControllerAdvice
|
|
132
|
+
class GlobalExceptionHandler {
|
|
133
|
+
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
134
|
+
|
|
135
|
+
@ExceptionHandler(EntityNotFoundException.class)
|
|
136
|
+
ResponseEntity<ApiResponse<Void>> handleNotFound(EntityNotFoundException ex) {
|
|
137
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
138
|
+
.body(ApiResponse.error(ex.getMessage()));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
142
|
+
ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException ex) {
|
|
143
|
+
String message = ex.getBindingResult().getFieldErrors().stream()
|
|
144
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
145
|
+
.collect(Collectors.joining(", "));
|
|
146
|
+
return ResponseEntity.badRequest().body(ApiResponse.error(message));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@ExceptionHandler(Exception.class)
|
|
150
|
+
ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
|
|
151
|
+
log.error("Unexpected error", ex);
|
|
152
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
153
|
+
.body(ApiResponse.error("Internal server error"));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Caching
|
|
159
|
+
|
|
160
|
+
`@EnableCaching` 설정 클래스에 추가 필요.
|
|
161
|
+
|
|
162
|
+
```java
|
|
163
|
+
@Service
|
|
164
|
+
public class OrderCacheService {
|
|
165
|
+
private final OrderRepository repo;
|
|
166
|
+
|
|
167
|
+
public OrderCacheService(OrderRepository repo) { this.repo = repo; }
|
|
168
|
+
|
|
169
|
+
@Cacheable(value = "order", key = "#id")
|
|
170
|
+
public OrderResponse getById(Long id) {
|
|
171
|
+
return repo.findById(id)
|
|
172
|
+
.map(OrderResponse::from)
|
|
173
|
+
.orElseThrow(() -> new OrderNotFoundException(id));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@CacheEvict(value = "order", key = "#id")
|
|
177
|
+
public void evict(Long id) {}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Async Processing
|
|
182
|
+
|
|
183
|
+
`@EnableAsync` 설정 클래스에 추가 필요.
|
|
184
|
+
|
|
185
|
+
```java
|
|
186
|
+
@Service
|
|
187
|
+
public class NotificationService {
|
|
188
|
+
@Async
|
|
189
|
+
public CompletableFuture<Void> sendAsync(Notification notification) {
|
|
190
|
+
// 이메일/SMS 발송
|
|
191
|
+
return CompletableFuture.completedFuture(null);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Logging (SLF4J)
|
|
197
|
+
|
|
198
|
+
```java
|
|
199
|
+
@Service
|
|
200
|
+
public class OrderService {
|
|
201
|
+
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
|
|
202
|
+
|
|
203
|
+
public OrderResponse create(CreateOrderRequest request) {
|
|
204
|
+
log.info("create_order customerName={}", request.customerName());
|
|
205
|
+
try {
|
|
206
|
+
// 로직
|
|
207
|
+
log.info("order_created id={}", saved.getId());
|
|
208
|
+
return OrderResponse.from(saved);
|
|
209
|
+
} catch (Exception ex) {
|
|
210
|
+
log.error("create_order_failed customerName={}", request.customerName(), ex);
|
|
211
|
+
throw ex;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Request Logging Filter
|
|
218
|
+
|
|
219
|
+
```java
|
|
220
|
+
@Component
|
|
221
|
+
public class RequestLoggingFilter extends OncePerRequestFilter {
|
|
222
|
+
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
|
|
223
|
+
|
|
224
|
+
@Override
|
|
225
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
226
|
+
FilterChain filterChain) throws ServletException, IOException {
|
|
227
|
+
long start = System.currentTimeMillis();
|
|
228
|
+
try {
|
|
229
|
+
filterChain.doFilter(request, response);
|
|
230
|
+
} finally {
|
|
231
|
+
long duration = System.currentTimeMillis() - start;
|
|
232
|
+
log.info("req method={} uri={} status={} durationMs={}",
|
|
233
|
+
request.getMethod(), request.getRequestURI(),
|
|
234
|
+
response.getStatus(), duration);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Pagination and Sorting
|
|
241
|
+
|
|
242
|
+
```java
|
|
243
|
+
PageRequest page = PageRequest.of(pageNumber, pageSize, Sort.by("createdAt").descending());
|
|
244
|
+
Page<OrderEntity> results = orderRepository.findAll(page);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Production Defaults
|
|
248
|
+
|
|
249
|
+
- 생성자 주입 선호, 필드 주입 지양
|
|
250
|
+
- Spring Boot 3+: `spring.mvc.problemdetails.enabled=true` (RFC 7807)
|
|
251
|
+
- HikariCP 풀 크기 설정, 타임아웃 구성
|
|
252
|
+
- 조회에 `@Transactional(readOnly = true)` 사용
|
|
253
|
+
- `@NonNull`과 `Optional`로 null 안전성 강제
|
|
254
|
+
|
|
255
|
+
**Remember**: 컨트롤러는 얇게, 서비스는 집중되게, 레포지토리는 단순하게, 오류는 중앙에서 처리.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: springboot-security
|
|
3
|
+
description: Spring Security best practices for authn/authz, validation, CSRF, secrets, headers, rate limiting, and dependency security in Java Spring Boot services.
|
|
4
|
+
origin: ECC
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Spring Boot Security Review
|
|
8
|
+
|
|
9
|
+
인증, 입력 처리, 엔드포인트 생성, 시크릿 처리 시 사용.
|
|
10
|
+
|
|
11
|
+
## When to Activate
|
|
12
|
+
|
|
13
|
+
- 인증 추가 (JWT, OAuth2, 세션 기반)
|
|
14
|
+
- 인가 구현 (@PreAuthorize, 역할 기반 접근)
|
|
15
|
+
- 사용자 입력 검증 (Bean Validation, 커스텀 검증기)
|
|
16
|
+
- CORS, CSRF, 보안 헤더 설정
|
|
17
|
+
- 시크릿 관리 (Vault, 환경 변수)
|
|
18
|
+
- 속도 제한 또는 무차별 대입 보호 추가
|
|
19
|
+
- 의존성 CVE 스캔
|
|
20
|
+
|
|
21
|
+
## Authentication
|
|
22
|
+
|
|
23
|
+
- 상태 없는 JWT 또는 취소 목록이 있는 불투명 토큰 선호
|
|
24
|
+
- 세션에는 `httpOnly`, `Secure`, `SameSite=Strict` 쿠키 사용
|
|
25
|
+
- `OncePerRequestFilter` 또는 리소스 서버로 토큰 검증
|
|
26
|
+
|
|
27
|
+
```java
|
|
28
|
+
@Component
|
|
29
|
+
public class JwtAuthFilter extends OncePerRequestFilter {
|
|
30
|
+
private final JwtService jwtService;
|
|
31
|
+
|
|
32
|
+
public JwtAuthFilter(JwtService jwtService) {
|
|
33
|
+
this.jwtService = jwtService;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Override
|
|
37
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
38
|
+
FilterChain chain) throws ServletException, IOException {
|
|
39
|
+
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
|
40
|
+
if (header != null && header.startsWith("Bearer ")) {
|
|
41
|
+
String token = header.substring(7);
|
|
42
|
+
Authentication auth = jwtService.authenticate(token);
|
|
43
|
+
SecurityContextHolder.getContext().setAuthentication(auth);
|
|
44
|
+
}
|
|
45
|
+
chain.doFilter(request, response);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Authorization
|
|
51
|
+
|
|
52
|
+
- 메서드 보안 활성화: `@EnableMethodSecurity`
|
|
53
|
+
- `@PreAuthorize("hasRole('ADMIN')")` 또는 `@PreAuthorize("@authz.canEdit(#id)")` 사용
|
|
54
|
+
- 기본 거부; 필요한 범위만 노출
|
|
55
|
+
|
|
56
|
+
```java
|
|
57
|
+
@RestController
|
|
58
|
+
@RequestMapping("/api/admin")
|
|
59
|
+
public class AdminController {
|
|
60
|
+
|
|
61
|
+
@PreAuthorize("hasRole('ADMIN')")
|
|
62
|
+
@GetMapping("/users")
|
|
63
|
+
public Page<UserDto> listUsers(Pageable pageable) {
|
|
64
|
+
return userService.findAll(pageable);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@PreAuthorize("@authz.isOwner(#id, authentication)")
|
|
68
|
+
@DeleteMapping("/users/{id}")
|
|
69
|
+
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
|
|
70
|
+
userService.delete(id);
|
|
71
|
+
return ResponseEntity.noContent().build();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Input Validation
|
|
77
|
+
|
|
78
|
+
```java
|
|
79
|
+
// BAD: 검증 없음
|
|
80
|
+
@PostMapping("/users")
|
|
81
|
+
public User createUser(@RequestBody UserDto dto) {
|
|
82
|
+
return userService.create(dto);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// GOOD: 검증된 DTO
|
|
86
|
+
public record CreateUserDto(
|
|
87
|
+
@NotBlank @Size(max = 100) String name,
|
|
88
|
+
@NotBlank @Email String email,
|
|
89
|
+
@NotNull @Min(0) @Max(150) Integer age
|
|
90
|
+
) {}
|
|
91
|
+
|
|
92
|
+
@PostMapping("/users")
|
|
93
|
+
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) {
|
|
94
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
95
|
+
.body(userService.create(dto));
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## SQL Injection Prevention
|
|
100
|
+
|
|
101
|
+
```java
|
|
102
|
+
// BAD: 문자열 연결
|
|
103
|
+
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
|
|
104
|
+
|
|
105
|
+
// GOOD: 파라미터화된 네이티브 쿼리
|
|
106
|
+
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
|
|
107
|
+
List<User> findByName(@Param("name") String name);
|
|
108
|
+
|
|
109
|
+
// GOOD: Spring Data 파생 쿼리 (자동 파라미터화)
|
|
110
|
+
List<User> findByEmailAndActiveTrue(String email);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Password Encoding
|
|
114
|
+
|
|
115
|
+
```java
|
|
116
|
+
@Bean
|
|
117
|
+
public PasswordEncoder passwordEncoder() {
|
|
118
|
+
return new BCryptPasswordEncoder(12); // cost factor 12
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public User register(CreateUserDto dto) {
|
|
122
|
+
String hashedPassword = passwordEncoder.encode(dto.password());
|
|
123
|
+
return userRepository.save(new User(dto.email(), hashedPassword));
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## CSRF Protection
|
|
128
|
+
|
|
129
|
+
```java
|
|
130
|
+
// 순수 JWT API — stateless 인증
|
|
131
|
+
http
|
|
132
|
+
.csrf(csrf -> csrf.disable())
|
|
133
|
+
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
|
134
|
+
|
|
135
|
+
// 세션 기반 브라우저 앱 — CSRF 활성화 유지
|
|
136
|
+
http.csrf(Customizer.withDefaults());
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Secrets Management
|
|
140
|
+
|
|
141
|
+
```yaml
|
|
142
|
+
# BAD: application.yml에 하드코딩
|
|
143
|
+
spring:
|
|
144
|
+
datasource:
|
|
145
|
+
password: mySecretPassword123
|
|
146
|
+
|
|
147
|
+
# GOOD: 환경 변수 플레이스홀더
|
|
148
|
+
spring:
|
|
149
|
+
datasource:
|
|
150
|
+
password: ${DB_PASSWORD}
|
|
151
|
+
|
|
152
|
+
# GOOD: Spring Cloud Vault
|
|
153
|
+
spring:
|
|
154
|
+
cloud:
|
|
155
|
+
vault:
|
|
156
|
+
uri: https://vault.example.com
|
|
157
|
+
token: ${VAULT_TOKEN}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Security Headers
|
|
161
|
+
|
|
162
|
+
```java
|
|
163
|
+
http
|
|
164
|
+
.headers(headers -> headers
|
|
165
|
+
.contentSecurityPolicy(csp -> csp
|
|
166
|
+
.policyDirectives("default-src 'self'; script-src 'self'"))
|
|
167
|
+
.frameOptions(HeadersConfigurer.FrameOptionsConfig::deny)
|
|
168
|
+
.xssProtection(Customizer.withDefaults())
|
|
169
|
+
.referrerPolicy(rp -> rp
|
|
170
|
+
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)));
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## CORS Configuration
|
|
174
|
+
|
|
175
|
+
```java
|
|
176
|
+
@Bean
|
|
177
|
+
public CorsConfigurationSource corsConfigurationSource() {
|
|
178
|
+
CorsConfiguration config = new CorsConfiguration();
|
|
179
|
+
config.setAllowedOrigins(List.of("https://app.example.com")); // 프로덕션에서 * 금지
|
|
180
|
+
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
|
|
181
|
+
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
|
182
|
+
config.setAllowCredentials(true);
|
|
183
|
+
config.setMaxAge(3600L);
|
|
184
|
+
|
|
185
|
+
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
186
|
+
source.registerCorsConfiguration("/api/**", config);
|
|
187
|
+
return source;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Rate Limiting (Bucket4j)
|
|
194
|
+
|
|
195
|
+
```java
|
|
196
|
+
@Component
|
|
197
|
+
public class RateLimitFilter extends OncePerRequestFilter {
|
|
198
|
+
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
|
|
199
|
+
|
|
200
|
+
private Bucket createBucket() {
|
|
201
|
+
return Bucket.builder()
|
|
202
|
+
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
|
|
203
|
+
.build();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Override
|
|
207
|
+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
|
208
|
+
FilterChain chain) throws ServletException, IOException {
|
|
209
|
+
String clientIp = request.getRemoteAddr(); // ForwardedHeaderFilter 설정 시 정확한 IP 반환
|
|
210
|
+
Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createBucket());
|
|
211
|
+
|
|
212
|
+
if (bucket.tryConsume(1)) {
|
|
213
|
+
chain.doFilter(request, response);
|
|
214
|
+
} else {
|
|
215
|
+
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
|
216
|
+
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Dependency Security
|
|
223
|
+
|
|
224
|
+
- CI에서 OWASP Dependency Check / Snyk 실행
|
|
225
|
+
- Spring Boot 및 Spring Security 지원 버전 유지
|
|
226
|
+
- 알려진 CVE 발견 시 빌드 실패
|
|
227
|
+
|
|
228
|
+
## Release Checklist
|
|
229
|
+
|
|
230
|
+
- [ ] 인증 토큰 검증 및 만료 확인
|
|
231
|
+
- [ ] 모든 민감한 경로에 인가 가드
|
|
232
|
+
- [ ] 모든 입력 검증 및 새니타이즈
|
|
233
|
+
- [ ] SQL 문자열 연결 없음
|
|
234
|
+
- [ ] 앱 타입에 맞는 CSRF 설정
|
|
235
|
+
- [ ] 시크릿 외부화, 커밋 없음
|
|
236
|
+
- [ ] 보안 헤더 설정
|
|
237
|
+
- [ ] API에 속도 제한
|
|
238
|
+
- [ ] 의존성 스캔 완료
|
|
239
|
+
- [ ] 로그에 민감 데이터 없음
|
|
240
|
+
|
|
241
|
+
**Remember**: 기본 거부, 입력 검증, 최소 권한, 설정으로 보안 우선.
|