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()を使用する場合は、UserDetailsgetAuthorities()が返す権限に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[ユーザー削除]
    end

DispatcherTypeによるマッチング

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マッチング
  • PathPatternRequestMatcherAntPathRequestMatcherの違いと使い分け
  • 実践的な設定パターンとテスト方法

認可設定は「ホワイトリスト方式」を採用し、anyRequest().denyAll()で明示的に許可されていないエンドポイントへのアクセスを拒否することが、セキュアなアプリケーション設計の基本です。また、設定したルールは必ずテストで検証し、意図した通りに動作することを確認してください。

参考リンク