ホームページ >Java >&#&チュートリアル >Spring Security と OAuth について

Spring Security と OAuth について

Susan Sarandon
Susan Sarandonオリジナル
2025-01-14 16:05:46690ブラウズ

この記事では、Spring セキュリティについて調査し、OAuth 2.0 を使用した認証システムを構築します。

Spring Security は、Java ベースのアプリケーションに堅牢な認証およびアクセス制御メカニズムを実装するための、強力で高度にカスタマイズ可能なフレームワークです。これは Spring エコシステムの中核コンポーネントであり、Web アプリケーション、REST API、およびその他のバックエンド サービスを保護するために広く使用されています。 Spring Security を使用すると、アプリケーションで安全なプラクティスを構築および適用するための強固な基盤が得られます。


Spring Security の仕組み

Spring Security の動作方法について詳しく説明する前に、Java ベースの Web サーバーの リクエスト処理ライフサイクル を理解することが重要です。 Spring Security はこのライフサイクルにシームレスに統合され、受信リクエストを保護します。


Spring Security によるリクエスト処理ライフサイクル

Spring Security を使用して Spring ベースのアプリケーションで HTTP リクエストを処理するライフサイクルには、いくつかの段階が含まれており、それぞれがリクエストの処理、検証、セキュリティ保護において重要な役割を果たします。


1. クライアントリクエスト

クライアント (ブラウザ、モバイル アプリ、Postman などの API ツールなど) が HTTP リクエストをサーバーに送信すると、ライフサイクルが始まります。

例:

GET /api/admin/dashboard HTTP/1.1


2. サーブレットコンテナ

サーブレット コンテナ (例: Tomcat) はリクエストを受信し、それを Spring アプリケーションのフロント コントローラーである DispatcherServlet に委任します。ここからアプリケーションの処理パイプラインが開始されます。


3. Spring セキュリティ フィルター チェーン

DispatcherServlet がリクエストを処理する前に、Spring Security のフィルター チェーン がリクエストをインターセプトします。フィルター チェーンは一連のフィルターであり、それぞれが特定のセキュリティ タスクの処理を担当します。これらのフィルターは、リクエストがアプリケーション ロジックに到達する前に認証および認可の要件を満たしていることを確認します。

チェーン内のキーフィルター:

  1. 認証フィルター:

    これらのフィルターは、ユーザー名/パスワード、JWT、セッション Cookie などの有効な認証情報がリクエストに含まれているかどうかを検証します。

  2. 認可フィルター:

    認証後、これらのフィルターは、認証されたユーザーが、要求されたリソースにアクセスするために必要なロールまたは権限を持っていることを確認します。

  3. その他のフィルター:

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

4. セキュリティコンテキスト

認証が成功すると、Spring Security は Authentication オブジェクトを作成し、それを SecurityContext に保存します。このオブジェクトはスレッドローカル ストレージに保存されることが多く、リクエストのライフサイクル全体を通じてアクセスできます。

認証オブジェクト:

  • プリンシパル: 認証されたユーザー (ユーザー名など) を表します。

  • 資格情報: JWT トークンやパスワードなどの認証の詳細が含まれます。

  • 権限: ユーザーに割り当てられたロールと権限が含まれます。

フィルター チェーン内のフローの例:

  • リクエストは認証フィルターを通過します。

  • 資格情報が有効な場合、Authentication オブジェクトが作成され、SecurityContext に追加されます。

  • 認証情報が無効な場合、ExceptionTranslationFilter はクライアントに 401 Unauthorized 応答を送信します。


5. DispatcherServlet

リクエストが Spring Security フィルター チェーンを正常に通過すると、DispatcherServlet が引き継ぎます。

  1. ハンドラーマッピング:

    URL と HTTP メソッドに基づいて、受信リクエストを適切なコントローラー メソッドにマッピングします。

  2. コントローラー呼び出し:

    マップされたコントローラーはリクエストを処理し、適切なレスポンスを返します。多くの場合、サービスやリポジトリなどの他の Spring コンポーネントの助けを借ります。

Spring Security がライフサイクルにどのように適合するか

Spring Security はフィルターを通じてこのライフサイクルに統合され、初期段階でリクエストをインターセプトします。リクエストがアプリケーション ロジックに到達するまでに、リクエストはすでに認証および許可されており、正当なトラフィックのみがコア アプリケーションによって処理されることが保証されます。


Spring Security の設計では、認証、認可、その他のセキュリティ対策が宣言的に処理されることを保証し、開発者が必要に応じて動作をカスタマイズまたは拡張できる柔軟性を提供します。これは、ベスト プラクティスを強制するだけでなく、最新のアプリケーションにおける複雑なセキュリティ要件の実装を簡素化します。

Understanding Spring Security and OAuth

Spring Security コンポーネント: フィルター チェーンを超えて

Spring Security の フィルター チェーン について説明しました。次に、認証と認可のプロセスで重要な役割を果たす他の重要なコンポーネントについて詳しく説明します。

認証マネージャー

AuthenticationManager は、ユーザーの資格情報を検証し、それらが有効かどうかを判断するために使用される単一のメソッド、authenticate(Authentication 認証) を定義するインターフェースです。 AuthenticationManager は、複数のプロバイダを登録できるコーディネーターと考えることができ、リクエストの種類に基づいて、認証リクエストを正しいプロバイダに配信します。

認証プロバイダー

AuthenticationProvider は、資格情報に基づいてユーザーを認証するためのコントラクトを定義するインターフェイスです。これは、ユーザー名/パスワード、OAuth、LDAP などの特定の認証メカニズムを表します。複数の AuthenticationProvider 実装を共存させることができるため、アプリケーションはさまざまな認証戦略をサポートできます。

コアコンセプト:

  1. 認証オブジェクト:

    AuthenticationProvider は、ユーザーの資格情報 (ユーザー名やパスワードなど) をカプセル化する Authentication オブジェクトを処理します。

  2. 認証メソッド:

    各 AuthenticationProvider は、実際の認証ロジックが存在する、authenticate(Authentication 認証) メソッドを実装します。このメソッド:

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
  1. メソッドをサポートします: support(Class>authentication) メソッドは、AuthenticationProvider が指定されたタイプの認証を処理できるかどうかを示します。これにより、Spring Security は特定の認証リクエストを処理するための正しいプロバイダーを決定できるようになります。

:

  • データベースを利用した AuthenticationProvider がユーザー名とパスワードを検証します。

  • OAuth ベースの AuthenticationProvider は、外部 ID プロバイダーによって発行されたトークンを検証します。

ユーザー詳細サービス

UserDetailsS​​ervice は、Spring ドキュメントでユーザー固有のデータをロードするコア インターフェイスとして説明されています。これには、パラメーターとしてユーザー名を受け取り、 ==User== アイデンティティ オブジェクトを返す単一のメソッド loadUserByUsername が含まれています。基本的には、loadUserByUsername メソッドをオーバーライドする UserDetailsS​​ervice のクラスを作成して実装します。

* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.

これら 3 つがどのように連携するかというと、AuthenticationManager は AuthenticationProvider に、指定された Provider の種類に従って認証を実行するよう依頼し、UserDetailsS​​ervice 実装は AuthenticationProvider が userdetails を証明するのを支援します。

設定などに進む前に、JWT ベースの認証のための Spring Security の簡潔なフローを次に示します。

1. ユーザーリクエスト

  • ユーザーは、認証情報 (ユーザー名とパスワード) または JWT トークン (ヘッダー内) を使用して認証されたエンドポイントにリクエストを送信し、リクエストは認証フィルターに渡されます

  • AuthenticationFilter (例: UsernamePasswordAuthenticationFilter):

    • 送信された資格情報 (通常はユーザー名とパスワードの形式) に基づいてユーザー認証を処理します。ここで UsernamePasswordAuthenticationFilter が活躍します。
    • リクエストをリッスンし、ユーザー名とパスワードを抽出して、AuthenticationManager に渡します。
    • しかし、ユーザー名とパスワードは渡しません。トークンだけを渡します。そのため、この AuthenticationFilter の前に、ユーザーが認証されており、ユーザー名とパスワードをチェックする必要がないことを認証プロセスに伝えるフィルターが必要です。これは、JWTFilter
    • を作成することで行われます。

2.JWTフィルター

このカスタム フィルターは OncePerRequestFilter を拡張し、 UsernamePasswordAuthenticationFilter の前に配置され、リクエストからトークンを抽出して検証します。

トークンが有効な場合、UsernamePasswordAuthenticationToken を作成し、そのトークンをセキュリティ コンテキストに設定します。これにより、リクエストが認証されたことを Spring セキュリティに伝え、このリクエストが UsernamePasswordAuthenticationFilter に渡されると、UsernamePasswordAuthenticationToken を持っているためそのまま渡されます

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

UserDetails クラスを使用してユーザー名とパスワードを認証した後、トークンの代わりにユーザー名とパスワードを渡した場合、この UsernamePasswordAuthenticationToken は AuthenticationManager と AuthenticationProvider を使用して生成されます。

3. 認証マネージャー

  • AuthenticationManager: これは認証リクエストを受け取り、それを設定した適切な AuthenticationProvider に委任します。
* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.

4.認証プロバイダ

  • UserDetailsS​​ervice: AuthenticationProvider は UserDetailsS​​ervice を使用して、ユーザー名に基づいてユーザーの詳細を読み込みます。そして、これを UserDetailsS​​ervice

  • の実装で提供します。
  • 資格情報の検証: 提供されたパスワードとユーザーの詳細に保存されているパスワードを比較します (通常は PasswordEncoder を使用します)。

package com.oauth.backend.services;

import com.oauth.backend.entities.User;
import com.oauth.backend.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;


@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
    public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user==null){
            throw new UsernameNotFoundException(email);
        }
        return new UserDetailsImpl(user);
    }
}

Spring セキュリティが何をすべきかを認識できるように、これらすべてのさまざまなフィルターと Bean を構成する必要があるため、すべての構成を指定する構成クラスを作成します。

@Component
public class JWTFilter extends OncePerRequestFilter {

    private final JWTService jwtService;
    private final UserDetailsService userDetailsService;
    public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if(authHeader == null || !authHeader.startsWith("Bearer")) {
            filterChain.doFilter(request,response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userName = jwtService.extractUserName(jwt);

        Authentication authentication
                = SecurityContextHolder.getContext().getAuthentication();

        if(userName !=null  && authentication == null) {
            //Authenticate
            UserDetails userDetails
                    = userDetailsService.loadUserByUsername(userName);

            if(jwtService.isTokenValid(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }



}

これまで Spring Security を使用して認証を理解し、構成してきました。ここからはテストしてみます。

AuthController (ログインと登録を処理します) と ProductController (ダミーの保護されたコントローラー) の 2 つのコントローラーを備えたシンプルなアプリを作成します

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services;

import com.oauth.backend.entities.User;
import com.oauth.backend.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;


@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
    public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user==null){
            throw new UsernameNotFoundException(email);
        }
        return new UserDetailsImpl(user);
    }
}
@Component
public class JWTFilter extends OncePerRequestFilter {

    private final JWTService jwtService;
    private final UserDetailsService userDetailsService;
    public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if(authHeader == null || !authHeader.startsWith("Bearer")) {
            filterChain.doFilter(request,response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userName = jwtService.extractUserName(jwt);

        Authentication authentication
                = SecurityContextHolder.getContext().getAuthentication();

        if(userName !=null  && authentication == null) {
            //Authenticate
            UserDetails userDetails
                    = userDetailsService.loadUserByUsername(userName);

            if(jwtService.isTokenValid(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }



}
@Bean  
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{  
return config.getAuthenticationManager();  
}
@Bean  
public AuthenticationProvider authenticationProvider(){  
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();  
authenticationProvider.setUserDetailsService(userDetailsServiceImpl);  
authenticationProvider.setPasswordEncoder(passwordEncoder);  
return authenticationProvider;  
}

これまで、登録、ログイン、検証を実装してきましたが、Login With Google/Github 機能も追加したい場合は、OAuth2.0 を使用して実行できます

OAuth 2.0

OAuth 2.0 は、ユーザーが他のプラットフォーム (Google Drive や Github など) に保存されているリソースへのアクセスをサードパーティのアプリケーションに、それらのプラットフォームの資格情報を共有せずに許可できるようにする認証用に作成されたプロトコルです。

主に、「Google でログイン」、「github でログイン」などのソーシャル ログインを有効にするために使用されます。

Google、Facebook、Github などのプラットフォームは、このソーシャル サインインまたはアクセスの承認のために OAuth 2.0 プロトコルを実装する承認サーバーを提供します。

OAuth 2.0 の主要な概念

  • リソース所有者

  • クライアント

  • 認可サーバー

  • リソースサーバー

  • アクセストークン

  • スコープ

  • 助成金

次に、各コンセプトを 1 つずつ見ていきます

リソース所有者

リソース所有者は、サードパーティ アプリケーション (あなたのアプリケーション) を承認したいユーザーです。

クライアント

これは、リソース サーバーからデータまたはリソースにアクセスしようとしている (サードパーティの) アプリケーションです。

リソースサーバー

これはユーザーのデータが保存されるサーバーであり、サードパーティのアプリケーションによってアクセスされます。

認可サーバー

リソース所有者を認証し、クライアント (Google アカウントなど) にアクセス トークンを発行するサーバー。

アクセストークン

認可サーバーによってクライアントに発行される資格情報。これにより、クライアントはユーザーに代わってリソース サーバーにアクセスできるようになります。通常、有効期間は短く、すぐに期限切れになるため、ユーザーが再度認証する必要がないように、このアクセス トークンを更新するためのリフレッシュ トークンも提供されます。

スコープ

ユーザーによって付与される特定の権限。クライアントがユーザーのデータに対してできることとできないことを定義します。たとえば、承認の場合は、プロフィール、名前などのユーザー情報のみが必要ですが、ファイルアクセスの場合は別のスコープが必要です。

助成金

クライアント アプリケーションが認可サーバーからアクセス トークンを取得できる方法を指します。許可は、クライアント アプリケーションがリソース所有者の保護されたデータへのアクセスを許可されるプロセスと条件を定義します。

クライアント シークレットやその他の認証情報をブラウザに公開する必要がないため、安全です

OAuth 2.0 によって提供される、主に使用される 2 つの許可タイプがあります

  1. 認可コードグラント

    これは最も使用されているタイプの許可/メソッドであり、最も安全であり、サーバー側アプリケーション用です

    この例では、クライアントからバックエンドに認可コードが与えられ、バックエンドはクライアントにアクセス トークンを与えます。

    プロセス:

    1. クライアントはユーザーを認可サーバーにリダイレクトします。
    2. ユーザーはログインして同意します。
    3. 認可サーバーは認可コードを発行します。
    4. クライアントは、バックエンドと認証コードを交換してアクセス トークンを取得します。
  2. 暗黙的な許可

    シングルページ アプリ (SPA) またはバックエンドのないアプリケーションによって使用されます。この場合、アクセス トークンはブラウザ自体で直接生成され、発行されます。

    プロセス:

    1. クライアントはユーザーを認可サーバーにリダイレクトします。
    2. ユーザーはログインして同意します。
    3. 認可サーバーはアクセストークンを直接発行します。

完全に理解するために両方を個別に実装しますが、最初に必要となる認可コードグラントを実装します

  1. 認可サーバー

    プラットフォーム (google や github など) のいずれかにすることも、KeyCloak を使用して独自のプラットフォームを作成することも、OAuth 2.0 標準に準拠した独自のプラットフォームを構築することもできます (これは次のブログで行うかも知れません?)

  2. Spring Boot アプリケーション

    これは、コード交換、検証、ユーザー詳細の保存、JWT トークンの割り当てなどのすべての操作を処理するメインのバックエンド アプリケーション/サービスになります

  3. React アプリケーション (フロントエンド)

    これは、認可のためにユーザーを認可サーバーにリダイレクトするクライアントになります。

つまり、私たちの実装でやることは、フロントエンド(web/app)がバックエンドエンドポイントへのリダイレクトURIを使用してユーザーをGoogleログインにリダイレクトすることです。これによりさらに制御が行われます。これについては後で説明し、redirect_urlとともに説明します。また、アプリのクライアント ID も渡します。これらはすべてクエリ パラメーターで送信されます。

いいえ、ユーザーが Google に正常にログインすると、認証サーバー (Google の) はリクエストをバックエンド エンドポイントにリダイレクトします。そこで私たちが行うことは、認証サーバーと認証コードを交換して、アクセス トークンとリフレッシュ トークンを取得することです。必要に応じて認証を処理し、最後に、Cookie を含む応答をフロントエンドに送り返し、ダッシュボードまたは保護されたページにリダイレクトします。

ここでコードを調べますが、OAuth クライアントの Google コンソール ダッシュボードの承認されたリダイレクト URL にバックエンド エンドポイントの URL を必ず追加してください。

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

これで、これで問題なく動作します。テスト用に、コンテキストを持ち、ログインと登録の機能を知っているだけの単純なフロントエンド アプリケーションを作成できます。

ここまで読んでいただきありがとうございました。何かご提案がございましたら、コメント欄に書き込んでください

以上がSpring Security と OAuth についての詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。