Spring Security
認証ライブラリ
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 Security公式サイト
- Spring Security Reference Documentation
- Spring Security GitHub
- Spring Boot Security Auto-configuration
- OAuth 2.0 Resource Server Documentation
書き方の例
基本的なインストールと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();
}
}