| name | springboot-security |
| description | Java Spring Boot 서비스의 인증/권한 부여, 유효성 검사, CSRF, 비밀 정보(secrets), 헤더, 속도 제한 및 의존성 보안을 위한 Spring Security 모범 사례입니다. |
| origin | ECC |
Spring Boot 보안 검토 (Spring Boot Security Review)
인증 기능을 추가하거나, 입력을 처리하거나, 엔드포인트를 생성하거나, 비밀 정보를 다룰 때 사용하세요.
사용 시점
- 인증(JWT, OAuth2, 세션 기반)을 추가할 때
- 권한 부여(@PreAuthorize, 역할 기반 접근)를 구현할 때
- 사용자 입력 유효성 검사(Bean Validation, 커스텀 검증기)를 수행할 때
- CORS, CSRF 또는 보안 헤더를 구성할 때
- 비밀 정보(Vault, 환경 변수)를 관리할 때
- 속도 제한(rate limiting) 또는 무차별 대입 공격(brute-force) 방지 기능을 추가할 때
- 의존성의 CVE(취약점)를 스캔할 때
인증 (Authentication)
- 상태가 없는(stateless) JWT 또는 무효화 리스트가 포함된 불투명 토큰(opaque tokens)을 선호합니다.
- 세션에는
httpOnly, Secure, SameSite=Strict 쿠키를 사용합니다.
OncePerRequestFilter 또는 리소스 서버를 사용하여 토큰 유효성을 검사합니다.
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public JwtAuthFilter(JwtService jwtService) {
this.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);
}
}
권한 부여 (Authorization)
- 메서드 보안 활성화:
@EnableMethodSecurity
@PreAuthorize("hasRole('ADMIN')") 또는 @PreAuthorize("@authz.canEdit(#id)") 사용
- 기본적으로 거부(deny by default)하고, 필요한 범위(scope)만 노출합니다.
@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();
}
}
입력 유효성 검사 (Input Validation)
- 컨트롤러에서
@Valid와 함께 Bean Validation 사용
- DTO에 제약 조건 적용:
@NotBlank, @Email, @Size, 커스텀 검증기
- 렌더링 전 모든 HTML을 화이트리스트 방식으로 정제(sanitize)
@PostMapping("/users")
public User createUser(@RequestBody UserDto dto) {
return userService.create(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));
}
SQL 인젝션 방지
- Spring Data 레포지토리 또는 파라미터화된 쿼리 사용
- 네이티브 쿼리의 경우
: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);
List<User> findByEmailAndActiveTrue(String email);
비밀번호 인코딩
- 항상 BCrypt 또는 Argon2로 비밀번호를 해싱하고, 절대 평문으로 저장하지 마세요.
- 수동 해싱이 아닌
PasswordEncoder 빈을 사용하세요.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
public User register(CreateUserDto dto) {
String hashedPassword = passwordEncoder.encode(dto.password());
return userRepository.save(new User(dto.email(), hashedPassword));
}
CSRF 보호
- 브라우저 세션 앱의 경우 CSRF를 활성화 상태로 유지하고, 폼/헤더에 토큰을 포함합니다.
- Bearer 토큰을 사용하는 순수 API의 경우 CSRF를 비활성화하고 상태 없는(stateless) 인증에 의존합니다.
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
비밀 정보(Secrets) 관리
- 소스 코드에 비밀 정보를 포함하지 말고, 환경 변수나 Vault에서 로드하세요.
application.yml에 자격 증명을 직접 적지 말고 플레이스홀더를 사용하세요.
- 토큰과 DB 자격 증명을 정기적으로 교체하세요.
spring:
datasource:
password: mySecretPassword123
spring:
datasource:
password: ${DB_PASSWORD}
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)));
CORS 구성
- 컨트롤러별이 아닌 보안 필터 수준에서 CORS를 구성하세요.
- 허용 오리진(allowed origins)을 제한하고, 프로덕션에서 절대
*를 사용하지 마세요.
@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;
}
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
속도 제한 (Rate Limiting)
- 비용이 많이 드는 엔드포인트에 Bucket4j 또는 게이트웨이 수준의 제한을 적용하세요.
- 급증하는 요청에 대해 로깅 및 알림을 설정하고, 재시도 힌트와 함께 429 코드를 반환하세요.
@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\"}");
}
}
}
의존성 보안
- CI에서 OWASP Dependency Check / Snyk을 실행하세요.
- Spring Boot와 Spring Security를 지원되는 버전으로 유지하세요.
- 알려진 CVE가 발견되면 빌드를 중단하세요.
로깅 및 PII (개인 식별 정보)
- 비밀 정보, 토큰, 비밀번호 또는 전체 카드 데이터를 절대 로깅하지 마세요.
- 민감한 필드를 마스킹하고 구조화된 JSON 로깅을 사용하세요.
파일 업로드
- 크기, 콘텐츠 유형 및 확장자 유효성을 검사하세요.
- 웹 루트 외부 폴더에 저장하고 필요한 경우 스캔을 수행하세요.
릴리스 전 체크리스트
기억하세요: 기본적으로 거부하고(Deny by default), 입력을 검증하며, 최소 권한 원칙을 따르고, 설정에 의한 보안을 우선시하세요.