Spring Securityでは、authorizeHttpRequests()メソッドを使用してエンドポイント単位でアクセス制御を設定できます。本記事では、requestMatchersによるURLパターンマッチングと、hasRole/hasAuthority/permitAll/authenticatedなどの認可表現を組み合わせた実践的な認可設定を解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Java |
17以上 |
| Spring Boot |
3.4.x |
| Spring Security |
6.4.x |
| ビルドツール |
Maven または Gradle |
| IDE |
VS Code または IntelliJ IDEA |
事前に以下の知識があると理解がスムーズです。
- Spring Securityの基本概念(認証・認可の違い)
- SecurityFilterChainの仕組み
- REST APIの基本的な設計
authorizeHttpRequestsの基本構造#
authorizeHttpRequests()は、HTTPリクエストに対する認可ルールを定義するためのDSLです。Spring Security 6.x以降では、従来のauthorizeRequests()に代わり、この新しいAPIが推奨されています。
基本的な設定例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").authenticated()
.anyRequest().denyAll()
);
return http.build();
}
}
|
この設定では、リクエストURLに応じて異なる認可ルールを適用しています。
authorizeHttpRequestsの処理フロー#
認可ルールは上から順に評価され、最初にマッチしたルールが適用されます。
flowchart TD
A[HTTPリクエスト] --> B{/public/** にマッチ?}
B -->|Yes| C[permitAll - 許可]
B -->|No| D{/admin/** にマッチ?}
D -->|Yes| E{ROLE_ADMIN を持つ?}
E -->|Yes| F[許可]
E -->|No| G[403 Forbidden]
D -->|No| H{/api/** にマッチ?}
H -->|Yes| I{認証済み?}
I -->|Yes| J[許可]
I -->|No| K[401 Unauthorized]
H -->|No| L[denyAll - 拒否]ルールの順序は非常に重要です。より具体的なパターンを先に、より汎用的なパターンを後に配置してください。
認可表現(Authorization Expressions)#
Spring Securityは、様々な認可表現を提供しています。それぞれの用途と違いを理解することが重要です。
主要な認可表現一覧#
| 認可表現 |
説明 |
例 |
permitAll() |
認証不要でアクセス許可 |
公開ページ、ログインページ |
denyAll() |
すべてのアクセスを拒否 |
デフォルトルール |
authenticated() |
認証済みユーザーのみ許可 |
一般的な保護リソース |
hasRole(role) |
指定ロールを持つユーザーのみ許可 |
管理者ページ |
hasAnyRole(roles...) |
いずれかのロールを持つユーザーを許可 |
複数ロール対応 |
hasAuthority(authority) |
指定権限を持つユーザーのみ許可 |
細かい権限制御 |
hasAnyAuthority(authorities...) |
いずれかの権限を持つユーザーを許可 |
複数権限対応 |
permitAllとdenyAll#
permitAll()は認証を必要としないエンドポイントに使用します。静的リソースやログインページなど、誰でもアクセスできるべきリソースに適用します。
1
2
3
4
5
|
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/home", "/login", "/signup").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated()
)
|
denyAll()はデフォルトですべてのリクエストを拒否する「ホワイトリスト方式」を実現します。明示的に許可されていないエンドポイントへのアクセスを防ぐ、堅牢なセキュリティ設計を実現できます。
1
2
3
4
5
|
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/users/**").hasRole("USER")
.anyRequest().denyAll() // 明示されていないパスはすべて拒否
)
|
authenticatedと匿名アクセス#
authenticated()は、ログイン済みのユーザーであれば誰でもアクセスできるリソースに使用します。
1
2
3
4
5
|
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/profile").authenticated()
.requestMatchers("/api/settings").authenticated()
.anyRequest().permitAll()
)
|
hasRoleとhasAuthority#
hasRole()とhasAuthority()は似ていますが、重要な違いがあります。
1
2
3
4
5
6
7
|
// hasRoleは自動的に「ROLE_」プレフィックスを付与
.requestMatchers("/admin/**").hasRole("ADMIN")
// 内部的には ROLE_ADMIN と比較される
// hasAuthorityはそのままの文字列で比較
.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
// ROLE_ADMIN と比較される
|
| メソッド |
プレフィックス |
DB格納値の例 |
hasRole("ADMIN") |
ROLE_ が自動付与 |
ROLE_ADMIN |
hasAuthority("ADMIN") |
プレフィックスなし |
ADMIN |
hasAuthority("ROLE_ADMIN") |
プレフィックスなし |
ROLE_ADMIN |
hasRole()を使用する場合は、UserDetailsのgetAuthorities()が返す権限にROLE_プレフィックスが含まれている必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// UserDetailsServiceの実装例
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// ROLE_プレフィックスを付けて権限を設定
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities
);
}
|
hasAnyRoleとhasAnyAuthority#
複数のロールまたは権限のいずれかを持つユーザーを許可する場合に使用します。
1
2
3
4
5
6
7
|
.authorizeHttpRequests(authorize -> authorize
// ADMIN または MANAGER のいずれかのロールを持つユーザー
.requestMatchers("/management/**").hasAnyRole("ADMIN", "MANAGER")
// read または write のいずれかの権限を持つユーザー
.requestMatchers("/api/documents/**").hasAnyAuthority("read", "write")
)
|
複合条件による認可#
複数の条件を組み合わせる場合は、access()メソッドとAuthorizationManagersを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import static org.springframework.security.authorization.AuthorizationManagers.allOf;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasRole;
import static org.springframework.security.authorization.AuthorityAuthorizationManager.hasAuthority;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// ADMIN ロールと db 権限の両方が必要
.requestMatchers("/db/**").access(
allOf(hasRole("ADMIN"), hasAuthority("db"))
)
.anyRequest().authenticated()
);
return http.build();
}
|
requestMatchersによるURLマッチング#
requestMatchers()メソッドは、認可ルールを適用するURLパターンを指定します。Spring Security 6.4では、複数のマッチング方式がサポートされています。
Antパターンマッチング#
デフォルトでは、Antスタイルのパスパターンが使用されます。
| パターン |
説明 |
マッチ例 |
/api/* |
1階層のみマッチ |
/api/users, /api/items |
/api/** |
すべてのサブパスにマッチ |
/api/users, /api/users/1/profile |
/api/users/{id} |
パス変数を含むパターン |
/api/users/1, /api/users/abc |
/*.html |
拡張子指定 |
/index.html, /about.html |
1
2
3
4
5
6
7
8
9
10
11
12
|
.authorizeHttpRequests(authorize -> authorize
// /resource 以下のすべてのパスに適用
.requestMatchers("/resource/**").hasAuthority("USER")
// /api/v1 直下の1階層のみに適用
.requestMatchers("/api/v1/*").authenticated()
// 複数パターンを指定
.requestMatchers("/static/**", "/public/**", "/assets/**").permitAll()
.anyRequest().denyAll()
)
|
HTTPメソッドによるマッチング#
特定のHTTPメソッドに対してのみ認可ルールを適用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import org.springframework.http.HttpMethod;
.authorizeHttpRequests(authorize -> authorize
// GETリクエストは読み取り権限が必要
.requestMatchers(HttpMethod.GET, "/api/articles/**").hasAuthority("read")
// POST/PUT/DELETEは書き込み権限が必要
.requestMatchers(HttpMethod.POST, "/api/articles/**").hasAuthority("write")
.requestMatchers(HttpMethod.PUT, "/api/articles/**").hasAuthority("write")
.requestMatchers(HttpMethod.DELETE, "/api/articles/**").hasAuthority("write")
.anyRequest().authenticated()
)
|
RESTful APIの設計では、HTTPメソッドごとに異なる権限を設定することが一般的です。
flowchart LR
subgraph "READ権限"
A[GET /api/users] --> B[ユーザー一覧取得]
C[GET /api/users/1] --> D[ユーザー詳細取得]
end
subgraph "WRITE権限"
E[POST /api/users] --> F[ユーザー作成]
G[PUT /api/users/1] --> H[ユーザー更新]
I[DELETE /api/users/1] --> J[ユーザー削除]
endDispatcherTypeによるマッチング#
Spring MVCの内部ディスパッチ(FORWARD、ERROR)に対する認可も設定できます。
1
2
3
4
5
6
7
8
9
|
import jakarta.servlet.DispatcherType;
.authorizeHttpRequests(authorize -> authorize
// FORWARDとERRORディスパッチを許可(Spring MVCのビューレンダリングとエラーハンドリング用)
.dispatcherTypeMatchers(DispatcherType.FORWARD, DispatcherType.ERROR).permitAll()
.requestMatchers("/endpoint").permitAll()
.anyRequest().authenticated()
)
|
これは、Thymeleafなどのテンプレートエンジンを使用する場合や、Spring Bootのエラーハンドリング機能を使用する場合に重要です。
正規表現によるマッチング#
より厳密なパターンマッチングが必要な場合は、正規表現を使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
.authorizeHttpRequests(authorize -> authorize
// ユーザー名は英数字のみ許可
.requestMatchers(RegexRequestMatcher.regexMatcher("/users/[A-Za-z0-9]+"))
.hasAuthority("USER")
// APIバージョンを正規表現でマッチ
.requestMatchers(RegexRequestMatcher.regexMatcher("/api/v[0-9]+/.*"))
.authenticated()
.anyRequest().denyAll()
)
|
カスタムRequestMatcherの実装#
独自のマッチングロジックが必要な場合は、RequestMatcherインターフェースを実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import org.springframework.security.web.util.matcher.RequestMatcher;
import jakarta.servlet.http.HttpServletRequest;
public class QueryParameterRequestMatcher implements RequestMatcher {
private final String parameterName;
private final String expectedValue;
public QueryParameterRequestMatcher(String parameterName, String expectedValue) {
this.parameterName = parameterName;
this.expectedValue = expectedValue;
}
@Override
public boolean matches(HttpServletRequest request) {
String value = request.getParameter(parameterName);
return expectedValue.equals(value);
}
}
|
使用例:
1
2
3
4
5
6
7
8
9
10
11
|
.authorizeHttpRequests(authorize -> authorize
// ?admin=true のクエリパラメータがある場合はADMIN権限が必要
.requestMatchers(new QueryParameterRequestMatcher("admin", "true"))
.hasRole("ADMIN")
// ラムダ式でも定義可能
.requestMatchers(request -> request.getParameter("debug") != null)
.hasAuthority("DEBUG")
.anyRequest().authenticated()
)
|
PathPatternRequestMatcherとAntPathRequestMatcherの違い#
Spring Security 6.4では、URLマッチングに複数の実装が存在します。それぞれの特徴と使い分けを理解することが重要です。
マッチャーの種類と特徴#
| マッチャー |
説明 |
推奨用途 |
| PathPatternRequestMatcher |
Spring MVCのPathPatternParserを使用 |
Spring MVC統合環境(推奨) |
| AntPathRequestMatcher |
従来のAntスタイルマッチング |
レガシー環境、非MVC環境 |
| RegexRequestMatcher |
正規表現によるマッチング |
複雑なパターン |
PathPatternRequestMatcherの有効化#
Spring MVCと統合された環境では、PathPatternRequestMatcherを使用することが推奨されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// PathPatternRequestMatcherを有効化
@Bean
public PathPatternRequestMatcherBuilderFactoryBean usePathPattern() {
return new PathPatternRequestMatcherBuilderFactoryBean();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// PathPatternRequestMatcherが自動的に使用される
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
}
|
Spring MVCとの統合における注意点#
Spring MVCとSpring Securityでは、同じパスパターンを一貫して解釈する必要があります。不一致があると、セキュリティホールや意図しない403エラーが発生する可能性があります。
flowchart TB
subgraph "正しい設定"
A1[Spring Security<br/>PathPatternRequestMatcher] --> B1["/api/users/**"]
C1[Spring MVC<br/>PathPatternParser] --> B1
B1 --> D1[一貫したマッチング]
end
subgraph "問題のある設定"
A2[Spring Security<br/>AntPathRequestMatcher] --> B2["/api/users/**"]
C2[Spring MVC<br/>PathPatternParser] --> B3["/api/users/{id}"]
B2 --> D2[不一致の可能性]
B3 --> D2
end複数サーブレット環境でのマッチング#
複数のサーブレットマッピングがある場合は、ベースパスを明示的に指定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import static org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher.withDefaults;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
PathPatternRequestMatcher.Builder mvc = withDefaults().basePath("/spring-mvc");
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(mvc.matcher("/admin/**")).hasRole("ADMIN")
.requestMatchers(mvc.matcher("/api/**")).authenticated()
.anyRequest().permitAll()
);
return http.build();
}
|
実践的な設定パターン#
実際のアプリケーションで使用する典型的な認可設定パターンを紹介します。
REST API向け設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(authorize -> authorize
// ヘルスチェックエンドポイント
.requestMatchers("/api/health", "/api/info").permitAll()
// 認証関連
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
.requestMatchers("/api/auth/refresh").authenticated()
// ユーザーリソース
.requestMatchers(HttpMethod.GET, "/api/users/me").authenticated()
.requestMatchers(HttpMethod.PUT, "/api/users/me").authenticated()
.requestMatchers("/api/users/**").hasRole("ADMIN")
// 記事リソース(CRUD)
.requestMatchers(HttpMethod.GET, "/api/articles/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/articles").hasAnyRole("AUTHOR", "ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/articles/**").hasAnyRole("AUTHOR", "ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/articles/**").hasRole("ADMIN")
// デフォルト
.anyRequest().denyAll()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
|
管理画面向け設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Bean
@Order(1)
public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/admin/**")
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin/login").permitAll()
.requestMatchers("/admin/dashboard").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/admin/users/**").hasRole("ADMIN")
.requestMatchers("/admin/settings/**").hasRole("ADMIN")
.requestMatchers("/admin/reports/**").hasAnyRole("ADMIN", "MANAGER")
.anyRequest().hasRole("ADMIN")
)
.formLogin(form -> form
.loginPage("/admin/login")
.defaultSuccessUrl("/admin/dashboard")
);
return http.build();
}
|
マルチテナント環境での設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@Bean
public SecurityFilterChain multiTenantSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// テナント固有のリソースはテナント権限が必要
.requestMatchers("/api/tenants/{tenantId}/**")
.access((authentication, context) -> {
String tenantId = context.getVariables().get("tenantId");
Authentication auth = authentication.get();
boolean hasAccess = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("TENANT_" + tenantId));
return new AuthorizationDecision(hasAccess);
})
// 共通リソース
.requestMatchers("/api/common/**").authenticated()
.anyRequest().denyAll()
);
return http.build();
}
|
認可設定のテスト#
設定した認可ルールが正しく機能するかをテストすることは非常に重要です。
MockMvcを使用したテスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class AuthorizationTest {
@Autowired
private MockMvc mockMvc;
@Test
void publicEndpoint_shouldBeAccessibleWithoutAuth() throws Exception {
mockMvc.perform(get("/api/health"))
.andExpect(status().isOk());
}
@Test
void protectedEndpoint_shouldReturn401WhenNotAuthenticated() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "USER")
void protectedEndpoint_shouldReturn200WhenAuthenticated() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_shouldReturn403ForNonAdmin() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_shouldReturn200ForAdmin() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk());
}
}
|
カスタム権限のテスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Test
@WithMockUser(authorities = {"read", "write"})
void documentEndpoint_shouldAllowWithWriteAuthority() throws Exception {
mockMvc.perform(post("/api/documents")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\": \"Test\"}"))
.andExpect(status().isCreated());
}
@Test
@WithMockUser(authorities = {"read"})
void documentEndpoint_shouldDenyWithoutWriteAuthority() throws Exception {
mockMvc.perform(post("/api/documents")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\": \"Test\"}"))
.andExpect(status().isForbidden());
}
|
トラブルシューティング#
URL認可設定で発生しやすい問題と解決方法を紹介します。
403 Forbiddenが発生する#
| 原因 |
解決方法 |
| ロールのプレフィックスが一致しない |
hasRole()使用時はDB側にROLE_プレフィックスを付ける |
| ルールの順序が不適切 |
より具体的なパターンを先に配置 |
| CSRFトークンが不足 |
API向けはCSRFを無効化するか、トークンを送信 |
| DispatcherTypeが考慮されていない |
FORWARD/ERRORをpermitAllに設定 |
ルールが適用されない#
1
2
3
4
5
6
7
8
9
10
11
|
// 問題: /api/users/123 が anyRequest() にマッチしてしまう
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
.requestMatchers("/api/users/**").hasRole("ADMIN") // 到達しない
)
// 解決: 具体的なルールを先に配置
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/users/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
|
デバッグ方法#
Spring Securityのデバッグログを有効にして、認可処理の詳細を確認できます。
1
2
3
4
5
|
# application.yml
logging:
level:
org.springframework.security: DEBUG
org.springframework.security.web.access: TRACE
|
まとめ#
本記事では、Spring SecurityのURL認可設定について解説しました。
authorizeHttpRequests()によるエンドポイント単位の認可設定
permitAll/denyAll/authenticated/hasRole/hasAuthorityなどの認可表現
requestMatchers()によるAntパターン、HTTPメソッド、正規表現でのURLマッチング
PathPatternRequestMatcherとAntPathRequestMatcherの違いと使い分け
- 実践的な設定パターンとテスト方法
認可設定は「ホワイトリスト方式」を採用し、anyRequest().denyAll()で明示的に許可されていないエンドポイントへのアクセスを拒否することが、セキュアなアプリケーション設計の基本です。また、設定したルールは必ずテストで検証し、意図した通りに動作することを確認してください。
参考リンク#