アプリケーションセキュリティは、現代のエンタープライズシステムにおいて不可欠な要素です。特にWebアプリケーションでは、不正アクセスやデータ漏洩リスクが高まる中、適切な認証・認可の実装が求められます。本記事では、Spring BootアプリケーションにSpring Securityを統合し、実践的なセキュリティ対策を実現する方法を解説します。
- セキュリティの基本概念 アプリケーションの安全確保には以下の特性が重要です:
認証:ユーザーのアイデンティティを検証 認可:許可された操作・リソースへのアクセス制御 データ保護:機密情報の漏洩防止 攻撃対策:XSS、CSRF、SQLインジェクションなどへの防御 セッション管理:不正なセッション乗っ取りの防止
Spring Security は、上記すべてを包括的にカバーする、Spring公式のセキュリティフレームワークです。Spring Boot との統合により、環境構築と基本設定が非常に簡潔になります。
- 基本設定と初期動作 まずは依存関係の追加から開始します:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
この依存を追加すると、デフォルトで以下の動作が有効になります:
すべてのHTTPリクエストが認証を必要とする 自動生成される一時ログインページが提供される デフォルトユーザー名:user、パスワード:コンソール出力で確認 CSRFトークンの自動発行と検証 HTTP Basic認証の有効化
簡易的なアプリケーションでは、プロパティファイルでユーザー情報を定義できます:
spring.security.user.name=steward
spring.security.user.password=s3cr3t
spring.security.user.roles=MANAGER
ただし、本番環境では静的設定に依存せず、動的かつ永続的なアカウント管理への対応が求められます。
- カスタムセキュリティ戦略 より細かな制御を行うには、Javaコンフィギュレーションを使用します。Spring Security 5.7以降では、従来のWebSecurityConfigurerAdapterに代わる新しい方法が推奨されています。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain chain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/static/**", "/health").permitAll()
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAccess()
)
.formLogin(form -> form
.loginPage("/login") // オリジナルログイン画面を指定可能
.defaultSuccessUrl("/dashboard", true)
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
)
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/**")); // API系はCSRF豁免
return http.build();
}
@Bean
public PasswordEncoder encoder() {
return new Argon2PasswordEncoder(); // BCryptに代わる推奨方式
}
@Bean
public UserDetailsService userDetailsService(UserRepository repo) {
return username -> repo.findByLogin(username)
.map(user -> User.builder()
.username(user.getLogin())
.password(user.getPassword())
.authorities(user.getRoles().stream()
.map(r -> "ROLE_" + r.toUpperCase()).toList())
.build())
.orElseThrow(() -> new UsernameNotFoundException(username));
}
}
この構成により、細かいURLごとのアクセス制御と、永続化されたユーザーデータの統合が実現できます。
- ユーザー認証:実践的アプローチ 4.1 パスワードハッシュと暗号化 平文パスワードの保存は厳禁です。Spring Securityは多重ハッシュ方式に対応しており、DelegatingPasswordEncoderを活用することで、将来的なアルゴリズム移行も容易です。
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
// 出力例:{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fzLmGqO
4.2 JWTによる状態LESS認証 REST APIやマイクロサービス間通信では、セッション非依存のJWT認証が広く採用されています。
首先、依存関係の追加:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
</dependency>
JWTトークンの生成と検証クラス:
@Component
public class TokenIssuer {
private final String issuer;
private final Key signingKey;
public TokenIssuer(@Value("${security.jwt.issuer}") String issuer,
@Value("${security.jwt.secret}") String secret) {
this.issuer = issuer;
this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String issue(UserDetails principal, Duration validity) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validity.toMillis());
return Jwts.builder()
.setIssuer(issuer)
.setSubject(principal.getUsername())
.claim("auth", principal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).toList())
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(signingKey, SignatureAlgorithm.HS256)
.compact();
}
public JwtParser parser() {
return Jwts.parserBuilder()
.setSigningKey(signingKey)
.build();
}
}
HTTPリクエストごとのJWT検証フィルター:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final TokenIssuer tokenIssuer;
private final CustomUserDetails loadUser;
public JwtAuthFilter(TokenIssuer issuer, CustomUserDetails service) {
this.tokenIssuer = issuer;
this.loadUser = service;
}
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse resp,
FilterChain chain) {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Jws<Claims> claims = tokenIssuer.parser()
.parseClaimsJws(token);
String subject = claims.getBody().getSubject();
UserDetails details = loadUser.loadUserByUsername(subject);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
details, null, details.getAuthorities()
);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException ex) {
// 不正または期限切れトークンの場合は、認証失敗として処理
}
}
chain.doFilter(req, resp);
}
}
このフィルターをセキュリティチェーンに組み込むことで、状態LESSな認証が可能になります。
4.3 OAuth 2.0 クライアントと統合 Google、GitHub、Okta等の外部 IdPと連携するために、Spring BootのOAuth2 Client統合を使います。
spring:
security:
oauth2:
client:
registration:
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: user:email,read:user
client-name: GitHub Login
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid,profile,email
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
user-name-attribute: email
@Bean
SecurityFilterChain OAuth2Chain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login**", "/webjars/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/profile", true)
)
.logout(st -> st.logoutUrl("/logout").logoutSuccessUrl("/"));
return http.build();
}
ユーザー情報の取得は、以下のように@AuthenticationPrincipalで取得可能です:
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal OAuth2User oauth2User,
Model model) {
model.addAttribute("name", oauth2User.getAttribute("name"));
model.addAttribute("email", oauth2User.getAttribute("email"));
return "profile";
}
- メソッド単位のアクセス制御 APIやサービス層で、個別のメソッド単位で権限を制御したい場合、@PreAuthorizeや@PostAuthorizeが必要です。
まず、AOPライブラリとEnable属性を追加します:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig { }
メソッド側で:
@Service
public class AdminService {
@PreAuthorize("hasAuthority('ADMIN') and @permissionChecker.canAccess(#id)")
public User dtoById(Long id) { ... }
@PostFilter("filterObject.staffOnly or hasRole('ADMIN')")
public List<User> listAll() { ... }
}
カスタム権限評価は、PermissionEvaluator実装で拡張可能です:
@Component("permissionChecker")
public class CustomPermissionChecker implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication auth,
Object target, Object permission) {
// リソース・権限の動的評価ロジック
}
}
- セキュリティにおける推奨事項
最小権限の原則——必要な最小限の権限のみ付与 HTTP Strict Transport Security(HSTS)の有効化 CSPヘッダーによるスクリプト実行制御 Cookieフラグ:HttpOnly+Secure+SameSite=Strict 入力検証(JSR-303 Bean Validation活用) SQLインジェクションにははもちろん、ORMやJOINの安全な使用 ログに機微情報が記録されないよう、シャドウロギングの抑制 CVEデータベース(NVD)とOSSスキャナー(OWASP DC)での定期監視
- 統合Tipsと拡張ポイント présente以下のような構成で柔軟なセキュリティ制御が可能です:
カスタムAccessDeniedHandlerで403レスポンスを制御 AuthenticationSuccessHandlerでログイン時イベント実装(例:多要素認証必須チェック) SessionRegistryで同一ユーザーの多重ログイン制限 認証成功後にカスタムヘッダの注入(X-Auth-User-IDなど)
このように、Spring Securityは非常に高い拡張性を備えており、プロジェクト要件に応じた厳密なセキュリティARDS(Access Control Rules)の構築が可能になります。