JWT forRESTAPIを使用したSpringSecurity

公開: 2022-03-11

Springは、Javaエコシステムで信頼できるフレームワークと見なされており、広く使用されています。 Springをフレームワークと呼ぶことは、さまざまなフレームワークを網羅する包括的な用語であるため、もはや有効ではありません。 これらのフレームワークの1つは、強力でカスタマイズ可能な認証および承認フレームワークであるSpringSecurityです。 これは、Springベースのアプリケーションを保護するための事実上の標準と見なされています。

その人気にもかかわらず、シングルページアプリケーションに関しては、構成が単純で簡単ではないことを認めなければなりません。 その理由は、Webページのレンダリングがサーバー側で行われ、通信がセッションベースであるMVCアプリケーション指向のフレームワークとして始まったためだと思います。

バックエンドがJavaとSpringに基づいている場合は、認証/承認にSpring Securityを使用し、ステートレス通信用に構成するのが理にかなっています。 これがどのように行われるかを説明する記事はたくさんありますが、私にとっては、初めて設定するのはまだイライラし、複数のソースからの情報を読んで要約する必要がありました。 そのため、この記事を書くことにしました。ここでは、構成プロセス中に遭遇する可能性のある、必要なすべての微妙な詳細と失敗を要約してカバーしようとします。

用語の定義

技術的な詳細に飛び込む前に、私たち全員が同じ言語を話すことを確認するために、SpringSecurityのコンテキストで使用される用語を明示的に定義したいと思います。

これらは、対処する必要のある用語です。

  • 認証とは、提供された資格情報に基づいて、ユーザーのIDを確認するプロセスを指します。 一般的な例は、Webサイトにログインするときにユーザー名とパスワードを入力することです。 あなたはそれをあなたは誰ですか?という質問への答えと考えることができます。 。
  • 承認とは、ユーザーが正常に認証されたと仮定して、ユーザーが特定のアクションを実行したり、特定のデータを読み取ったりするための適切な権限を持っているかどうかを判断するプロセスを指します。 あなたはそれを質問への答えと考えることができますユーザーはこれをする/読むことができますか?
  • 原則は、現在認証されているユーザーを指します。
  • 付与された権限とは、認証されたユーザーの権限を指します。
  • ロールとは、認証されたユーザーの権限のグループを指します。

基本的なSpringアプリケーションの作成

Spring Securityフレームワークの構成に移る前に、基本的なSpringWebアプリケーションを作成しましょう。 このために、Spring Initializrを使用して、テンプレートプロジェクトを生成できます。 単純なWebアプリケーションの場合、SpringWebフレームワークの依存関係だけで十分です。

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

プロジェクトを作成したら、次のように単純なRESTコントローラーをプロジェクトに追加できます。

 @RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }

この後、プロジェクトをビルドして実行すると、Webブラウザーで次のURLにアクセスできます。

  • http://localhost:8080/hello/userは、文字列Hello User
  • http://localhost:8080/hello/admin Hello Admin 、文字列HelloAdminを返します。

これで、Spring Securityフレームワークをプロジェクトに追加できます。これを行うには、 pom.xmlファイルに次の依存関係を追加します。

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

他のSpringフレームワークの依存関係を追加しても、対応する構成を提供するまで、通常はアプリケーションにすぐに影響しませんが、Spring Securityは、すぐに影響するという点で異なり、通常、新しいユーザーを混乱させます。 追加した後、プロジェクトを再構築して実行し、結果を表示する代わりに前述のURLのいずれかにアクセスしようとすると、 http://localhost:8080/loginにリダイレクトされます。 Spring Securityフレームワークでは、すべてのURLに対してすぐに認証が必要になるため、これはデフォルトの動作です。

認証に合格するには、デフォルトのユーザー名userを使用して、コンソールで自動生成されたパスワードを検索します。

 Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

アプリケーションを再実行するたびにパスワードが変更されることに注意してください。 この動作を変更してパスワードを静的にしたい場合は、 application.propertiesファイルに次の構成を追加できます。

 spring.security.user.password=Test12345_

これで、ログインフォームにクレデンシャルを入力すると、URLにリダイレクトされ、正しい結果が表示されます。 すぐに使用できる認証プロセスはセッションベースであり、ログアウトする場合は、次のURLにアクセスできることに注意してください: http://localhost:8080/logout

このすぐに使用できる動作は、セッションベースの認証を使用する従来のMVC Webアプリケーションに役立つ場合がありますが、シングルページアプリケーションの場合、ほとんどのユースケースではクライアント側にあるため、通常は役に立ちません。レンダリングとJWTベースのステートレス認証。 この場合、Spring Securityフレームワークを大幅にカスタマイズする必要があります。これについては、この記事の残りの部分で行います。

例として、従来の書店Webアプリケーションを実装し、CRUD APIを提供して作成者と書籍を作成し、さらにユーザー管理と認証用のAPIを提供するバックエンドを作成します。

Springセキュリティアーキテクチャの概要

構成のカスタマイズを開始する前に、まず、SpringSecurity認証が舞台裏でどのように機能するかについて説明しましょう。

次の図は、フローを示し、認証要求がどのように処理されるかを示しています。

春のセキュリティアーキテクチャ

春のセキュリティアーキテクチャ

それでは、この図をコンポーネントに分解し、それぞれについて個別に説明しましょう。

スプリングセキュリティフィルターチェーン

Spring Securityフレームワークをアプリケーションに追加すると、すべての着信要求をインターセプトするフィルターチェーンが自動的に登録されます。 このチェーンはさまざまなフィルターで構成されており、それぞれが特定のユースケースを処理します。

例えば:

  • 構成に基づいて、要求されたURLがパブリックにアクセス可能かどうかを確認します。
  • セッションベースの認証の場合、ユーザーが現在のセッションですでに認証されているかどうかを確認します。
  • ユーザーが要求されたアクションを実行する権限を持っているかどうかなどを確認します。

私が言及したい重要な詳細の1つは、Spring Securityフィルターが最も低い順序で登録され、最初に呼び出されるフィルターであるということです。 一部のユースケースでは、カスタムフィルターをそれらの前に配置する場合は、それらの順序にパディングを追加する必要があります。 これは、次の構成で実行できます。

 spring.security.filter.order=10

この構成をapplication.propertiesファイルに追加すると、SpringSecurityフィルターの前に10個のカスタムフィルター用のスペースができます。

AuthenticationManager

AuthenticationManagerは、複数のプロバイダーを登録できるコーディネーターと考えることができ、要求の種類に基づいて、正しいプロバイダーに認証要求を配信します。

AuthenticationProvider

AuthenticationProviderは、特定のタイプの認証を処理します。 そのインターフェースは2つの機能のみを公開します。

  • authenticateは、リクエストで認証を実行します。
  • このプロバイダーが指定された認証タイプをサポートしているかどうかのチェックsupportsします。

サンプルプロジェクトで使用しているインターフェイスの重要な実装の1つは、 DaoAuthenticationProvider ​​erviceからユーザーの詳細を取得するUserDetailsServiceです。

UserDetailsS​​ervice

UserDetailsServiceは、Springのドキュメントでユーザー固有のデータをロードするコアインターフェイスとして説明されています。

ほとんどのユースケースでは、認証プロバイダーはデータベースからの資格情報に基づいてユーザーID情報を抽出し、検証を実行します。 このユースケースは非常に一般的であるため、Spring開発者は、単一の関数を公開する別個のインターフェースとしてそれを抽出することにしました。

  • loadUserByUsernameは、パラメーターとしてusernameを受け入れ、ユーザーIDオブジェクトを返します。

SpringSecurityでJWTを使用した認証

Spring Securityフレームワークの内部について説明した後、JWTトークンを使用したステートレス認証用にフレームワークを構成しましょう。

Spring Securityをカスタマイズするには、クラスパスに@EnableWebSecurityアノテーションが付けられた構成クラスが必要です。 また、カスタマイズプロセスを簡素化するために、フレームワークはWebSecurityConfigurerAdapterクラスを公開します。 このアダプターを拡張し、次のように両方の機能をオーバーライドします。

  1. 正しいプロバイダーで認証マネージャーを構成する
  2. Webセキュリティを構成します(パブリックURL、プライベートURL、承認など)
 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }

サンプルアプリケーションでは、ユーザーIDをMongoDBデータベースのusersコレクションに格納します。 これらのIDはUserエンティティによってマップされ、それらのCRUD操作はUserRepoリポジトリによって定義されます。

ここで、認証要求を受け入れるときに、提供された資格情報を使用してデータベースから正しいIDを取得し、それを検証する必要があります。 このためには、次のように定義されているUserDetailsServiceインターフェースの実装が必要です。

 public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

ここでは、 UserDetailsインターフェイスを実装するオブジェクトを返す必要があり、 Userエンティティがそれを実装していることがわかります(実装の詳細については、サンプルプロジェクトのリポジトリを参照してください)。 単一関数のプロトタイプのみを公開するという事実を考慮すると、それを関数インターフェースとして扱い、ラムダ式として実装を提供できます。

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }

ここで、 auth.userDetailsService関数呼び出しは、 UserDetailsServiceインターフェースの実装を使用してDaoAuthenticationProviderインスタンスを開始し、それを認証マネージャーに登録します。

認証プロバイダーとともに、資格情報の検証に使用される正しいパスワードエンコードスキーマを使用して認証マネージャーを構成する必要があります。 このために、 PasswordEncoderインターフェースの推奨される実装をBeanとして公開する必要があります。

サンプルプロジェクトでは、bcryptパスワードハッシュアルゴリズムを使用します。

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }

認証マネージャーを構成したら、Webセキュリティを構成する必要があります。 REST APIを実装しており、JWTトークンを使用したステートレス認証が必要です。 したがって、次のオプションを設定する必要があります。

  • CORSを有効にし、CSRFを無効にします。
  • セッション管理をステートレスに設定します。
  • 不正リクエストの例外ハンドラを設定します。
  • エンドポイントに権限を設定します。
  • JWTトークンフィルターを追加します。

この構成は次のように実装されます。

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

SpringSecurityの内部UsernamePasswordAuthenticationFilterの前にJwtTokenFilterを追加したことに注意してください。 これを行うのは、認証/承認を実行するためにこの時点でユーザーIDにアクセスする必要があり、その抽出は、提供されたJWTトークンに基づくJWTトークンフィルター内で行われるためです。 これは次のように実装されます。

 @Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }

ログインAPI関数を実装する前に、もう1つの手順を実行する必要があります。認証マネージャーにアクセスする必要があります。 デフォルトでは、パブリックにアクセスできないため、構成クラスでBeanとして明示的に公開する必要があります。

これは次のように実行できます。

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

これで、ログインAPI関数を実装する準備が整いました。

 @Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }

ここでは、認証マネージャーを使用して提供された資格情報を確認し、成功した場合は、JWTトークンを生成して、応答本文のユーザーID情報とともに応答ヘッダーとして返します。

SpringSecurityによる承認

前のセクションでは、認証プロセスを設定し、パブリック/プライベートURLを構成しました。 これは単純なアプリケーションには十分かもしれませんが、ほとんどの実際のユースケースでは、ユーザーに対して常に役割ベースのアクセスポリシーが必要です。 この章では、この問題に対処し、SpringSecurityフレームワークを使用して役割ベースの承認スキーマを設定します。

サンプルアプリケーションでは、次の3つの役割を定義しました。

  • USER_ADMINを使用すると、アプリケーションユーザーを管理できます。
  • AUTHOR_ADMINを使用すると、作成者を管理できます。
  • BOOK_ADMINを使用すると、書籍を管理できます。

次に、それらを対応するURLに適用する必要があります。

  • api/publicは一般公開されています。
  • api/admin/userは、 USER_ADMINロールを持つユーザーにアクセスできます。
  • api/authorは、 AUTHOR_ADMINロールを持つユーザーにアクセスできます。
  • api/bookは、 BOOK_ADMINロールを持つユーザーにアクセスできます。

Spring Securityフレームワークには、認証スキーマを設定するための2つのオプションがあります。

  • URLベースの構成
  • 注釈ベースの構成

まず、URLベースの構成がどのように機能するかを見てみましょう。 これは、次のようにWebセキュリティ構成に適用できます。

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }

ご覧のとおり、このアプローチは単純明快ですが、欠点が1つあります。 アプリケーションの承認スキーマは複雑になる可能性があり、すべてのルールを1つの場所で定義すると、非常に大きく複雑になり、読みにくくなります。 このため、私は通常、注釈ベースの構成を使用することを好みます。

Spring Securityフレームワークは、Webセキュリティ用に次のアノテーションを定義します。

  • @PreAuthorizeはSpringExpressionLanguageをサポートし、メソッドを実行する前に式ベースのアクセス制御を提供するために使用されます。
  • @PostAuthorizeはSpringExpressionLanguageをサポートし、メソッドの実行後に式ベースのアクセス制御を提供するために使用されます(メソッドの結果にアクセスする機能を提供します)。
  • @PreFilterは、Spring Expression Languageをサポートし、定義したカスタムセキュリティルールに基づいてメソッドを実行する前に、コレクションまたは配列をフィルタリングするために使用されます。
  • @PostFilterはSpringExpressionLanguageをサポートし、定義したカスタムセキュリティルールに基づいてメソッドを実行した、返されたコレクションまたは配列をフィルタリングするために使用されます(メソッドの結果にアクセスする機能を提供します)。
  • @SecuredはSpringExpressionLanguageをサポートしておらず、メソッドのロールのリストを指定するために使用されます。
  • @RolesAllowedはSpringExpressionLanguageをサポートしておらず、@Securedアノテーションと同等の@Securedアノテーションです。

これらのアノテーションはデフォルトで無効になっており、アプリケーションで次のように有効にできます。

 @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }


@Secured securedEnabled = trueは、@Securedアノテーションを有効にします。
jsr250Enabled = trueは、 @RolesAllowedアノテーションを有効にします。
prePostEnabled = trueは、 @PostAuthorize @PreAuthorize @ PostAuthorize、 @PreFilter@PostFilterアノテーションを有効にします。

それらを有効にした後、次のようにAPIエンドポイントにロールベースのアクセスポリシーを適用できます。

 @Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }

セキュリティアノテーションは、クラスレベルとメソッドレベルの両方で提供できることに注意してください。

示されている例は単純であり、実際のシナリオを表すものではありませんが、Spring Securityは豊富なアノテーションのセットを提供し、それらを使用することを選択した場合は複雑な承認スキーマを処理できます。

ロール名デフォルトプレフィックス

この別のサブセクションでは、多くの新規ユーザーを混乱させるもう1つの微妙な詳細を強調したいと思います。

Spring Securityフレームワークは、次の2つの用語を区別します。

  • Authorityは、個人の許可を表します。
  • Roleは、権限のグループを表します。

両方とも、 GrantedAuthorityと呼ばれる単一のインターフェースで表すことができ、後で次のようにSpringSecurityアノテーション内のSpringExpressionLanguageでチェックできます。

  • Authority :@PreAuthorize(“ hasAuthority('EDIT_BOOK')”)
  • Role :@PreAuthorize(“ hasRole('BOOK_ADMIN')”)

これら2つの用語の違いをより明確にするために、SpringSecurityフレームワークはデフォルトでロール名にROLE_プレフィックスを追加します。 したがって、 BOOK_ADMIN ROLE_BOOK_ADMINチェックします。

個人的には、この動作は混乱を招き、アプリケーションで無効にすることを好みます。 次のように、SpringSecurity構成内で無効にすることができます。

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }

SpringSecurityを使用したテスト

Spring Securityフレームワークを使用するときにユニットテストまたは統合テストでエンドポイントをテストするには、 spring-boot-starter-testとともにspring-security-test依存関係を追加する必要があります。 pom.xmlビルドファイルは次のようになります。

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>

この依存関係により、テスト関数にセキュリティコンテキストを追加するために使用できるいくつかのアノテーションにアクセスできます。

これらの注釈は次のとおりです。

  • @WithMockUserをテストメソッドに追加して、モックユーザーとの実行をエミュレートできます。
  • @WithUserDetailsをテストメソッドに追加して、 UserDetailsService UserDetailsの実行をエミュレートできます。
  • @WithAnonymousUserをテストメソッドに追加して、匿名ユーザーでの実行をエミュレートできます。 これは、ユーザーが特定のユーザーとしてテストの大部分を実行し、いくつかのメソッドをオーバーライドして匿名にする場合に役立ちます。
  • @WithSecurityContextは、使用するSecurityContextを決定し、上記の3つのアノテーションはすべてそれに基づいています。 特定のユースケースがある場合は、 @WithSecurityContextを使用して独自のアノテーションを作成して必要なSecurityContextを作成できます。 その説明は私たちの記事の範囲外です。詳細については、SpringSecurityのドキュメントを参照してください。

特定のユーザーでテストを実行する最も簡単な方法は、 @WithMockUserアノテーションを使用することです。 これを使用して模擬ユーザーを作成し、次のようにテストを実行できます。

 @Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }

ただし、このアプローチにはいくつかの欠点があります。 まず、モックユーザーが存在しないため、後でデータベースからユーザー情報を照会する統合テストを実行すると、テストは失敗します。 次に、モックユーザーはorg.springframework.security.core.userdetails.Userクラスのインスタンスです。これは、SpringフレームワークのUserDetailsインターフェースの内部実装であり、独自の実装がある場合は、後で競合が発生する可能性があります。テストの実行。

以前の欠点がアプリケーションのブロッカーである場合は、 @WithUserDetailsアノテーションが最適です。 これは、カスタムUserDetailsおよびUserDetailsService実装がある場合に使用されます。 ユーザーが存在することを前提としているため、テストを実行する前に、データベースに実際の行を作成するか、 UserDetailsServiceモックインスタンスを提供する必要があります。

このアノテーションの使用方法は次のとおりです。

 @Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }

前述のインターフェイスのカスタム実装があるため、これはサンプルプロジェクトの統合テストで推奨されるアノテーションです。

@WithAnonymousUserを使用すると、匿名ユーザーとして実行できます。 これは、特定のユーザーでほとんどのテストを実行したいが、匿名ユーザーとしていくつかのテストを実行したい場合に特に便利です。 たとえば、以下は、モックユーザーでtest1test2のテストケースを実行し、匿名ユーザーでtest3を実行します。

 @SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }

まとめ

最後になりますが、Spring Securityフレームワークはおそらく美人コンテストに勝つことはなく、確実に学習曲線が急です。 初期構成が複雑なため、自家製のソリューションに置き換えられた多くの状況に遭遇しました。 ただし、開発者がその内部を理解し、初期構成を設定できるようになると、比較的簡単に使用できるようになります。

この記事では、構成の微妙な詳細をすべて示してみました。例がお役に立てば幸いです。 完全なコード例については、サンプルのSpringSecurityプロジェクトのGitリポジトリを参照してください。