ホームページ >Java >&#&チュートリアル >Spring Security と OAuth について
この記事では、Spring セキュリティについて調査し、OAuth 2.0 を使用した認証システムを構築します。
Spring Security の動作方法について詳しく説明する前に、Java ベースの Web サーバーの リクエスト処理ライフサイクル を理解することが重要です。 Spring Security はこのライフサイクルにシームレスに統合され、受信リクエストを保護します。
Spring Security を使用して Spring ベースのアプリケーションで HTTP リクエストを処理するライフサイクルには、いくつかの段階が含まれており、それぞれがリクエストの処理、検証、セキュリティ保護において重要な役割を果たします。
クライアント (ブラウザ、モバイル アプリ、Postman などの API ツールなど) が HTTP リクエストをサーバーに送信すると、ライフサイクルが始まります。
例:
GET /api/admin/dashboard HTTP/1.1
サーブレット コンテナ (例: Tomcat) はリクエストを受信し、それを Spring アプリケーションのフロント コントローラーである DispatcherServlet に委任します。ここからアプリケーションの処理パイプラインが開始されます。
DispatcherServlet がリクエストを処理する前に、Spring Security のフィルター チェーン がリクエストをインターセプトします。フィルター チェーンは一連のフィルターであり、それぞれが特定のセキュリティ タスクの処理を担当します。これらのフィルターは、リクエストがアプリケーション ロジックに到達する前に認証および認可の要件を満たしていることを確認します。
認証フィルター:
これらのフィルターは、ユーザー名/パスワード、JWT、セッション Cookie などの有効な認証情報がリクエストに含まれているかどうかを検証します。
認可フィルター:
認証後、これらのフィルターは、認証されたユーザーが、要求されたリソースにアクセスするために必要なロールまたは権限を持っていることを確認します。
その他のフィルター:
* **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 は Authentication オブジェクトを作成し、それを SecurityContext に保存します。このオブジェクトはスレッドローカル ストレージに保存されることが多く、リクエストのライフサイクル全体を通じてアクセスできます。
プリンシパル: 認証されたユーザー (ユーザー名など) を表します。
資格情報: JWT トークンやパスワードなどの認証の詳細が含まれます。
権限: ユーザーに割り当てられたロールと権限が含まれます。
リクエストは認証フィルターを通過します。
資格情報が有効な場合、Authentication オブジェクトが作成され、SecurityContext に追加されます。
認証情報が無効な場合、ExceptionTranslationFilter はクライアントに 401 Unauthorized 応答を送信します。
リクエストが Spring Security フィルター チェーンを正常に通過すると、DispatcherServlet が引き継ぎます。
ハンドラーマッピング:
URL と HTTP メソッドに基づいて、受信リクエストを適切なコントローラー メソッドにマッピングします。
コントローラー呼び出し:
マップされたコントローラーはリクエストを処理し、適切なレスポンスを返します。多くの場合、サービスやリポジトリなどの他の Spring コンポーネントの助けを借ります。
Spring Security はフィルターを通じてこのライフサイクルに統合され、初期段階でリクエストをインターセプトします。リクエストがアプリケーション ロジックに到達するまでに、リクエストはすでに認証および許可されており、正当なトラフィックのみがコア アプリケーションによって処理されることが保証されます。
Spring Security の設計では、認証、認可、その他のセキュリティ対策が宣言的に処理されることを保証し、開発者が必要に応じて動作をカスタマイズまたは拡張できる柔軟性を提供します。これは、ベスト プラクティスを強制するだけでなく、最新のアプリケーションにおける複雑なセキュリティ要件の実装を簡素化します。
Spring Security の フィルター チェーン について説明しました。次に、認証と認可のプロセスで重要な役割を果たす他の重要なコンポーネントについて詳しく説明します。
AuthenticationManager は、ユーザーの資格情報を検証し、それらが有効かどうかを判断するために使用される単一のメソッド、authenticate(Authentication 認証) を定義するインターフェースです。 AuthenticationManager は、複数のプロバイダを登録できるコーディネーターと考えることができ、リクエストの種類に基づいて、認証リクエストを正しいプロバイダに配信します。
AuthenticationProvider は、資格情報に基づいてユーザーを認証するためのコントラクトを定義するインターフェイスです。これは、ユーザー名/パスワード、OAuth、LDAP などの特定の認証メカニズムを表します。複数の AuthenticationProvider 実装を共存させることができるため、アプリケーションはさまざまな認証戦略をサポートできます。
認証オブジェクト:
AuthenticationProvider は、ユーザーの資格情報 (ユーザー名やパスワードなど) をカプセル化する Authentication オブジェクトを処理します。
認証メソッド:
各 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.
データベースを利用した AuthenticationProvider がユーザー名とパスワードを検証します。
OAuth ベースの AuthenticationProvider は、外部 ID プロバイダーによって発行されたトークンを検証します。
UserDetailsService は、Spring ドキュメントでユーザー固有のデータをロードするコア インターフェイスとして説明されています。これには、パラメーターとしてユーザー名を受け取り、 ==User== アイデンティティ オブジェクトを返す単一のメソッド loadUserByUsername が含まれています。基本的には、loadUserByUsername メソッドをオーバーライドする UserDetailsService のクラスを作成して実装します。
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
これら 3 つがどのように連携するかというと、AuthenticationManager は AuthenticationProvider に、指定された Provider の種類に従って認証を実行するよう依頼し、UserDetailsService 実装は AuthenticationProvider が userdetails を証明するのを支援します。
ユーザーは、認証情報 (ユーザー名とパスワード) または JWT トークン (ヘッダー内) を使用して認証されたエンドポイントにリクエストを送信し、リクエストは認証フィルターに渡されます
AuthenticationFilter (例: UsernamePasswordAuthenticationFilter):
このカスタム フィルターは 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 を使用して生成されます。
* Validates the user’s credentials. * Returns an authenticated `Authentication` object upon success. * Throws an `AuthenticationException` if authentication fails.
UserDetailsService: AuthenticationProvider は UserDetailsService を使用して、ユーザー名に基づいてユーザーの詳細を読み込みます。そして、これを UserDetailsService
資格情報の検証: 提供されたパスワードとユーザーの詳細に保存されているパスワードを比較します (通常は 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 は、ユーザーが他のプラットフォーム (Google Drive や Github など) に保存されているリソースへのアクセスをサードパーティのアプリケーションに、それらのプラットフォームの資格情報を共有せずに許可できるようにする認証用に作成されたプロトコルです。
主に、「Google でログイン」、「github でログイン」などのソーシャル ログインを有効にするために使用されます。
Google、Facebook、Github などのプラットフォームは、このソーシャル サインインまたはアクセスの承認のために OAuth 2.0 プロトコルを実装する承認サーバーを提供します。
次に、各コンセプトを 1 つずつ見ていきます
リソース所有者は、サードパーティ アプリケーション (あなたのアプリケーション) を承認したいユーザーです。
これは、リソース サーバーからデータまたはリソースにアクセスしようとしている (サードパーティの) アプリケーションです。
これはユーザーのデータが保存されるサーバーであり、サードパーティのアプリケーションによってアクセスされます。
リソース所有者を認証し、クライアント (Google アカウントなど) にアクセス トークンを発行するサーバー。
認可サーバーによってクライアントに発行される資格情報。これにより、クライアントはユーザーに代わってリソース サーバーにアクセスできるようになります。通常、有効期間は短く、すぐに期限切れになるため、ユーザーが再度認証する必要がないように、このアクセス トークンを更新するためのリフレッシュ トークンも提供されます。
ユーザーによって付与される特定の権限。クライアントがユーザーのデータに対してできることとできないことを定義します。たとえば、承認の場合は、プロフィール、名前などのユーザー情報のみが必要ですが、ファイルアクセスの場合は別のスコープが必要です。
クライアント アプリケーションが認可サーバーからアクセス トークンを取得できる方法を指します。許可は、クライアント アプリケーションがリソース所有者の保護されたデータへのアクセスを許可されるプロセスと条件を定義します。
クライアント シークレットやその他の認証情報をブラウザに公開する必要がないため、安全です
OAuth 2.0 によって提供される、主に使用される 2 つの許可タイプがあります
これは最も使用されているタイプの許可/メソッドであり、最も安全であり、サーバー側アプリケーション用です
この例では、クライアントからバックエンドに認可コードが与えられ、バックエンドはクライアントにアクセス トークンを与えます。
プロセス:
シングルページ アプリ (SPA) またはバックエンドのないアプリケーションによって使用されます。この場合、アクセス トークンはブラウザ自体で直接生成され、発行されます。
プロセス:
完全に理解するために両方を個別に実装しますが、最初に必要となる認可コードグラントを実装します
認可サーバー
プラットフォーム (google や github など) のいずれかにすることも、KeyCloak を使用して独自のプラットフォームを作成することも、OAuth 2.0 標準に準拠した独自のプラットフォームを構築することもできます (これは次のブログで行うかも知れません?)
Spring Boot アプリケーション
これは、コード交換、検証、ユーザー詳細の保存、JWT トークンの割り当てなどのすべての操作を処理するメインのバックエンド アプリケーション/サービスになります
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 サイトの他の関連記事を参照してください。