Spring Security

認証ライブラリJavaSpringセキュリティOAuth2JWT認証認可Spring Bootエンタープライズ

認証ライブラリ

Spring Security

概要

Spring Securityは、Java/Spring Bootアプリケーション向けの包括的なセキュリティフレームワークです。2025年現在、最新のSpring Security 6.5がリリースされ、OAuth 2.0 Pushed Authorization Requests (PAR)、Demonstrating Proof of Possession (DPoP)仕様への対応、Micrometer統合による自動コンテキスト伝播など、最新のセキュリティ標準をサポートしています。Spring Boot 3.x系との深い統合により、認証(Authentication)と認可(Authorization)、一般的なセキュリティ攻撃に対する保護機能を提供する、Springベースアプリケーションのデファクトスタンダードです。

詳細

Spring Security 6.5は、モダンなJavaアプリケーションに最適化された包括的なセキュリティ機能を提供します。Filter Chainパターンを採用し、各リクエストがセキュリティフィルターを通過することで認証・認可を実行します。SecurityFilterChainを使用した柔軟なセキュリティ設定により、URLレベル、メソッドレベルでの細粒度なアクセス制御が可能です。

2025年最新の機能

  • OAuth 2.0 PAR対応: Pushed Authorization Requestsによるセキュリティ強化
  • DPoP仕様サポート: Demonstrating Proof of Possessionによるトークンバインディング
  • Micrometer統合: 自動コンテキスト伝播とメトリクス監視
  • CVE-2025-41232対応: プライベートメソッドセキュリティアノテーションの修正
  • Spring Boot 3.x完全対応: Java 17+とSpring Framework 6.0要件

アーキテクチャの特徴

OAuth2/JWT、SAML、LDAP、基本認証など多様な認証メカニズムをサポートし、サーブレット(命令型)とリアクティブ(反応型)両方のアプリケーションに対応。SecurityFilterChainによる宣言的セキュリティ設定、メソッドレベルセキュリティ、CSRF保護、セッション管理など、Webアプリケーションセキュリティの全領域をカバーします。

主な特徴

  • 包括的な認証サポート: フォームベース、HTTP Basic、OAuth2、JWT、SAML、LDAPなど
  • 細粒度の認可制御: URLレベル、メソッドレベルでの柔軟なアクセス制御
  • CSRF、セッション固定、クリックジャッキング保護: 一般的なWeb攻撃に対する標準的な防御
  • Spring Boot統合: 自動設定による簡単なセットアップと設定
  • リアクティブサポート: WebFluxアプリケーションでの非同期セキュリティ処理
  • 最新標準準拠: OAuth2、OpenID Connect、SAMLなど業界標準への対応

メリット・デメリット

メリット

  • 成熟した実績: エンタープライズ環境での豊富な運用実績
  • 包括的な機能: 認証から認可、セキュリティ攻撃対策まで一元提供
  • Spring統合: Spring FrameworkやSpring Bootとのシームレスな統合
  • 柔軟な設定: Java ConfigとXML設定の両方をサポート
  • 標準準拠: OAuth2、OpenID Connect、SAMLなど業界標準への対応
  • 豊富なドキュメント: 充実した公式ドキュメントとコミュニティサポート
  • テスト支援: MockMvcとの統合によるセキュリティテストの容易化

デメリット

  • 学習コストの高さ: 豊富な機能の反面、習得には時間が必要
  • 設定の複雑性: 高度な設定では複雑な構成が必要になる場合がある
  • Spring依存: Springフレームワークに強く依存するため他のフレームワークでは使用困難
  • パフォーマンスオーバーヘッド: セキュリティフィルターによる若干の性能影響
  • バージョン互換性: メジャーバージョンアップ時の移行コストが発生する場合がある

参考ページ

書き方の例

基本的なインストールとSpring Boot設定

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") // JWT用
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client") // OAuth2クライアント用
}
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout.permitAll());
        
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

基本認証とユーザー管理

// UserDetailsService実装
@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

// コントローラーでの認証情報取得
@RestController
public class UserController {
    
    @GetMapping("/me")
    public ResponseEntity<String> getCurrentUser(Authentication authentication) {
        String username = authentication.getName();
        return ResponseEntity.ok("Hello, " + username);
    }
    
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin")
    public ResponseEntity<String> adminOnly() {
        return ResponseEntity.ok("Admin area");
    }
}

OAuth2/JWT統合

# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://your-auth-server.com
          audiences: https://your-resource-server.com
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid,profile,email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
// JWT Resource Server設定
@Configuration
public class JwtSecurityConfig {

    @Bean
    public SecurityFilterChain jwtFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthorityPrefix("ROLE_");
        authoritiesConverter.setAuthoritiesClaimName("roles");

        JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
        jwtConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return jwtConverter;
    }
}

メソッドレベルセキュリティ

// メソッドセキュリティの有効化
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
}

// サービスクラスでの利用
@Service
public class DocumentService {

    @PreAuthorize("hasRole('USER')")
    public List<Document> getAllDocuments() {
        return documentRepository.findAll();
    }

    @PreAuthorize("hasRole('ADMIN') or @documentService.isOwner(#id, authentication.name)")
    public Document getDocument(Long id) {
        return documentRepository.findById(id)
            .orElseThrow(() -> new DocumentNotFoundException("Document not found"));
    }

    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public Document updateDocument(Long id, Document document) {
        return documentRepository.save(document);
    }

    public boolean isOwner(Long documentId, String username) {
        return documentRepository.findById(documentId)
            .map(doc -> doc.getOwner().equals(username))
            .orElse(false);
    }
}

REST API認証

// RESTful API用のセキュリティ設定
@Configuration
public class ApiSecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
            .exceptionHandling(exceptions -> exceptions
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            );

        return http.build();
    }

    // カスタム認証エントリーポイント
    @Component
    public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, 
                           AuthenticationException authException) throws IOException {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json");
            response.getWriter().write("""
                {
                    "error": "Unauthorized",
                    "message": "Authentication required",
                    "timestamp": "%s"
                }
                """.formatted(Instant.now().toString()));
        }
    }
}

テストとセキュリティ設定

// テスト設定
@SpringBootTest
@AutoConfigureTestDatabase
@Testcontainers
class SecurityIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(roles = "USER")
    void testUserCanAccessUserEndpoint() throws Exception {
        mockMvc.perform(get("/api/user/profile"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void testAdminCanAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }

    @Test
    void testUnauthorizedAccessDenied() throws Exception {
        mockMvc.perform(get("/api/user/profile"))
            .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "testuser", roles = "USER")
    void testMethodLevelSecurity() throws Exception {
        mockMvc.perform(post("/api/documents")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                    {
                        "title": "Test Document",
                        "content": "Test Content"
                    }
                    """))
            .andExpect(status().isCreated());
    }
}

// JWT統合テスト
@TestMethodOrder(OrderAnnotation.class)
class JwtIntegrationTest {

    @Test
    @Order(1)
    void testJwtAuthentication() throws Exception {
        String token = generateValidJwtToken();
        
        mockMvc.perform(get("/api/protected")
                .header("Authorization", "Bearer " + token))
            .andExpect(status().isOk());
    }

    private String generateValidJwtToken() {
        return Jwts.builder()
            .setSubject("testuser")
            .claim("roles", List.of("USER"))
            .setIssuedAt(new Date())
            .setExpiration(Date.from(Instant.now().plusSeconds(3600)))
            .signWith(SignatureAlgorithm.HS256, "test-secret")
            .compact();
    }
}