OAuth2およびJWTREST保護にSpringBootを使用する
公開: 2022-03-11この記事は、JSON Web Token(JWT)のサーバー側実装をセットアップする方法に関するガイドです-SpringBootとMavenを使用したOAuth2認証フレームワーク。
OAuth2を最初に把握することをお勧めします。これは、上記のリンクされたドラフトを読んだり、このようなWebで役立つ情報を検索したりして入手できます。
OAuth2は、2006年に作成された最初のバージョンのOAuthに代わる承認フレームワークです。保護されたリソースにアクセスするために、クライアントと1つ以上のHTTPサービス間の承認フローを定義します。
OAuth2は、次のサーバー側の役割を定義します。
- リソース所有者:リソースのアクセスを制御する責任があるサービス
- リソースサーバー:実際にリソースを提供するサービス
- 承認サーバー:クライアントとリソース所有者の間の仲介者として機能するサービス処理承認プロセス
JSON Web Token(JWT)は、2つのパーティ間で転送されるクレームを表現するための仕様です。 クレームは、暗号化された構造のペイロードとして使用されるJSONオブジェクトとしてエンコードされ、クレームをデジタル署名または暗号化できるようにします。
含まれる構造は、JSON Web署名(JWS)またはJSON Web暗号化(JWE)です。
JWTは、OAuth2プロトコル内で使用されるアクセストークンと更新トークンの形式として選択できます。
OAuth2とJWTは、次の機能により、過去数年間で大きな人気を博しました。
- ステートレスRESTプロトコル用のステートレス認証システムを提供します
- 複数のリソースサーバーが単一の認証サーバーを共有できるマイクロサービスアーキテクチャにうまく適合します
- JSON形式により、クライアント側でトークンコンテンツを管理しやすい
ただし、プロジェクトで次の考慮事項が重要な場合、OAuth2とJWTが常に最良の選択であるとは限りません。
- ステートレスプロトコルは、サーバー側でのアクセスの取り消しを許可しません
- トークンの有効期間が固定されているため、セキュリティを損なうことなく長時間実行されるセッションを管理するための複雑さが増します(トークンの更新など)。
- クライアント側のトークンの安全なストアの要件
予想されるプロトコルフロー
OAuth2の主な機能の1つは、承認プロセスをリソース所有者から分離するための承認レイヤーの導入ですが、簡単にするために、記事の結果は、すべてのリソース所有者、承認サーバー、およびリソースサーバーの役割。 このため、通信はサーバーとクライアントの2つのエンティティ間でのみ流れます。
この単純化は、記事の目的、つまりSpringBootの環境でのそのようなシステムのセットアップに焦点を当てるのに役立つはずです。
簡略化されたフローを以下に説明します。
- 承認リクエストは、パスワード承認付与を使用してクライアントからサーバー(リソース所有者として機能)に送信されます
- アクセストークンが(更新トークンとともに)クライアントに返されます
- 次に、保護されたリソースアクセスの要求ごとに、アクセストークンがクライアントからサーバー(リソースサーバーとして機能)に送信されます。
- サーバーは必要な保護されたリソースで応答します
春のセキュリティと春のブート
まず、このプロジェクト用に選択されたテクノロジースタックの簡単な紹介。
選択するプロジェクト管理ツールはMavenですが、プロジェクトが単純であるため、Gradleなどの他のツールに切り替えるのは難しくありません。
この記事の続きでは、Spring Securityの側面のみに焦点を当てていますが、すべてのコードの抜粋は、完全に機能するサーバー側アプリケーションから取得されています。ソースコードは、RESTリソースを消費するクライアントとともにパブリックリポジトリで利用できます。
Spring Securityは、Springベースのアプリケーションにほぼ宣言型のセキュリティサービスを提供するフレームワークです。 そのルーツはSpringの最初の初めからのものであり、カバーされているさまざまなセキュリティテクノロジーの数が多いため、モジュールのセットとして編成されています。
Spring Securityアーキテクチャを簡単に見てみましょう(より詳細なガイドはここにあります)。
セキュリティは主に認証、つまりIDの検証、承認、リソースへのアクセス権の付与に関するものです。
Spring Securityは、サードパーティによって提供されるか、ネイティブに実装される、幅広い認証モデルをサポートします。 リストはここにあります。
認可に関しては、3つの主要な領域が特定されています。
- Webは承認を要求します
- メソッドレベルの承認
- ドメインオブジェクトインスタンスの承認へのアクセス
認証
基本的なインターフェースは、認証方法を提供するAuthenticationManager
です。 UserDetailsService
は、ユーザーの情報収集に関連するインターフェースであり、標準のJDBCまたはLDAPメソッドの場合、直接実装または内部で使用できます。
承認
メインインターフェイスはAccessDecisionManager
。 上記の3つの領域すべての実装は、 AccessDecisionVoter
のチェーンに委任されます。 後者のインターフェイスの各インスタンスは、 Authentication
(ユーザーID、プリンシパルという名前)、リソース、およびConfigAttribute
のコレクション間の関連付けを表します。これは、リソースの所有者がリソース自体へのアクセスを許可した方法を説明する一連のルールです。ユーザーロールの使用。
Webアプリケーションのセキュリティは、サーブレットフィルタのチェーンで上記の基本要素を使用して実装され、クラスWebSecurityConfigurerAdapter
は、リソースのアクセスルールを表現する宣言的な方法として公開されます。
メソッドのセキュリティは、最初に@EnableGlobalMethodSecurity(securedEnabled = true)
アノテーションが存在することで有効になり、次に、@ Secured、@ @Secured
、 @PreAuthorize
などの保護対象の各メソッドに適用する一連の特殊なアノテーションを使用して有効になり@PostAuthorize
。
Spring Bootは、高品質の標準を維持しながら開発を容易にするために、これらすべてに意見のあるアプリケーション構成とサードパーティライブラリのコレクションを追加します。
SpringBootを使用したJWTOAuth2
ここで、元の問題に移り、SpringBootを使用してOAuth2とJWTを実装するアプリケーションをセットアップしましょう。
Javaの世界には複数のサーバー側OAuth2ライブラリが存在しますが(リストはここにあります)、Springセキュリティアーキテクチャに十分に統合されているため、多くを処理する必要がないため、Springベースの実装が自然な選択です。その使用のための低レベルの詳細の。
すべてのセキュリティ関連のライブラリ依存関係は、Spring Bootの助けを借りてMavenによって処理されます。これは、Mavenの構成ファイルpom.xml内に明示的なバージョンを必要とする唯一のコンポーネントです(つまり、ライブラリバージョンは、Mavenが最新のものを選択することによって自動的に推測されます挿入されたSpringBootバージョンと互換性のあるバージョン)。
SpringBootセキュリティに関連する依存関係を含むmavenの構成ファイルpom.xmlからの抜粋を以下に示します。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.0.RELEASE</version> </dependency>
このアプリは、OAuth2認証サーバー/リソース所有者とリソースサーバーの両方として機能します。
保護されたリソース(リソースサーバーとして)は/ api /パスの下に公開され、認証パス(リソース所有者/承認サーバーとして)は提案されたデフォルトに従って/ oauth/tokenにマップされます。
アプリの構造:
-
security
構成を含むセキュリティパッケージ errors
処理を含むエラーパッケージusers
、モデル、リポジトリ、コントローラーなどのRESTリソースのglee
パッケージ
次の段落では、上記の3つのOAuth2ロールのそれぞれの構成について説明します。 関連するクラスはsecurity
パッケージ内にあります。
-
OAuthConfiguration
、AuthorizationServerConfigurerAdapter
を拡張します ResourceServerConfiguration
、ResourceServerConfigurerAdapterを拡張しResourceServerConfigurerAdapter
- ServerSecurityConfig、
ServerSecurityConfig
を拡張WebSecurityConfigurerAdapter
-
UserService
、UserDetailsService
の実装
リソース所有者と承認サーバーのセットアップ
承認サーバーの動作は、 @EnableAuthorizationServer
アノテーションの存在によって有効になります。 その構成は、リソース所有者の動作に関連する構成とマージされ、両方ともクラスAuthorizationServerConfigurerAdapter
に含まれています。
ここで適用される構成は、以下に関連しています。
- クライアントアクセス(
ClientDetailsServiceConfigurer
を使用)-
inMemory
またはjdbc
メソッドを使用したクライアントの詳細にin-memoryまたはJDBCベースのストレージを使用することの選択 clientId
およびclientSecret
(選択したPasswordEncoder
Beanでエンコードされた)属性を使用したクライアントの基本認証refreshTokenValiditySeconds
accessTokenValiditySeconds
を使用したトークンへのアクセスと更新の有効期間authorizedGrantTypes
属性を使用して許可される付与タイプscopes
メソッドを使用してアクセススコープを定義します- クライアントのアクセス可能なリソースを特定する
-
- 承認サーバーエンドポイント(
AuthorizationServerEndpointsConfigurer
を使用)-
accessTokenConverter
でJWTトークンの使用を定義します - (リソース所有者として)認証を実行するための
UserDetailsService
およびAuthenticationManager
インターフェースの使用を定義します
-
package net.reliqs.gleeometer.security; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; @Configuration @EnableAuthorizationServer public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter { private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; private final UserDetailsService userService; @Value("${jwt.clientId:glee-o-meter}") private String clientId; @Value("${jwt.client-secret:secret}") private String clientSecret; @Value("${jwt.signing-key:123}") private String jwtSigningKey; @Value("${jwt.accessTokenValidititySeconds:43200}") // 12 hours private int accessTokenValiditySeconds; @Value("${jwt.authorizedGrantTypes:password,authorization_code,refresh_token}") private String[] authorizedGrantTypes; @Value("${jwt.refreshTokenValiditySeconds:2592000}") // 30 days private int refreshTokenValiditySeconds; public OAuthConfiguration(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, UserDetailsService userService) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; this.userService = userService; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(clientId) .secret(passwordEncoder.encode(clientSecret)) .accessTokenValiditySeconds(accessTokenValiditySeconds) .refreshTokenValiditySeconds(refreshTokenValiditySeconds) .authorizedGrantTypes(authorizedGrantTypes) .scopes("read", "write") .resourceIds("api"); } @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) { endpoints .accessTokenConverter(accessTokenConverter()) .userDetailsService(userService) .authenticationManager(authenticationManager); } @Bean JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); return converter; } }
次のセクションでは、リソースサーバーに適用する構成について説明します。
リソースサーバーのセットアップ
リソースサーバーの動作は@EnableResourceServer
アノテーションを使用して有効にされ、その構成はクラスResourceServerConfiguration
に含まれています。
ここで必要な構成は、前のクラスで定義されたクライアントのアクセスと一致させるためのリソースIDの定義だけです。
package net.reliqs.gleeometer.security; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId("api"); } }
最後の構成要素は、Webアプリケーションのセキュリティの定義に関するものです。

Webセキュリティの設定
Spring Webセキュリティ構成はクラスServerSecurityConfig
に含まれており、 @EnableWebSecurity
アノテーションを使用して有効になります。 @EnableGlobalMethodSecurity
は、メソッドレベルでセキュリティを指定することを許可します。 コントローラは通常クラスであり、インターフェイスを実装していないため、その属性proxyTargetClass
は、これをRestController
のメソッドで機能させるために設定されます。
それは以下を定義します:
- 使用する認証プロバイダー、Bean
authenticationProvider
プロバイダーの定義 - 使用するパスワードエンコーダー、Beanの
passwordEncoder
エンコーダーの定義 - 認証マネージャーBean
-
HttpSecurity
を使用した公開パスのセキュリティ構成 - 標準のSpringRESTエラーハンドラー
ResponseEntityExceptionHandler
の外部でエラーメッセージを処理するためのカスタムAuthenticationEntryPoint
の使用
package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler; import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final UserDetailsService userDetailsService; public ServerSecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint, @Qualifier("userService") UserDetailsService userDetailsService) { this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; this.userDetailsService = userDetailsService; } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(userDetailsService); return provider; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/signin/**").permitAll() .antMatchers("/api/glee/**").hasAnyAuthority("ADMIN", "USER") .antMatchers("/api/users/**").hasAuthority("ADMIN") .antMatchers("/api/**").authenticated() .anyRequest().authenticated() .and().exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(new CustomAccessDeniedHandler()); } }
以下のコード抽出は、リソース所有者の認証を提供するためのUserDetailsService
インターフェースの実装に関するものです。
package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.users.User; import net.reliqs.gleeometer.users.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.Service; @Service public class UserService implements UserDetailsService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException("User not found: " + username)); GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name()); return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), Arrays.asList(authority)); } }
次のセクションでは、セキュリティ制約がどのようにマッピングされるかを確認するためのRESTコントローラーの実装について説明します。
RESTコントローラー
RESTコントローラー内では、各リソースメソッドにアクセス制御を適用する2つの方法を見つけることができます。
- Springから渡された
OAuth2Authentication
のインスタンスをパラメーターとして使用する @PreAuthorize
または@PostAuthorize
アノテーションの使用
package net.reliqs.gleeometer.users; import lombok.extern.slf4j.Slf4j; import net.reliqs.gleeometer.errors.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolationException; import javax.validation.Valid; import javax.validation.constraints.Size; import java.util.HashSet; @RestController @RequestMapping("/api/users") @Slf4j @Validated class UserController { private final UserRepository repository; private final PasswordEncoder passwordEncoder; UserController(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; this.passwordEncoder = passwordEncoder; } @GetMapping Page<User> all(@PageableDefault(size = Integer.MAX_VALUE) Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmail(auth, pageable); } return repository.findAll(pageable); } @GetMapping("/search") Page<User> search(@RequestParam String email, Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmailContainsAndEmail(email, auth, pageable); } return repository.findByEmailContains(email, pageable); } @GetMapping("/findByEmail") @PreAuthorize("!hasAuthority('USER') || (authentication.principal == #email)") User findByEmail(@RequestParam String email, OAuth2Authentication authentication) { return repository.findByEmail(email).orElseThrow(() -> new EntityNotFoundException(User.class, "email", email)); } @GetMapping("/{id}") @PostAuthorize("!hasAuthority('USER') || (returnObject != null && returnObject.email == authentication.principal)") User one(@PathVariable Long id) { return repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString())); } @PutMapping("/{id}") @PreAuthorize("!hasAuthority('USER') || (authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)") void update(@PathVariable Long id, @Valid @RequestBody User res) { User u = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString())); res.setPassword(u.getPassword()); res.setGlee(u.getGlee()); repository.save(res); } @PostMapping @PreAuthorize("!hasAuthority('USER')") User create(@Valid @RequestBody User res) { return repository.save(res); } @DeleteMapping("/{id}") @PreAuthorize("!hasAuthority('USER')") void delete(@PathVariable Long id) { if (repository.existsById(id)) { repository.deleteById(id); } else { throw new EntityNotFoundException(User.class, "id", id.toString()); } } @PutMapping("/{id}/changePassword") @PreAuthorize("!hasAuthority('USER') || (#oldPassword != null && !#oldPassword.isEmpty() && authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)") void changePassword(@PathVariable Long id, @RequestParam(required = false) String oldPassword, @Valid @Size(min = 3) @RequestParam String newPassword) { User user = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString())); if (oldPassword == null || oldPassword.isEmpty() || passwordEncoder.matches(oldPassword, user.getPassword())) { user.setPassword(passwordEncoder.encode(newPassword)); repository.save(user); } else { throw new ConstraintViolationException("old password doesn't match", new HashSet<>()); } } }
結論
SpringSecurityとSpringBootは、ほぼ宣言的な方法で完全なOAuth2認証/認証サーバーをすばやくセットアップすることを許可します。 このチュートリアルで説明されているように、 application.properties/yml
/ ymlファイルから直接OAuth2クライアントのプロパティを構成することで、セットアップをさらに短縮できます。
すべてのソースコードは、次のGitHubリポジトリで入手できます:spring-glee-o-meter。 公開されたリソースを消費するAngularクライアントは、このGitHubリポジトリにあります:glee-o-meter。