この記事は、サーバー側の実装をセットアップする方法に関するガイドです。 JSON Web Token(JWT) - OAuth2 を使用した承認フレームワーク スプリングブーツ そして Maven 。
OAuth2を最初に把握することをお勧めします。上記のドラフトを読んだり、次のようなWebで役立つ情報を検索したりして入手できます。 この または この 。
OAuth2は、2006年に作成された最初のバージョンのOAuthに代わる承認フレームワークです。保護されたリソースにアクセスするために、クライアントと1つ以上のHTTPサービス間の承認フローを定義します。
OAuth2は、次のサーバー側の役割を定義します。
JSON Web Token(JWT)は、2者間で転送されるクレームの表現の仕様です。クレームは、暗号化された構造のペイロードとして使用されるJSONオブジェクトとしてエンコードされ、クレームをデジタル署名または暗号化できるようにします。
ノードjsの使い方
含まれる構造は、JSON Web署名(JWS)またはJSON Web暗号化(JWE)です。
JWTは、OAuth2プロトコル内で使用されるアクセストークンと更新トークンの形式として選択できます。
OAuth2とJWTは、次の機能により、過去数年間で大きな人気を博しました。
ただし、プロジェクトで次の考慮事項が重要な場合は、OAuth2とJWTが常に最良の選択であるとは限りません。
OAuth2の主な機能の1つは、承認プロセスをリソース所有者から分離するための承認レイヤーの導入ですが、簡単にするために、記事の結果は、すべてを偽装する単一のアプリケーションの構築です。 リソース所有者 、 承認サーバー 、および リソースサーバー 役割。このため、通信はサーバーとクライアントの2つのエンティティ間でのみ流れます。
この単純化は、記事の目的、つまりSpringBootの環境でのそのようなシステムのセットアップに焦点を当てるのに役立つはずです。
簡略化されたフローを以下に説明します。
まず、このプロジェクト用に選択されたテクノロジースタックの簡単な紹介。
クレジットカードのハッキングにお金を追加する
選択したプロジェクト管理ツールは Maven 、ただし、プロジェクトは単純であるため、次のような他のツールに切り替えることは難しくありません。 Gradle 。
この記事の続きでは、Spring Securityの側面のみに焦点を当てていますが、すべてのコードの抜粋は、完全に機能するサーバー側アプリケーションから取得されています。ソースコードは、RESTリソースを消費するクライアントとともにパブリックリポジトリで利用できます。
Spring Securityは、Springベースのアプリケーションにほぼ宣言型のセキュリティサービスを提供するフレームワークです。そのルーツはSpringの最初の初めからのものであり、カバーされているさまざまなセキュリティテクノロジーの数が多いため、モジュールのセットとして編成されています。
Spring Securityアーキテクチャを簡単に見てみましょう(より詳細なガイドがあります)。 ここに )。
セキュリティは主に約 認証 、つまり本人確認、および 承認 、リソースへのアクセス権の付与。
Spring Securityは、サードパーティによって提供されるか、ネイティブに実装される、幅広い認証モデルをサポートします。リストを見つけることができます ここに 。
認可に関しては、3つの主要な領域が特定されています。
基本的なインターフェースはAuthenticationManager
です認証方法を提供する責任があります。 UserDetailsService
は、ユーザーの情報収集に関連するインターフェースであり、標準のJDBCまたはLDAPメソッドの場合、直接実装または内部で使用できます。
メインインターフェイスはAccessDecisionManager
です。上記の3つの領域すべての実装は、AccessDecisionVoter
のチェーンに委任されます。後者のインターフェースの各インスタンスは、Authentication
間の関連付けを表します。 (ユーザーID、プリンシパルという名前)、リソース、およびConfigAttribute
のコレクション、リソースの所有者が、おそらくユーザーロールを使用して、リソース自体へのアクセスを許可する方法を説明する一連のルール。
Webアプリケーションのセキュリティは、サーブレットフィルタのチェーンで上記の基本要素とクラスWebSecurityConfigurerAdapter
を使用して実装されます。リソースのアクセスルールを表現する宣言的な方法として公開されています。
Python関数は、通常の状況では、その値についてモジュール変数を参照できません。
メソッドのセキュリティは、@EnableGlobalMethodSecurity(securedEnabled = true)
の存在によって最初に有効になります。次に、@Secured
、@PreAuthorize
、@PostAuthorize
などの保護対象の各メソッドに適用する一連の特殊な注釈を使用します。
Spring Bootは、高品質の標準を維持しながら開発を容易にするために、これらすべてに意見のあるアプリケーション構成とサードパーティライブラリのコレクションを追加します。
ここで、元の問題に移り、SpringBootでOAuth2とJWTを実装するアプリケーションをセットアップしましょう。
Javaの世界には複数のサーバー側OAuth2ライブラリが存在します(リストは見つかります) ここに )、Springベースの実装は、Spring Securityアーキテクチャに十分に統合されているため、使用するために低レベルの詳細の多くを処理する必要がないため、当然の選択です。
すべてのセキュリティ関連のライブラリ依存関係は、Spring Bootの助けを借りてMavenによって処理されます。これは、Mavenの構成ファイル内に明示的なバージョンを必要とする唯一のコンポーネントです。 pom.xml (つまり、ライブラリのバージョンは、挿入されたSpring Bootバージョンと互換性のある最新バージョンを選択するMavenによって自動的に推測されます)。
Mavenの構成ファイルからの抜粋を以下に示します pom.xml Spring Bootセキュリティに関連する依存関係を含む:
org.springframework.boot spring-boot-starter-security org.springframework.security.oauth.boot spring-security-oauth2-autoconfigure 2.1.0.RELEASE
アプリは、OAuth2認証サーバー/リソース所有者とリソースサーバーの両方として機能します。
需要の価格弾力性はどのように測定されますか?
保護されたリソース(リソースサーバーとして)は、 /火/ パス、認証パス(リソース所有者/承認サーバーとして)はにマップされます / oauth / token 、提案されたデフォルトに従います。
アプリの構造:
security
セキュリティ構成を含むパッケージerrors
エラー処理を含むパッケージusers
、glee
モデル、リポジトリ、コントローラーなどのRESTリソースのパッケージ次の段落では、上記の3つのOAuth2ロールのそれぞれの構成について説明します。関連するクラスはsecurity
内にありますパッケージ:
OAuthConfiguration
、拡張AuthorizationServerConfigurerAdapter
ResourceServerConfiguration
、拡張 ResourceServerConfigurerAdapter
ServerSecurityConfig
、拡張 WebSecurityConfigurerAdapter
UserService
、実装 UserDetailsService
承認サーバーの動作は、@EnableAuthorizationServer
の存在によって有効になります。注釈。その構成は、リソース所有者の動作に関連する構成とマージされ、両方ともクラスAuthorizationServerConfigurerAdapter
に含まれています。
ここで適用される構成は、以下に関連しています。
ClientDetailsServiceConfigurer
を使用)inMemory
を使用して、クライアントの詳細にインメモリまたはJDBCベースのストレージを使用することを選択します。またはjdbc
メソッドclientId
を使用したクライアントの基本認証およびclientSecret
(選択したPasswordEncoder
Beanでエンコード)属性accessTokenValiditySeconds
を使用したトークンへのアクセスと更新の有効期間およびrefreshTokenValiditySeconds
属性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アプリケーションのセキュリティの定義に関するものです。
SpringWebセキュリティ構成はクラスServerSecurityConfig
に含まれており、@EnableWebSecurity
を使用して有効になります。注釈。 @EnableGlobalMethodSecurity
メソッドレベルでセキュリティを指定することを許可します。その属性proxyTargetClass
コントローラは通常クラスであり、インターフェイスを実装していないため、これをRestController
のメソッドで機能させるために設定されます。
それは以下を定義します:
authenticationProvider
passwordEncoder
HttpSecurity
を使用した公開パスのセキュリティ構成AuthenticationEntryPoint
標準のSpringRESTエラーハンドラーの外部でエラーメッセージを処理するためResponseEntityExceptionHandler
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コントローラー内では、各リソースメソッドにアクセス制御を適用する2つの方法を見つけることができます。
OAuth2Authentication
のインスタンスを使用するSpringからパラメータとして渡される@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 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 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
から直接OAuth2クライアントのプロパティを構成することで、セットアップをさらに短縮できます。これで説明されているように、ファイル チュートリアル 。
すべてのソースコードは、次のGitHubリポジトリで入手できます。 スプリンググリーオメーター 。公開されたリソースを消費するAngularクライアントは、次のGitHubリポジトリにあります。 グリーオメーター 。
良いコードの書き方
OAuth2は、サードパーティアプリケーションがアクセストークンの共有を通じてHTTPサービスへの制限付きアクセスを取得できるようにする承認フレームワークです。その仕様は、OAuth 1.0プロトコルに取って代わり、廃止されます。
JWTはJSONWeb Tokenの略で、2者間で転送されるクレームの表現の仕様です。クレームは、暗号化された構造のペイロードとして使用されるJSONオブジェクトとしてエンコードされます。これにより、クレームをデジタル署名または暗号化できます。
Spring Securityは、Springベースのアプリケーションに認証と承認を提供することに焦点を当てたフレームワークです。
Spring Bootは、Springプラットフォームとサードパーティライブラリの意見をまとめたものであり、本番グレードの品質レベルを維持しながら、Springベースのアプリケーションの構成を最小限に抑えることができます。