spring-security について
Spring Security で認証と認可を設定する開発者向けのクイックリファレンスです。JWT、ロール、メソッドセキュリティの例に加え、本番環境の確認項目とトラブルシューティングを扱います。
主な機能
- 01JWT 認証パターン
- 02ロールベースのアクセス制御
- 03メソッド単位の認可
- 04CORS とセキュリティヘッダー
- 05セキュリティテストの指針
活用シーン
- 01JWT 認証フィルターの設定
- 02ロールによるコントローラーメソッドの制限
- 03401、403、CORS の問題の調査
MCP 対応
README では、より詳しい Spring Security ドキュメントの取得先として `mcp__documentation__fetch_docs` を挙げています。
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-securityインストール後、Claude Code で/spring-securityを入力して読み込まれたか確認します。
詳細
Spring Security - クイックリファレンス
このスキルを使用する場合
- JWT 認証を設定する
- ロールベースのアクセス制御
- メソッドレベルのセキュリティ
詳細な知識:包括的なドキュメントについては、technology:
spring-securityを指定してmcp__documentation__fetch_docsを使用してください。
基本的なパターン
セキュリティ設定
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWT フィルター
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws Exception {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String email = jwtService.extractUsername(jwt);
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(email);
if (jwtService.isTokenValid(jwt, user)) {
var auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
メソッドセキュリティ
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) { ... }
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
@GetMapping("/{id}/profile")
public UserProfile getProfile(@PathVariable Long id) { ... }
UserDetails
public class UserPrincipal implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
}
よく使うアノテーション
| アノテーション | 用途 |
|---|---|
@PreAuthorize | メソッド実行前にチェックする |
hasRole('X') | ロールが必要 |
hasAnyRole | いずれかのロールが必要 |
避けるべきアンチパターン
- ステートレスでない場合に CSRF を無効にしない
- フロントエンド用の CORS 設定を忘れない
- JWT シークレットをハードコードしない
本番環境への対応
JWT サービス
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${security.jwt.secret-key}")
private String secretKey;
@Value("${security.jwt.expiration}")
private long jwtExpiration;
@Value("${security.jwt.refresh-expiration}")
private long refreshExpiration;
public String generateToken(UserDetails userDetails) {
return buildToken(userDetails, jwtExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(userDetails, refreshExpiration);
}
private String buildToken(UserDetails userDetails, long expiration) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (ExpiredJwtException | MalformedJwtException e) {
return false;
}
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
CORS 設定
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of(
"https://yourdomain.com",
"https://app.yourdomain.com"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("Authorization"));
config.setMaxAge(3600L);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
レート制限
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final Cache<String, AtomicInteger> requestCounts = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws Exception {
String clientIP = getClientIP(request);
AtomicInteger count = requestCounts.get(clientIP, k -> new AtomicInteger(0));
if (count.incrementAndGet() > 100) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
return;
}
chain.doFilter(request, response);
}
private String getClientIP(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
セキュリティヘッダー
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// ... existing config
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; frame-ancestors 'none'"))
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.block(true))
.contentTypeOptions(Customizer.withDefaults())
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)))
.build();
}
セキュリティテスト
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void unauthenticated_shouldReturn401() throws Exception {
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_withAdminRole_shouldSucceed() throws Exception {
mockMvc.perform(delete("/api/v1/admin/users/1"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_withUserRole_shouldReturn403() throws Exception {
mockMvc.perform(delete("/api/v1/admin/users/1"))
.andExpect(status().isForbidden());
}
}
監視メトリクス
| メトリクス | 目標 |
|---|---|
| 認証失敗の試行回数 | 急増を監視する |
| トークンの検証時間 | < 5ms |
| レート制限への到達回数 | < 1% |
| セキュリティ例外 | 未処理の例外 0 件 |
チェックリスト
- JWT シークレットを環境変数から取得する
- トークンの有効期限を設定する
- リフレッシュトークンの仕組みを用意する
- CORS の許可リストを設定する(本番環境ではワイルドカードを使用しない)
- レート制限を有効にする
- セキュリティヘッダーを設定する
- メソッドレベルのセキュリティを設定する(@PreAuthorize)
- パスワードをエンコードする(BCrypt)
- HTTPS を強制する
- セキュリティ統合テストを実施する
このスキルを使用しない場合
- Spring Boot アプリケーションのセットアップ →
spring-bootスキルを使用 - OAuth2 サーバーの実装 → OAuth2 専用スキルを使用
- 一般的な暗号技術 →
security-expertスキルを使用 - データベースアクセス →
spring-data-jpaスキルを使用 - REST API のパターン →
spring-webスキルを使用
アンチパターン
| アンチパターン | 問題点 | 正しい方法 |
|---|---|---|
| JWT シークレットのハードコード | セキュリティリスク | 環境変数を使用する |
| ステートレスでない場合に CSRF を無効にする | 攻撃に対して脆弱になる | JWT(ステートレス)の場合のみ無効にする |
| 本番環境ですべてのアクセスを許可する | セキュリティが確保されない | 適切な認可を設定する |
| パスワードを平文で保存する | 重大なセキュリティ上の欠陥 | BCryptPasswordEncoder を使用する |
| トークンに有効期限を設定しない | トークンが無効にならない | 適切な有効期限を設定する |
| すべての CORS オリジンを許可する | CSRF の脆弱性 | 特定のオリジンを許可リストに追加する |
簡単なトラブルシューティング
| 問題 | 考えられる原因 | 解決策 |
|---|---|---|
| 401 Unauthorized | トークンがない、または無効 | Authorization ヘッダーの形式を確認する |
| 403 Forbidden | 権限が不足している | ユーザーのロールと @PreAuthorize を確認する |
| CORS エラー | オリジンが許可されていない | CORS 設定にオリジンを追加する |
| フィルターが実行されない | フィルターの順序が誤っている | addFilterBefore の配置を確認する |
| JWT の解析に失敗する | シークレットキーが誤っている | JWT_SECRET が一致することを確認する |
| Authentication が null | フィルターが適用されていない | SecurityContextHolder が設定されていることを確認する |
参考資料
高度な設定については、
mcp__documentation__fetch_docsを参照してください。
- Technology:
spring-security、Topic:basics- Spring Security ドキュメント
コメント
Markdown 対応