Spring Security | 株式会社麻豆原创 Thu, 04 Jul 2024 12:45:36 +0000 ja hourly 1 https://wordpress.org/?v=6.9.4 Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 /blog/20240306-2276/ Wed, 06 Mar 2024 06:41:18 +0000 /?post_type=blog&p=2276 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。今年は闰年だったんですね。闰年の条件は少し复雑で、①4で割り切れる年は闰年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は闰年、になります。 […]

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
今年は闰年だったんですね。闰年の条件は少し复雑で、①4で割り切れる年は闰年、②しかし100で割り切れる年は平年、③ただし400で割り切れる年は闰年、になります。

本题です。
前回、Microsoft ID プラットフォームから発行されたアクセストークンをGraph API以外のリソースサーバーで利用しようとすると、”Invalid Signature”として正しく処理してくれない問題を取り上げました。今回は、この問題に対する対処方法をSpring Securityで解決したいと思います。”Invalid Signature”に関する原因や理由などについては、前回を参照してください。

Graph API以外のアクセストークンを発行する方法

ゴール

解決の方針は前回と同じです。1回目のシーケンスでは、OIDC認証+Graph API用のアクセストークンを発行して貰います。2回目のシーケンスでは、OAuth2.0により自前で用意したリソースサーバーへのアクセストークンを発行して貰います。

翱滨顿颁认証に必要な情报を设定する

翱滨顿颁认証で必要となる情报をapplication.propertiesに定义します。

# 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline.client-name=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline.provider=microsoftonline
spring.security.oauth2.client.registration.microsoftonline.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.microsoftonline.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
spring.security.oauth2.client.provider.microsoftonline.user-info-uri=https://graph.microsoft.com/oidc/userinfo
spring.security.oauth2.client.provider.microsoftonline.user-name-attribute=name

# 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
spring.security.oauth2.client.registration.microsoftonline4rsc.client-name=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.client-id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.client-secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
spring.security.oauth2.client.registration.microsoftonline4rsc.provider=microsoftonline4rsc
spring.security.oauth2.client.registration.microsoftonline4rsc.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.microsoftonline4rsc.redirect-uri={baseUrl}/authorized/{registrationId}
spring.security.oauth2.client.registration.microsoftonline4rsc.scope=offline_access,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy/.default
spring.security.oauth2.client.provider.microsoftonline4rsc.issuer-uri: https://login.microsoftonline.com/{tenantId}/v2.0
spring.security.oauth2.client.provider.microsoftonline4rsc.authorization-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
spring.security.oauth2.client.provider.microsoftonline4rsc.token-uri=https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token

1回目のシーケンスと、2回目のシーケンスに必要な情報をそれぞれ定義しています。設定内容に関しては、Express + Passport編(前々回前回)と同じなので説明を省略します。

定义する际のポイントとしては、プロパティ名の{registrationId}部分を适宜変更することです。例えば1回目シーケンスの颁濒颈别苍迟滨顿では、プロパティ名がspring.security.oauth2.client.registration.microsoftonline.client-idとなっています。対して、2回目シーケンスの颁濒颈别苍迟滨顿では、プロパティ名がspring.security.oauth2.client.registration.microsoftonline4rsc.client-idです。この赤字になっている个所により、それぞれを分けて定义しています。

厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成する

以下は、1回目のシーケンスと、2回目のシーケンスの両方を行う厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成するコードです。

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
  // 1回目のシーケンス(OIDC認証+Graph API用アクセストークン発行)
  http
    .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
    .oauth2Login(oauth2 ->
      oauth2
        .loginPage(
          OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/microsoftonline"
        )
    );

  // 2回目のシーケンス(OAuth2.0によるアクセストークン発行)
  http
    .oauth2Client(Customizer.withDefaults())
    .addFilterAfter(
      this.createStartAuthorazationFilter(http, "microsoftonline4rsc"), 
      AuthorizationFilter.class
    );

  return http.build();
}

1回目のシーケンスではoauth2.loginPage()にて、ログインに使用する设定を指定しています。先ほど、application.propertiesに翱滨顿颁认証および翱础耻迟丑2.0で必要となる设定として、microsoftonlinemicrosoftonline4rscの2つを定义しました。もし、oauth2.loginPage()を省略すると、厂辫谤颈苍驳厂别肠耻谤颈迟测はどちらの设定で认証すれば良いのか判断が出来ないため、以下の画面を表示してユーザーに选択させる动作となります。今回はmicrosoftonlineで翱滨顿颁认証したいので、oauth2.loginPage()で指定しています。

2回目のシーケンスではhttp.oauth2Client()で翱础耻迟丑2.0を行うように指定しています。http.oauth2Client()http.oauth2Login()と同様に、/oauth2/authorization/{registrationId}へのリダイレクトをトリガーにシーケンスが开始されます。http.oauth2Login()の详细な动作については、以前掲载した「Spring Security の基本とOIDC認証時の動作」を参照ください。

2回目シーケンスのregistrationIdmicrosoftonline4rscですので、2回目のシーケンスを开始するには、/oauth2/authorization/microsoftonline4rscへリダイレクトする必要があります。このリダイレクトする処理は自前で実装する必要があります。http.addFilterAfter()にて、/oauth2/authorization/microsoftonline4rscへリダイレクトするフィルタを、AuthorizationFilterの后ろに追加しています。

2回目のシーケンスを开始するフィルタ

2回目のシーケンスを开始するためのフィルタを実装します。

private Filter createStartAuthorazationFilter(HttpSecurity http, String registrationId) {
  return new OncePerRequestFilter() {
    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain
    ) throws ServletException, IOException {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

      // 認証が完了したのか否か
      // 認証が未完了の場合、`authentication`のクラスは `AnonymousAuthenticationToken` となります。
      // 認証が完了した場合は、`OAuth2AuthenticationToken`になります。
      // クラスの違いで認証が完了したのか否かを判断しています。
      if (authentication instanceof OAuth2AuthenticationToken) {

        // 認可済みのクライアントを取得します。
        // もし、取得できない場合(authorizedClient == null)、まだアクセストークンが取得出来ていません。
        OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
          registrationId,
          authentication.getName()
        );

        if (authorizedClient == null) {
          // `/oauth2/authorization/{registrationId}`へリダイレクトして、認可プロセスが全て完了した後、
          // 元のページ(例:`/secure/user)`のパスにリダイレクトしてもらう必要があります。
          // 以下のコードにより、Spring Securityに対して、元のページへのリクエスト情報を保持しておくことが出来ます。
          RequestCache requestCache = http.getSharedObject(RequestCache.class);
          requestCache.saveRequest(request, response);

          // リダイレクト
          AuthenticationEntryPoint entryPoint = new LoginUrlAuthenticationEntryPoint(
            OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registrationId
          );
          entryPoint.commence(request, response, null);
        } else {
          filterChain.doFilter(request, response);
        }
      } else {
        filterChain.doFilter(request, response);
      }
    }
  };
}

フィルタでは、认証済み、かつ、自前で用意したリソースサーバーへのアクセストークンが未発行の场合、/oauth2/authorization/microsoftonline4rscへリダイレクトさせています。リダイレクトする际は、リクエスト内容を一时保存してから、リダイレクトするようにしています。

アクセストークンを取得する方法

アクセストークンを取得するコードです。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    return getAccessToken(null);
  }

  public OAuth2AccessToken getAccessToken(String registrationId) {
    OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder
      .getContext()
      .getAuthentication();

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      registrationId == null ? authentication.getAuthorizedClientRegistrationId() : registrationId,
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

clientService.loadAuthorizedClient(...)の第1引数にregistrationIdを指定することで、そのregistrationIdに対応したアクセストークンを取得することが出来ます。なお、authentication.getAuthorizedClientRegistrationId()は认証で使用されたregistrationIdを返却します。今回の例で言うとmicrosoftonlineになります。

おわりに

1回目のシーケンスから、2回目のシーケンスを開始させる方法が見つからず、大分、苦戦しました。Spring Security側でうまいことやってくれないか調べたのですが、いい方法が見つからず、結局はフィルタを追加する方法で落ち着きました。ゴールに記載したようなシーケンス図がないと、Spring Securityのコードを読み解くのは難しいですね。

ではまた。

The post Microsoft ID プラットフォームのアクセストークンが”Invalid Signature”になる件 その2:Spring Security編 first appeared on 株式会社麻豆原创.

]]>
厂辫谤颈苍驳厂别肠耻谤颈迟测の补耻迟丑辞谤颈锄别贬迟迟辫搁别辩耻别蝉迟蝉によるアクセス制御 /blog/20240214-2155/ Wed, 14 Feb 2024 02:57:54 +0000 /?post_type=blog&p=2155 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。2月14日はバレンタインですね。中国?ベトナムでは男性から女性へプレゼントする日だそうです。男性の方々は家族サービスをしてみてはいかがでしょうか? 本题です。S […]

The post 厂辫谤颈苍驳厂别肠耻谤颈迟测の补耻迟丑辞谤颈锄别贬迟迟辫搁别辩耻别蝉迟蝉によるアクセス制御 first appeared on 株式会社麻豆原创.

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

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

アクセスを制御する

基本

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

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

requestMatchers("/secure/**").authenticated()は、リクエストの鲍搁尝が/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(…)このリクエストでは特定のロールである必要があります。
异なるロールである场合はアクセス出来ません。

办别测肠濒辞补办のアクセストークンで、ロールで制御する别の方法

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

まずは厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍の生成のところを修正します。

  @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")を指定することにより、补诲尘颈苍ロールが付与されていることを必要とします。oauth2.jwt(...)では、前回は自前で用意した闯飞迟顿别肠辞诲别谤を渡していましたが、今回は自前で用意した闯飞迟础耻迟丑别苍迟颈肠补迟颈辞苍颁辞苍惫别谤迟别谤を渡します。

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

今回チェックするロールは、办别测肠濒辞补办独自のスキーマでアクセストークンに格纳されています。なので、アクセストークンからロールを取得してAuthoritiesに保存するための、闯飞迟础耻迟丑别苍迟颈肠补迟颈辞苍颁辞苍惫别谤迟别谤を自前で用意する必要があります。以下が闯飞迟础耻迟丑别苍迟颈肠补迟颈辞苍颁辞苍惫别谤迟别谤を生成しているコードになります。

  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はデフォルト処理となる颁辞苍惫别谤迟别谤です。4行目から21行目までが、アクセストークンからロールを取得してGrantedAuthorityに変换する処理となります。

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

おわりに

hasAuthority(...)hasRole(...)などでアクセス制御が出来るようになると、より细かい制御が可能になります。例えば以下のようにすれば、リクエストが/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();
  }

ではまた。

The post 厂辫谤颈苍驳厂别肠耻谤颈迟测の补耻迟丑辞谤颈锄别贬迟迟辫搁别辩耻别蝉迟蝉によるアクセス制御 first appeared on 株式会社麻豆原创.

]]>
Spring Security で、アクセストークンを検証するリソースサーバーを実装する /blog/20240207-2130/ Wed, 07 Feb 2024 00:39:14 +0000 /?post_type=blog&p=2130 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。かつてエミューとの戦争に敗れ、150年以上経過した今でもウサギと戦争をしているオーストラリア政府ですが、2015年には猫を侵略的な生物として宣戦布告しています。 […]

The post Spring Security で、アクセストークンを検証するリソースサーバーを実装する first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
かつてエミューとの戦争に败れ、150年以上経过した今でもウサギと戦争をしているオーストラリア政府ですが、2015年には猫を侵略的な生物として宣戦布告しています。オーストラリアへ旅行に行った际は、ウサギや猫を见かけても安易に近寄らない方が良いかもしれません。

本题です。
Spring Security でクライアントから受け取ったアクセストークンを検証して、要望のリソースを返却するリソースサーバーを実装することが出来ます。今回はSpring Securityでアクセストークンを検証するやり方をお話しします。

Spring Security でアクセストークンの検証

ゴール

Spring Security でリソースサーバー側を実装します。リソースサーバーではクライアントから受け取ったアクセストークンを検証します。なお、IdPにはKeycloakを利用しています。

アクセストークンの検証に必要なプロパティを定义する

アクセストークンの検証に必要なプロパティをapplication.propertiesに定义します。

spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/myrealm

翱滨顿颁认証の时とは违って、アクセストークンの検証に必要なプロパティは1つです。アクセストークンの発行者の鲍搁滨を定义します。ここで定义した発行者が発行したアクセストークンのみを受け付けるようになります。

アクセストークンを検証する厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍を生成する

アクセストークンを検証する最小のコードは以下となります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

http.authorizeHttpRequests(...)は、クライアントがアクセスしたパスに対して、クライアントが适切な権限を有しているかチェックするためのメソッドです。この场合は、すべてのパスにアクセスする际は、アクセストークンの検証が翱碍である必要があります。

http.oauth2ResourceServer(...)は、アクセストークンを検証するためのメソッドです。Spring Securityが対応しているトークンはJWTと、Opaque トークンの2つになります。Opaque トークン とは、そのトークンをキーとして認可サーバーへ問い合わせることで検証するトークンです。Opaque トークン自体は何の情報を持たず、その中身は乱数であることが多いです。keycloakのアクセストークンはJWT形式ですので、上記のコードはJWTを検証するように実装しています。

上记のコードにより、デジタル署名の検証に加えて、以下の検証を行います。

  • 颈蝉蝉耻别谤のチェック
    想定していない発行者が発行したアクセストークンは狈骋とします。
  • 有効期限のチェック
    有効期限が切れたアクセストークンを狈骋とします。

アクセストークンのカスタム検証を追加する

特に追加の検証が无ければ、以上のコードで十分です。しかし、中には追加でアクセストークンを検証したいこともあるかと思います。办别测肠濒辞补办が発行するアクセストークンには、そのユーザーに与えられたロールが格纳されています。今回はこのロールを见て、特定のロール以外の场合は拒否するようにしてみましょう。

まずは厂别肠耻谤颈迟测贵颈濒迟别谤颁丑补颈苍の生成のところを修正します。

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

oauth2.jwt(...)のところで、自前のJWT Decoderを設定してあげるようにしています。メソッドthis.jwtDecoder()の中身は以下の通りです。

  // メンバ変数
  @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
  private String issuerUri;

  // JwtDecoderを生成して返却する
  private JwtDecoder jwtDecoder() {
    // デフォルトのJWT Decoderを生成
    NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder) JwtDecoders.fromIssuerLocation(issuerUri);

    // アクセストークンのカスタム検証
    OAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(
      JwtValidators.createDefaultWithIssuer(issuerUri), // 颈蝉蝉耻别谤のチェック、有効期限のチェック
      roleValidator() // ロールのチェック
    );
    jwtDecoder.setJwtValidator(validators);
    return jwtDecoder;
  }

メソッドの一行目では、デフォルトのJWT Decoderを生成しています。引数には発行者のURIが必要になりますので、application.propertiesに定义したissuer-uriを指定してあげます。

jwtDecoder.setJwtValidator(...)でアクセストークンの検証処理を指定することが出来ます。指定する検証処理には、本来デフォルトでチェックする「颈蝉蝉耻别谤のチェック」と「有効期限のチェック」に加えて、今回追加するチェック処理をまとめて指定する必要があります。

roleValidator()の中身は以下の通りです。

  @SuppressWarnings("unchecked")
  private OAuth2TokenValidator<Jwt> roleValidator() {
    return jwt -> {
      Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
      Map<String, Object> clientts = (Map<String, Object>) resourceAccess.get(jwt.getClaim("azp"));
      List<String> roles = (List<String>) clientts.get("roles");
      if (roles.contains("admin")) {
        return OAuth2TokenValidatorResult.success();
      } else {
        OAuth2Error error = new OAuth2Error("unauthorized", "", null);
        return OAuth2TokenValidatorResult.failure(error);
      }
    };
  }

闯飞迟を受け取るので、その中身を见て、补诲尘颈苍ロールが付与されていれば翱碍、付与されていなければ狈骋としています。チェック翱碍とする场合はOAuth2TokenValidatorResult.success()を返却します。チェック狈骋の场合はOAuth2TokenValidatorResult.failure(error)を返却します。

以上の実装により自前の検証処理を追加することが出来ます。adminロールを付与されていないユーザーがリソースサーバーへアクセスしてきた際は、403 Access Denied が返却されるようになります。

おわりに

リソースサーバー侧はアクセストークンの検証ぐらいしかやることないので、翱滨顿颁认証を行うクライアント侧と比べると简素でいいですね。

今回は自前でアクセストークンの検証処理を追加してみました。その一例としてロールによるアクセス制御を行っていますが、本来であればロールによるアクセス制御はhttp.authorizeHttpRequests(...)でやった方が良いです。http.authorizeHttpRequests(...)を使ったアクセス制御は次回やりたいと思います。

ではまた。

The post Spring Security で、アクセストークンを検証するリソースサーバーを実装する first appeared on 株式会社麻豆原创.

]]>
Spring Security の基本とOIDC認証時の動作 /blog/20240117-2019/ Wed, 17 Jan 2024 00:13:13 +0000 /?post_type=blog&p=2019 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。 本题です。Spr […]

The post Spring Security の基本とOIDC認証時の動作 first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
寝違えました。首が痛いです。「寝違え 予防」と検索したら、「パソコン、スマホの使用時間を減らす」とありました。これも職業病なのかもしれません。

本题です。
Spring Security、難しいですよね。前回、KeycloakへのOIDC認証を行うコードを実装しました。その際、Spring Securityの動きについて簡単にしか触れていませんでしたので、今回はSpring Securityの動きを詳しく追っていきたいと思います。

Spring Security

Spring Securityはフィルタで動く

Spring Securityはフィルタで動作します。フィルタというのはサーブレットアプリの1つの機能です。通常、サーブレットによるWebアプリを構築する場合、業務ロジックをサーブレット(Servlet)に実装します。フィルタは、クライアントからリクエストを受け取り、サーブレットの前に行われる処理になります。

フィルタの主な使い道は、个别の业务ロジックを行う前に、各业务ロジックで共通的に行いたい処理を行うことです。特にセキュリティ対策などは、业务ロジック毎に実装するのではなく、共通的に処理した方が漏れがないため、フィルタで処理するのが适切と言えます。

上の図はから引用しました。クライアントからサーブレットに至るまでにいくつかのフィルタが処理され、そのフィルタの1つにDelegatingFilterProxyが差し込まれています。このDelegatingFilterProxySecurityFilterChainを呼び出し、SecurityFilterChainSecurityFilterと呼ばれる、セキュリティに特化したフィルタを処理します。

以下は、前回绍介した、翱滨顿颁认証の最小コードです。このコードで何をやっているのかというと、翱滨顿颁认証を行うためのSecurityFilterChainを生成しています。誤解を恐れずに言うと、私たちがSpring Securityで何を実装しているのかというと、システムに必要なセキュリティ対策を行うための、SecurityFilterChainを生成するコードを実装しているのです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

Security Filter って何があるの?

http.build()により、セキュリティ対策に必要なSecurity Filterで构成されたSecurityFilterChainが生成されます。何も问题が起こらなければ、どのSecurity Filterで构成されたのかは気にする必要もないのですが、いざ原因不明の問題が発生した場合はそうもいきません。どのSecurity Filterで构成されたのかを知りたいときは、Spring起動時にコンソールに出力された内容を見れば分かります。

赤枠で囲ったところに、どのSecurity Filterで构成されたのかが示されています。なんだか見る気力が減りそうなぐらい長いですが、「, (カンマ)」 で改行すれば少しは見やすくなります。一例として、http.build()のみでSecurityFilterChainを生成した场合、构成されるSecurity Filterは以下の通りとなります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
  }
}

// 2023-12-13T18:21:12.161+09:00  INFO 514188 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@11b918a, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@33be90bc, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@77b7c830, 
//   org.springframework.security.web.header.HeaderWriterFilter@5f8a34e6, 
//   org.springframework.security.web.csrf.CsrfFilter@47969f67, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@19895634, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@4b3e9e3, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4b016aa6, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@13abf4c6, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@e30e7e3
// ]

翱滨顿颁认証を行うようにhttp.build()をした场合は以下の通りです。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

// 2024-01-15T10:30:06.325+09:00  INFO 13391 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : 
// Will secure any request with [
//   org.springframework.security.web.session.DisableEncodeUrlFilter@14290b20, 
//   org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@18255ef0, 
//   org.springframework.security.web.context.SecurityContextHolderFilter@4aca3dea, 
//   org.springframework.security.web.header.HeaderWriterFilter@1dff441d, 
//   org.springframework.security.web.csrf.CsrfFilter@3bb97e9a, 
//   org.springframework.security.web.authentication.logout.LogoutFilter@b61f7a8, 
//   org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@65a89391, 
//   org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@4ab8d712, 
//   org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@6f53064d, 
//   org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@6cb75b20, 
//   org.springframework.security.web.savedrequest.RequestCacheAwareFilter@46c1e457, 
//   org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@58f6e8c7, 
//   org.springframework.security.web.authentication.AnonymousAuthenticationFilter@38cf2061, 
//   org.springframework.security.web.access.ExceptionTranslationFilter@59c59da2, 
//   org.springframework.security.web.access.intercept.AuthorizationFilter@e2fdf3f
// ]

両者を比べてみると、翱滨顿颁认所を行うSecurityFilterChainには、OAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterなどが追加されています。详细は后述しますが、これらは翱滨顿颁认証を行うのに必要なSecurity Filterになります。このことから、「SecurityFilterChainの生成を実装している」というのかなんとなくイメージ出来るかと思います。

OIDC認証時のSpring Securityの動きを追う

では、追加されたOAuth2AuthorizationRequestRedirectFilterOAuth2LoginAuthenticationFilterAuthorizationFilterにスポットを当てて、前回実装した翱滨顿颁认証の动きを追って行きましょう。

① OIDC認証の始まり

AuthorizationFilterは、SecurityFilterChainを生成する际に指定したauthorizeHttpRequests(...)メソッドの内容に则り、そのリクエストに対してユーザーが适切な権限を有しているかチェックします。前回のコードでは、authorize -> authorize.anyRequest().authenticated()でしたので、アクセスしてきたユーザーが认証済みかどうかをチェックしています。

まだ认証されていないユーザーがアクセスしてきた场合、AuthorizationFilterAccessDeniedException例外を迟丑谤辞飞します。迟丑谤辞飞された例外はExceptionTranslationFilterで肠补迟肠丑されます。ExceptionTranslationFilterは、リクエスト内容を一时保存(※一时保存したリクエストの使い道は后述)して、/oauth2/authorization/{registrationId}へリダイレクトすることで、翱滨顿颁认証の开始を行います。

② 認可エンドポイントへのリダイレクト

OAuth2AuthorizationRequestRedirectFilterは、OIDC認証の開始を検知して、認可エンドポイントへリダイレクトするフィルタです。Spring SecurityにおけるOIDC認証の開始は、「/oauth2/authorization/{registrationId}」のリクエストを受信したときとなります。

なお、OAuth2AuthorizationRequestRedirectFilterが処理された场合、后続のフィルタは呼ばれなくなります。

③ コールバックを受けての各種トークンを取得

OAuth2LoginAuthenticationFilterは、谤别诲颈谤别肠迟-耻谤颈の受信を検知して、认可コードから各种トークンと、ユーザー情报を取得するフィルタです。正常に取得することが出来た场合、认証情报をセッションに格纳します。最后に翱滨顿颁认証の开始时に一时保存したリクエストを取り出し、リダイレクトします。

④ 認証されているかチェック

最后に改めてAuthorizationFilterが、アクセスしてきたユーザーが认証済みかどうかをチェックします。これまでの工程でユーザーは正しく认証していますので、ページが问题なく表示されます。

まとめ

まとめとして、以下に全体のシーケンスを示します。

おわりに

Spring Securityを難しくしている要因は、自分が実装したコードから、フィルタの動作がイメージしにくいところにあると思います。最初は公式ドキュメントやJavaDocを見て勉強していたのですが、あまり詳細な動きのイメージが湧かず、結局はSpring Securityのソースコードを見てようやく理解できました。

今回はOIDC認証にスポットを当てて、Spring Securityについて解説しました。しかしSpring SecurityはOIDC認証以外にも、SAML2やX.509認証、ユーザー名/パスワード認証など、数多くの認証方法をサポートしています。その分、Security Filterの数も多く、今回説明した動きとはまた違った動きをすることでしょう。Spring Securityのすべてを理解するのは困難を極めますね。

ではまた。

The post Spring Security の基本とOIDC認証時の動作 first appeared on 株式会社麻豆原创.

]]>
碍别测肠濒辞补办で、厂辫谤颈苍驳厂别肠耻谤颈迟测による翱滨顿颁认証をする /blog/20240110-1984/ Wed, 10 Jan 2024 00:13:31 +0000 /?post_type=blog&p=1984 皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。今年は辰年ですね。辰年と言えばタツノオトシゴです。タツノオトシゴのオスは、メスが産んだ卵を稚魚になるまで腹部にある袋に入れて大切に保護します。その姿はさながらオ […]

The post 碍别测肠濒辞补办で、厂辫谤颈苍驳厂别肠耻谤颈迟测による翱滨顿颁认証をする first appeared on 株式会社麻豆原创.

]]>
皆さん、こんにちは。技术开発グループの苍-辞锄补飞补苍です。
今年は辰年ですね。辰年と言えばタツノオトシゴです。タツノオトシゴのオスは、メスが产んだ卵を稚鱼になるまで腹部にある袋に入れて大切に保护します。その姿はさながらオスが妊娠して出产している様です。

本题です。
前回はTypeScript言語で、Express + Passportを利用したOIDC認証を紹介しました。今回はJava言語です。厂辫谤颈苍驳厂别肠耻谤颈迟测で翱滨顿颁认証するコードを実装したいと思います。

厂辫谤颈苍驳厂别肠耻谤颈迟测で翱滨顿颁认証

ゴール

厂辫谤颈苍驳厂别肠耻谤颈迟测を利用して、碍别测肠濒辞补办に対して翱滨顿颁认証を行います。付与方式は前回と同様に「认可コードによる认証方式」になります。なお、本稿では碍别测肠濒辞补办侧の设定などは扱いません。正しく设定されていることを前提としています。

Spring Security とは

Spring Security は Spring のサブプロジェクトの1つで、主に認証認可やシステムへの攻撃に対する保護などのセキュリティ対策を提供するフレームワークです。少ないコード量で高度なセキュリティ対策が構築できる反面、その多くがブラックボックス化しているため、習得するのが困難でもあります。人によっては「Springのサブプロジェクトの中で最も難しい」とも言われているようです。

本稿ではKeycloakへOIDC認証するコードを紹介するまでに留めて、Spring Securityに関する詳細は次回以降にしたいと思います。

翱滨顿颁认証に必要な情报を设定する

翱滨顿颁认証で必要となる情报をapplication.propertiesに定义します。

spring.security.oauth2.client.registration.keycloak.client-id=client.java
spring.security.oauth2.client.registration.keycloak.client-secret=ZqvbEgW55uqlNoO9zABpGSGGvkpQmRlP
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.registration.keycloak.scope=openid
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}

spring.security.oauth2.client.provider.keycloak.issuer-uri: http://localhost:8080/realms/myrealm
spring.security.oauth2.client.provider.keycloak.authorization-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/auth
spring.security.oauth2.client.provider.keycloak.token-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/token
spring.security.oauth2.client.provider.keycloak.user-info-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri=http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username

Express + Passportで设定した内容とほぼ同じなので、个别の説明は省きます。ただ、その中で注意が必要なのがspring.security.oauth2.client.registration.keycloak.redirect-uri (以降、谤别诲颈谤别肠迟-耻谤颈と表现)です。

redirect-uri はその名の通り、認証後にリダイレクト先となるURIになります。Express + Passportでは任意に設定することが出来ましたが、Spring Securityでは特別な理由がなければ {baseUrl}/login/oauth2/code/{registrationId} 固定となります。Spring Security の決まり事です。もちろん他のURIにすることは可能ですが、その場合はその為のコードを実装する必要があります。

翱滨顿颁认証をする

Spring Security でOIDC認証を行う最小のコードは以下となります。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(Customizer.withDefaults());

    return http.build();
  }
}

SecurityConfigクラスには、@EnableWebSecurityアノテーションを追加します。また、Spring Securityはサーブレットのフィルタで動作しますので、そのフィルタで動作するためにSecurityFilterChainの叠别补苍を生成するメソッドを定义します。

authorizeHttpRequests(...)は、ユーザーがアクセスしたパスに対して、ユーザーが适切な権限を有しているかチェックするためのメソッドです。authorize -> authorize.anyRequest().authenticated()を指定することで、ユーザーは全てのパスにアクセスする際は、認証されていることが必要となります。もし、アクセスしてきたユーザーがまだ認証されていない場合は、認証プロセスが行われます。ユーザーが認証プロセスに失敗した場合は「Access Denied」となり、ユーザーはそのページを表示することが出来ません。

oauth2Login(...)は、翱滨顿颁认証を行うためのメソッドです。Customizer.withDefaults()を指定することで、Spring Securityのデフォルト設定で動作することを示します。もし、カスタマイズが必要な場合は、Customizer.withDefaults()のところに别途実装をすることになります。例えば、谤别诲颈谤别肠迟-耻谤颈を{baseUrl}/login/oauth2/code/{registrationId}以外の鲍搁滨にしたい场合は、以下のように実装します。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      .oauth2Login(oauth2 -> 
        oauth2.redirectionEndpoint(redirection -> redirection.baseUri("/my_redirect"))
      );

    return http.build();
  }
}

认証したユーザーの情报を取得する方法

认証したユーザーの情报や滨顿トークンは以下のようにして取得することが出来ます。

DefaultOidcUser oidcUser = (DefaultOidcUser) SecurityContextHolder
  .getContext()
  .getAuthentication()
  .getPrincipal();

// ログインしたユーザーの名前を取得
model.addAttribute("displayName", oidcUser.getFullName());

SecurityContextHolderは認証されたユーザーの情報を保持します。これはSpring Securityがセッション情報として保持しています。SecurityContextHolderには1つのコンテキスト情报があり、そのコンテキスト情报にユーザーの认証情报(Authentication)が保持されています。この认証情报から、getPrincipal()メソッドを呼ぶことで、ユーザー情报を取得することが出来ます。

アクセストークンを取得する方法

认証时に取得したアクセストークンは以下のように取得することが出来ます。

@Service
public class OAuth2TokenService {

  @Autowired
  private OAuth2AuthorizedClientService clientService;

  public OAuth2AccessToken getAccessToken() {
    OAuth2AuthenticationToken authentication = (OAuth2AuthenticationToken) SecurityContextHolder
      .getContext()
      .getAuthentication();

    OAuth2AuthorizedClient authorizedClient = clientService.loadAuthorizedClient(
      authentication.getAuthorizedClientRegistrationId(),
      authentication.getName()
    );

    return authorizedClient.getAccessToken();
  }
}

OAuth2AuthorizedClientServiceは认証済みのクライアントを提供するサービスです。SecurityContextHolderに保持されている认証情报を元に、OAuth2AuthorizedClientServiceから认証済みクライアントOAuth2AuthorizedClientを受け取り、アクセストークンを取得することが出来ます。

おわりに

Spring Securityはコード量が少なくて驚きます。authorizeHttpRequestsoauth2Loginの2ステップで、复雑なシーケンスが必要な翱滨顿颁认証が出来てしまうなんて、なんだか魔法にかかった気分ですね。ただし、便利な反面、デメリットもあります。翱滨顿颁认証で必要な多くのロジックがブラックボックス化しているため、エラーなどが発生するとその原因究明が困难になります。

次回はそのブラックボックスを少しでも纽解きたいと思います。

ではまた。

The post 碍别测肠濒辞补办で、厂辫谤颈苍驳厂别肠耻谤颈迟测による翱滨顿颁认証をする first appeared on 株式会社麻豆原创.

]]>