はじめに
この記事では、基本的な認証と認可を理解した後、運用環境でアプリケーションを保護するためのメソッド レベルのセキュリティ、SPA の CORS 構成、OWASP トップ 10 に基づくセキュリティのベスト プラクティスについて詳しく説明します。
1. メソッドレベルのセキュリティ
1.1 メソッドセキュリティを有効にする
@Configuration
@EnableMethodSecurity // Spring Boot 4.x
public class MethodSecurityConfig { }
1.2 @PreAuthorize — 実行前にチェックする
@Service
public class ArticleService {
// Chỉ ADMIN mới gọi được
@PreAuthorize("hasRole('ADMIN')")
public void deleteArticle(Long id) { }
// ADMIN hoặc EDITOR
@PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')")
public ArticleResponse updateArticle(Long id, UpdateArticleRequest request) { }
// Kiểm tra authority cụ thể
@PreAuthorize("hasAuthority('WRITE_ARTICLES')")
public ArticleResponse createArticle(CreateArticleRequest request) { }
// SpEL: Kiểm tra user hiện tại là owner
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public UserResponse getUserProfile(String username) { }
// SpEL: Truy cập tham số method
@PreAuthorize("@articleSecurity.isOwner(#articleId, authentication)")
public void editArticle(Long articleId, UpdateArticleRequest request) { }
}
1.3 @PostAuthorize — 実行後のチェック
// Kiểm tra return value
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public ArticleResponse getArticle(Long id) {
return articleRepository.findById(id)
.map(ArticleResponse::from)
.orElseThrow(() -> new ResourceNotFoundException("Article", "id", id));
}
1.4 カスタムセキュリティ式
@Component("articleSecurity")
public class ArticleSecurity {
private final ArticleRepository articleRepository;
public ArticleSecurity(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
public boolean isOwner(Long articleId, Authentication authentication) {
return articleRepository.findById(articleId)
.map(article -> article.getAuthor().getUsername()
.equals(authentication.getName()))
.orElse(false);
}
}
2. CORS 構成
2.1 なぜ CORS が必要なのでしょうか?
フロントエンド (React、Next.js) が実行されるとき localhost:3000 上記のバックエンド localhost:8080、ブラウザは同一オリジン ポリシーに従ってクロスオリジン リクエストをブロックします。 CORS を使用すると、サーバーはどのドメインにアクセスするかを指定できます。
2.2 CORS 構成
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ...other config
;
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"http://localhost:3000", // React dev server
"https://myapp.example.com" // Production frontend
));
config.setAllowedMethods(List.of(
"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
));
config.setAllowedHeaders(List.of(
"Authorization", "Content-Type", "X-Requested-With"
));
config.setExposedHeaders(List.of(
"X-Total-Count", "X-Page-Number"
));
config.setAllowCredentials(true);
config.setMaxAge(3600L); // Cache preflight 1 hour
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
3. CSRF 保護
3.1 CSRF はどのような場合に必要ですか?
- 純粋な REST API (JWT/トークン認証): CSRF (トークンベースの自己保護) を無効にする
- サーバーでレンダリングされたページ (Thymeleaf + フォーム ログイン): CSRF を有効にする
- SPA + Cookie ベースの認証: Cookie リポジトリを使用して CSRF を有効にします
// REST API: Disable CSRF
http.csrf(csrf -> csrf.disable());
// SPA với cookie auth: CSRF via cookie
http.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
);
4. セキュリティヘッダー
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.headers(headers -> headers
.contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff
.frameOptions(frame -> frame.deny()) // X-Frame-Options: DENY
.httpStrictTransportSecurity(hsts -> hsts // HSTS
.maxAgeInSeconds(31536000)
.includeSubDomains(true))
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'"))
);
return http.build();
}
5. レート制限
// Simple in-memory rate limiter (production: dùng Redis)
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Map<String, List<Long>> requestCounts =
new ConcurrentHashMap<>();
private static final int MAX_REQUESTS = 100;
private static final long TIME_WINDOW_MS = 60_000; // 1 phút
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
long now = System.currentTimeMillis();
requestCounts.computeIfAbsent(clientIp, k -> new ArrayList<>());
List<Long> timestamps = requestCounts.get(clientIp);
// Remove expired entries
timestamps.removeIf(t -> now - t > TIME_WINDOW_MS);
if (timestamps.size() >= MAX_REQUESTS) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Rate limit exceeded");
return;
}
timestamps.add(now);
filterChain.doFilter(request, response);
}
}
6. OWASP トップ 10 — Spring Boot のチェックリスト
| # | 脅威 | Spring Boot ソリューション |
|---|---|---|
| A01 | 壊れたアクセス制御 | @PreAuthorize、URL ベースの認証 |
| A02 | 暗号化の失敗 | BCrypt、AES暗号化、TLS |
| A03 | 注射 | パラメータ化されたクエリ (JPA)、入力検証 |
| A04 | 安全でない設計 | 脅威モデリング、最小権限 |
| A05 | セキュリティの設定ミス | セキュリティヘッダー、詳細エラーを無効にする |
| A06 | 脆弱なコンポーネント | dependabot、スプリングブート依存関係管理 |
| A07 | 認証の失敗 | 強力なパスワード、JWT 有効期限、ブルート フォース保護 |
| A08 | データ整合性の失敗 | CSRF 保護、署名付き JWT |
| A09 | ログの失敗 | 構造化されたログ、監査証跡 |
| A10 | SSRF | URL 検証、外部呼び出しの許可リスト |
概要
- メソッド セキュリティ (@PreAuthorize、@PostAuthorize) により、SpEL 式を使用したサービス層での承認が可能になります
- 正しく構成された CORS により、フロントエンドのクロスオリジン アクセスのオリジン、メソッド、ヘッダーが可能になります
- セキュリティ ヘッダー (HSTS、CSP、X-Frame-Options) は、XSS、クリックジャッキング、およびインジェクション攻撃から保護します
演習
- セキュリティ メソッドの実装: @PreAuthorize は、編集/削除を許可する前に、ユーザーがリソースの所有者であることを確認します。
- CORS 構成により、React フロントエンド (localhost:3000 および運用ドメイン) が API にアクセスできるようになります
- レート制限ミドルウェアの実装: IP ごとに最大 100 リクエスト/分、429 個の多すぎるリクエストを返します