springboot-security
Java Spring Boot 서비스의 인증·인가, 검증, CSRF, 시크릿, 헤더, 속도 제한, 의존성 보안에 대한 Spring Security 모범 사례.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Java Spring Boot 서비스의 인증·인가, 검증, CSRF, 시크릿, 헤더, 속도 제한, 의존성 보안에 대한 Spring Security 모범 사례.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
새 Spring Boot 서비스를 api-gateway + auth-api 에코시스템에 연결한다. auth-api 클라이언트 등록 → Gateway 라우팅 추가 → 서비스에 econo-passport 연동 → 동작 확인까지 한 번에 처리. 다음 상황에서 반드시 이 스킬을 사용한다: - "새 서비스 Gateway에 연결해줘", "새 서비스 auth 연동" - "서비스 등록해줘", "Gateway 뒤에 붙여줘" - "/register-service" 직접 호출 - 새 Spring Boot 서비스가 추가되고 인증이 필요할 때 ARGUMENTS: 서비스명, 서비스 경로(선택), 업스트림 URL(선택) 예: "EEOS-BE /Users/mando/study/eeos/EEOS-BE/eeos"
이 스킬은 사용자가 "PR 생성해줘", "PR 만들어줘", "pull request 생성", "pr 올려줘", "/git-pr" 등을 요청할 때 호출된다. 현재 브랜치의 커밋을 원격으로 push하고, `.github/PULL_REQUEST_TEMPLATE.md`를 채워 GitHub Pull Request를 생성한다.
신규 기능을 개발할 때 정책 문서 → 코드 → 테스트 순서로 작성한다. ADR은 기술적 결정에만 사용하고, 기능 정책은 docs/features/ 에 작성한다. 다음 상황에서 반드시 이 스킬을 사용한다: - "기능 추가해줘", "기획부터 해봐", "설계해봐" - 새로운 API 엔드포인트 또는 도메인 규칙이 생길 때 - "/new-feature" 직접 호출 ARGUMENTS: 기능 이름 또는 요구사항 (없으면 대화에서 추출)
Architecture Decision Record(ADR)를 작성한다. 기술적 결정사항, 설계 선택, 트레이드오프를 문서화하여 나중에 "왜 이렇게 했지?"를 알 수 있게 한다. 다음 상황에서 반드시 이 스킬을 사용한다: - "ADR 써줘", "결정사항 문서화해줘", "이 결정 기록해줘" - 기술 방향 선택 후 ("A 대신 B 쓰기로 했어") - 설계 논의가 끝났을 때 - 나중에 이 결정이 왜 내려졌는지 설명이 필요할 것 같을 때 - "/adr" 직접 호출 ARGUMENTS: 결정 내용 또는 결정 번호 (없으면 대화에서 추출)
AI 에이전트의 액션 스페이스, 도구 정의, 관측(Observation) 포맷을 설계·최적화해 작업 완수율을 높일 때 사용한다.
로컬 개발, 컨테이너 보안, 네트워킹, 볼륨 전략, 멀티 서비스 오케스트레이션을 위한 Docker 및 Docker Compose 패턴.
| name | springboot-security |
| description | Java Spring Boot 서비스의 인증·인가, 검증, CSRF, 시크릿, 헤더, 속도 제한, 의존성 보안에 대한 Spring Security 모범 사례. |
| origin | ECC |
인증을 추가하거나, 입력을 처리하거나, 엔드포인트를 만들거나, 시크릿을 다룰 때 사용한다.
httpOnly, Secure, SameSite=Strict 쿠키를 사용한다OncePerRequestFilter 또는 리소스 서버로 토큰을 검증한다@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
Authentication auth = jwtService.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
}
@EnableMethodSecurity@PreAuthorize("hasRole('ADMIN')") 또는 @PreAuthorize("@authz.canEdit(#id)")를 사용한다@RestController
@RequestMapping("/api/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<UserDto> listUsers() {
return userService.findAll();
}
@PreAuthorize("@authz.isOwner(#id, authentication)")
@DeleteMapping("/users/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
@Valid로 Bean Validation을 적용한다@NotBlank, @Email, @Size, 커스텀 검증기// 나쁜 예: 검증 없음
@PostMapping("/users")
public User createUser(@RequestBody UserDto dto) {
return userService.create(dto);
}
// 좋은 예: 검증된 DTO
public record CreateUserDto(
@NotBlank @Size(max = 100) String name,
@NotBlank @Email String email,
@NotNull @Min(0) @Max(150) Integer age
) {}
@PostMapping("/users")
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(userService.create(dto));
}
:param 바인딩을 사용하고, 절대 문자열을 연결하지 않는다// 나쁜 예: 네이티브 쿼리에서 문자열 연결
@Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true)
// 좋은 예: 파라미터 바인딩 네이티브 쿼리
@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)
List<User> findByName(@Param("name") String name);
// 좋은 예: Spring Data 파생 쿼리 (자동 파라미터화)
List<User> findByEmailAndActiveTrue(String email);
PasswordEncoder 빈을 사용한다@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 비용 인자 12
}
// 서비스에서
public User register(CreateUserDto dto) {
String hashedPassword = passwordEncoder.encode(dto.password());
return userRepository.save(new User(dto.email(), hashedPassword));
}
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
application.yml에 자격 증명을 두지 않고 플레이스홀더를 사용한다# 나쁜 예: application.yml에 하드코딩
spring:
datasource:
password: mySecretPassword123
# 좋은 예: 환경 변수 플레이스홀더
spring:
datasource:
password: ${DB_PASSWORD}
# 좋은 예: Spring Cloud Vault 통합
spring:
cloud:
vault:
uri: https://vault.example.com
token: ${VAULT_TOKEN}
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'"))
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
.xssProtection(Customizer.withDefaults())
.referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));
* 사용 금지@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://app.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
// SecurityFilterChain에서:
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
// Bucket4j를 사용한 엔드포인트별 속도 제한
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
private Bucket createBucket() {
return Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1))))
.build();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createBucket());
if (bucket.tryConsume(1)) {
chain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
}
}
}
기억할 점: 기본은 거부, 입력은 검증, 최소 권한, 그리고 안전한 기본 구성을 우선한다.