SpringSecurityのauthorizeHttpRequestsによるアクセス制御

皆さん、こんにちは。技術開発グループのn-ozawanです。
2月14日はバレンタインですね。中国・ベトナムでは男性から女性へプレゼントする日だそうです。男性の方々は家族サービスをしてみてはいかがでしょうか?

本題です。
Spring Securityでは、http.authorizeHttpRequests(...)を使って、パスへのアクセス制御を行います。このアクセス制御はSpring Securityにおいては基礎中の基礎であり、公式ドキュメントでも1ページ丸ごと使って解説しています。今回はhttp.authorizeHttpRequests(...)について解説しつつ、前回の別のやり方を示したいと思います。

アクセスを制御する

基本

例えば以下のコードがあるとします。

http
  .authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/secure/**").authenticated()
  )

requestMatchers("/secure/**").authenticated()は、リクエストのURLが/secure/**に該当する場合に認証が必要であることを示します。つまり、認証していないユーザーは/secure/**にアクセスすることが出来ません。コードの書き方としては、最初に制御対象のパスを指定して、そのパスに対してどう制御したいのかを指定します。

この構成を連続して記述することも可能です。

http
  .authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/login_page").permitAll()
    .anyRequest().authenticated()
  )

上記の場合、ログイン画面である/login_pageへのアクセスは誰でも見れるようにし、それ以外のアクセスは認証が必要であることを示します。

パスの指定

パスの指定でよく使われるのはanyRequest()requestMatchers(...)です。anyRequest()は全てのパスを指定します。requestMatchers(...)は個別に指定することも可能ですし、ワイルドカードで指定することも可能です。

http
  .authorizeHttpRequests(authorize -> authorize
    .requestMatchers("/hoge", "/foo", "/bar").permitAll()   // 個別に指定することが可能
    .requestMatchers("/api/**").permitAll()                 // ワイルドカードで指定することも可能
    .requestMatchers(HttpMethod.GET).permitAll()            // HTTPメソッドで指定することも可能
    .anyRequest().permitAll()                               // anyRequest()は全てのパスを指定
  )

制御内容

制御には以下があります。

メソッド説明
permitAll()このリクエストに認証は不要であり、誰でもアクセスが可能です。
denyAll()このリクエストはいかなる状況でも許可されません。
authenticated()このリクエストでは認証が必要になります。
認証されない限り、アクセスすることは出来ません。
hasAuthority(…)このリクエストでは特定の権限を有している必要があります。
権限を有していない場合はアクセス出来ません。
hasRole(…)このリクエストでは特定のロールである必要があります。
異なるロールである場合はアクセス出来ません。

keycloakのアクセストークンで、ロールで制御する別の方法

前回、Spring Security でアクセストークンの検証についてお話ししました。その中で、keycloakが発行するアクセストークンには、そのユーザーに与えられたロールが格納されており、そのロールにより拒否するかどうかを検証するやり方を紹介しました。今回はhttp.authorizeHttpRequests(...)を使って制御するやり方を紹介します。

まずはSecurityFilterChainの生成のところを修正します。

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().hasRole("admin"))
      .oauth2ResourceServer(oauth2 ->
        oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(this.jwtAuthenticationConverter()))
      );
    return http.build();
  }

http.authorizeHttpRequests(...)のところでは、hasRole("admin")を指定することにより、adminロールが付与されていることを必要とします。oauth2.jwt(...)では、前回は自前で用意したJwtDecoderを渡していましたが、今回は自前で用意したJwtAuthenticationConverterを渡します。

Spring Securityはユーザーの認証情報や、アクセストークンの情報などを、SecurityContextに保存します。今回特に注目すべきところは、AuthenticationのAuthoritiesです。Authoritiesには認証したユーザーが所有する権限が保存されています。hasRole("admin")はこのAuthoritiesに”ROLE_admin”が存在するかどうかをチェックします。

今回チェックするロールは、keycloak独自のスキーマでアクセストークンに格納されています。なので、アクセストークンからロールを取得してAuthoritiesに保存するための、JwtAuthenticationConverterを自前で用意する必要があります。以下がJwtAuthenticationConverterを生成しているコードになります。

  private JwtAuthenticationConverter jwtAuthenticationConverter() {
    DelegatingJwtGrantedAuthoritiesConverter converter = new DelegatingJwtGrantedAuthoritiesConverter(
      new JwtGrantedAuthoritiesConverter(),
      new Converter<Jwt, Collection<GrantedAuthority>>() {
        @Override
        public Collection<GrantedAuthority> convert(Jwt source) {
          Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
          for (String role : getRoles(source)) {
            grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role));
          }
          return grantedAuthorities;
        }

        @SuppressWarnings("unchecked")
        private Collection<String> getRoles(Jwt jwt) {
          Map<String, Object> claims = jwt.getClaims();
          Map<String, Object> resourceAccess = (Map<String, Object>) claims.get("resource_access");
          Map<String, Object> clientts = (Map<String, Object>) resourceAccess.get(claims.get("azp"));
          return (Collection<String>) clientts.get("roles");
        }
      }
    );

    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(converter);
    return jwtAuthenticationConverter;
  }

2行目から22行目までが、アクセストークンの内容から、Authoritiesに格納するGrantedAuthorityへ変換するための処理となります。3行目のJwtGrantedAuthoritiesConverterはデフォルト処理となるConverterです。4行目から21行目までが、アクセストークンからロールを取得してGrantedAuthorityに変換する処理となります。

DelegatingJwtGrantedAuthoritiesConverterは複数のJwtGrantedAuthoritiesConverterを束ねるクラスです。24行目から26行目にて、JwtAuthenticationConverterに、DelegatingJwtGrantedAuthoritiesConverterを設定して返却しています。

おわりに

hasAuthority(...)hasRole(...)などでアクセス制御が出来るようになると、より細かい制御が可能になります。例えば以下のようにすれば、リクエストが/api/admin/**のAPIのみ、adminロールを必要とし、それ以外のAPIはadminロールなしでもアクセスすることが可能となります。前回紹介したやり方と比べると、より柔軟に制御できるようになります。

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    // アクセストークン検証
    http
      .authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/api/admin/**").hasRole("admin"))
        .anyRequest().authenticated()
      .oauth2ResourceServer(oauth2 ->
        oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(this.jwtAuthenticationConverter()))
      );
    return http.build();
  }

ではまた。

採用情報

「チームアイオス」入団者募集

〜就活で悩むアナタに来て欲しい〜