【Spring Security徹底解説(3)】デフォルトログイン認証の実装原理

1. デフォルト設定のログイン認証プロセス

画像の説明を追加してください

2. プロセス分析

デフォルトの SecurityFilterChain (つまり、フォームログイン) を例にとると、Spring Security がサーバーに /hello リソースを要求するプロセス分析は次のようになります。
ここに画像の説明を挿入

  1. /hello インターフェースをリクエストするには、Spring Security を導入した後、まず一連のフィルターを通過します (1 つのリクエストは /test インターフェースです)。
  2. リクエストが に到着したときFilterSecurityInterceptor、リクエストが認証されていないことが判明しました。リクエストはインターセプトされ、AccessDeniedException例外がスローされます。
  3. スローされた例外はAccessDeniedExceptionキャッチされExceptionTranslationFilter、Filter は LoginUrlAuthenticationEntryPoint#commence メソッドを呼び出して client に戻り302(暂时重定向)、クライアントが /login ページにリダイレクトするように要求します。
  4. クライアントは /login リクエストを送信します。
  5. DefaultLoginPageGeneratingFilter/login リクエストは、フィルタに再度遭遇するとログイン ページを返します。

ログインページの由来

以下にDefaultLoginPageGeneratingFilter書き換えdoFilter方法を示します。これは、デフォルト設定でログイン ページが返され、ログイン ページが次のフィルタによって実装される理由も説明できます。

// DefaultLoginPageGeneratingFilter

	@Override
	public void doFilter(ServletRequest request,
	 ServletResponse response, 
	FilterChain chain)
			throws IOException, ServletException {
    
    
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

    private void doFilter(HttpServletRequest request, 
    HttpServletResponse response, 
    FilterChain chain) throws IOException, ServletException {
    
    
        boolean loginError = this.isErrorPage(request);
        boolean logoutSuccess = this.isLogoutSuccess(request);
        // 判断是否是登录请求、登录错误和注销确认
        // 不是的话给用户返回登录界面
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
    
    
            chain.doFilter(request, response);
        } else {
    
    
        // generateLoginPageHtml方法中有对页面登录代码进行了字符串拼接
        // 太长了,这里就不给出来了
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }

フォームログイン認証処理(ソースコード解析)

ログイン ページにリダイレクトされた後、どのように検証され、ユーザー名とパスワードを認証するかという質問が表示されます。

まず、デフォルトロードではフォーム認証が有効になっていることが分かりますが、[Spring Security入門(2)] Spring Securityの実装原理の中で、デフォルトでロードされるフィルタの1つがあることを編集者が指摘していましたUsernamePasswordAuthenticationFilter。はフォームリクエストを処理するために使用されます。実際には、のメソッドHttpSecurityformLogin呼び出すことで設定されたフィルターです。

次に、UsernamePasswordAuthenticationFilter が何を行うかを分析します (これはネイティブ フィルターではなく、doFilter ではなく、attemptAuthentication によってフィルターされ、パラメーターはネイティブ フィルターよりも 1 チェーン少ないです)。

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, 
	HttpServletResponse response)
			throws AuthenticationException {
    
    
			// 首先是判断是否是POST请求
		if (this.postOnly && !request.getMethod().equals("POST")) {
    
    
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 获取用户名和密码
		// 这是通过获取表单输入框名为username的数据
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		// 这是获取表单输入框名为password的数据
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		// 在一中小编也说了,这是Security中的认证
		// 通过调用AuthenticationManager中的authenticate方法
		// 需要传递的参数的Authentication对象,当时是这样解释的
		return this.getAuthenticationManager()
		.authenticate(authRequest);
	}

ここに画像の説明を挿入

デバッグ後、authenticateメソッドを入力して認証方法を確認します。デバッグの認証プロセスは次のとおりです。

  1. 認証メソッドを入力すると、ProviderManagerその下の認証メソッドが呼び出され、AuthenticationManager が書き換えられます。初めて、プロバイダーには、匿名認証に使用される AnoymousAuthenticationProvider オブジェクトのみが存在します。最終的に、これが認証されるかどうかを判断します。認証はサポートされていますが、プロバイダーはサポートされていません。
    ここに画像の説明を挿入ここに画像の説明を挿入

  2. この時点では匿名認証が一致しないため次のステップが実行されますが、parent属性が空ではないため親のauthenticateを呼び出して認証を行うことになります。(その親も ProviderManager オブジェクトですが、DaoAuthenticationProviderそのプロバイダー コレクションには認証オブジェクトがあります)。このことから、の AuthenticationManager オブジェクトは次の構築方法によって取得されることが
    ここに画像の説明を挿入間接的に推測できます。UsernamePasswordAuthenticationFilter
    ここに画像の説明を挿入

  3. provider.supportsメソッドが正常に一致したので、プロバイダーに検証させ、検証された結果セットを返します。
    ここに画像の説明を挿入AuthenticationProvider の認証メソッドは DaoAuthenticationProvider でオーバーライドされず、AbstractUserDetailsAuthenticationProviderその抽象親クラスによって実装されます。コア メソッドはオブジェクトretrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);を取得し、他のパラメーターを組み合わせて Authentication オブジェクトを作成し、それを返します。UserDetails

AbstractUserDetailsAuthenticationProvider下的authenticate方法

	@Override
	public Authentication authenticate(Authentication authentication) 
	throws AuthenticationException {
    
    
// 断言 authentication 是否是UsernamePasswordAuthenticationToken对象
	Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		// 获取一下用户名
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		// 从缓存中拿UserDetails 对象,显然没有,咱刚调试呢,哪来的缓存
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
    
    
		// 既然为空呢,就说明这不是从缓存中拿的,调为false
			cacheWasUsed = false;
			try {
    
    
			// 核心代码,获取UserDetails对象去
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
    
    
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
    
    
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
    
    
			this.preAuthenticationChecks.check(user);
			// 这里是验证密码的,通过子类DaoAuthenticationProvider的这个方法对密码去进行验证
			// 传过去的参数是user(UserDetails对象)和authentication对象
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
    
    
			if (!cacheWasUsed) {
    
    
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
    
    
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
    
    
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
  1. 以下は、コア メソッドretrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)の概要DaoAuthenticationProviderです。このメソッドは、UserDetails オブジェクト、つまりユーザーの詳細情報を返すために使用され、認証情報にパッケージ化されていると便利です。認証が成功したかどうか。
// 一共两个参数,一个是用户名,一个是传过来的认证信息
	@Override
	protected final UserDetails retrieveUser(String username, 
	UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		prepareTimingAttackProtection();
		try {
    
    
		// 核心方法就是这个,通过UserDetatilsService中的loadUserByUsername方法去获取UserDetails对象
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
    
    
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
    
    
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
    
    
			throw ex;
		}
		catch (Exception ex) {
    
    
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

デフォルト設定では InMemoryUserDetailsManager オブジェクトであり、UserDetails に関するメモリベースの操作オブジェクトであることがわかります。その中のloadUserByUsernameメソッドを見ると、記述も非常に簡単です。ユーザー名は大文字と小文字が区別されません
ここに画像の説明を挿入

  1. パスワード検証について話しましょう。パスワード検証は3ソース コードで指摘されています。UserDetails オブジェクト user を取得した後、additionalAuthenticationChecksサブクラスのメソッドが呼び出され、パスワード検証が実行されます。主なことは、出力ボックスに入力されたパスワードと UserDetails オブジェクトのパスワードを比較することです。UserDetails パスワードはエンコードされたパスワード (暗号文) として理解でき、入力ボックスへの入力は平文として理解できますPasswordEncoder。これと同じくらい簡単です。理解してください。次に、PasswordEncoder一致するものがあるかどうかを確認します。デフォルトはDelegatingPasswordEncoderパスワードエンコーダです。
    ここに画像の説明を挿入

三、ユーザー詳細サービス

Spring Security での UserDetailsS​​ervice の実装

ここに画像の説明を挿入

  • ユーザー詳細マネージャーUserDetailsS​​erviceをベースに、ユーザーの追加、ユーザーの更新、ユーザーの削除、パスワードの変更、ユーザーの存在判定の5つのメソッドを定義していきます。
  • JdbcDaoImplUserDetailsS​​ervice に基づいて、データベースからユーザーをクエリするメソッドは spring-jdbc を通じて実装されます。
  • InMemoryUserDetailsManagerUserDetailsManager でユーザーを追加、削除、変更、クエリするメソッドを実装しましたが、これらはすべてメモリベースの操作であり、データは永続化されません。
  • JdbcUserDetailsManagerJdbcDaoImpl から継承され、同時に UserDetailsManager インターフェイスを実装するため、JdbcUserDetailsManager を通じてユーザーの追加、削除、変更、クエリを行うことができ、これらの操作はデータベースに永続化されます。ただし、JdbcUserDetailsManagerには、データベース上のユーザーを操作するためのSQLがあらかじめ書かれているという制限があり、柔軟性に欠けるため、実際の開発ではあまり使われていません。
  • キャッシュユーザー詳細サービスUserDetailsS​​erviceがキャッシュされるのが特徴です。
  • UserDetailsS​​erviceDelegatorUserDetailsS​​erviceの遅延読み込み機能を提供します。
  • ReactiveUserDetailsS​​erviceAdapterこれは、webflux-web-security モジュールによって定義された UserDetailsS​​ervice の実装です。

デフォルトの UserDetailsS​​ervice 構成 (ソース コード分析)

UserDetailsS​​ervice に関するデフォルトの構成はUserDetailsServiceAutoConfiguration自動構成クラスにあります。(コードが非常に長いので、ここでは核心部分のみ抜粋します)

@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
		value = {
    
     AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
				AuthenticationManagerResolver.class },
		type = {
    
     "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
				"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
				"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {
    
    

	@Bean
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider<PasswordEncoder> passwordEncoder) {
    
    
			// 这里是从SecurityProperties中获取User对象(这里的User对象是SecurityProperties的静态内部类)
		SecurityProperties.User user = properties.getUser();
		List<String> roles = user.getRoles();
		// 然后创建InMemoryUserDetailsManager对象返回
		// 交给Spring容器管理
		return new InMemoryUserDetailsManager(User.withUsername(user.getName())
			.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
			.roles(StringUtils.toStringArray(roles))
			.build());
	}
	}

UserDetailsS​​erviceAutoConfiguration の注釈を観察してください@ConditionalOnMissingBean。どう思いますか? 自動構成 SecurityFilterChain が発生しました。
上記の構成の意味は、デフォルトの構成を使用する場合は、まずコンテナーに次のものが含まれていないことを確認する必要があります。AuthenticationManager、AuthenticationProvider、UserDetailsS​​ervice、AuthenticationManagerResolverこの状態の例。

デフォルトのユーザー名とパスワード

上記の UserDetailsS​​ervice の自動設定から、使用される User オブジェクトがSecurityPropertiesそこ、それがどのような User オブジェクトであるかを見てみましょう。

まず、getUser を呼び出して取得します。このユーザーは常に新しい User オブジェクトであり、静的な内部クラス インスタンスです。

以下の静的内部クラスの User 属性を見ると、ユーザー名は「user」、パスワードは UUID 文字列、ロールは複数回指定できるリスト コレクションであることがわかります。
注: 以下の getter メソッドと setter メソッドはインターセプトされません。

独自のユーザー名とパスワードを設定できますか?
もちろん落とすことも可能です。
ここに画像の説明を挿入ご覧のとおり、これはアノテーションSecurityPropertiesによって変更されます@ConfigurationProperties(ここでは、SecurityProperties が Spring コンテナーによって管理されるオブジェクトであることを知っておく必要があります)。

@ConfigurationProperties アノテーションは、設定ファイルに設定された値を、セッター注入を通じてアノテーションによって変更されたオブジェクトにマップします。

したがって、構成ファイルで独自の構成を作成し、独自のユーザー名とパスワードを構成できます。

たとえば、次のように構成します。

# application.yml
spring:
  security:
    user:
      name: xxx
      password: 123

ユーザー名とパスワードが変更されます。

画像の説明を追加してください

4. まとめ

  • AuthenticationManager、ProviderManager、AuthenticationProvider 系。
    ここに画像の説明を挿入
  • retrieveUserDaoAuthenticationProviderメソッドとAdditionalAuthenticationChecks メソッド (これら 2 つのメソッドは、それぞれ UserDetailsS​​ervice オブジェクトと PasswordEncoder オブジェクトを適用します)。UsernamePasswordAuthenticationFilter最終的には でProviderManager認証を渡し、最後にAbstractUserDetailsAuthenticationProviderDaoAuthenticationProvider の親クラスの認証に移行して認証することになりますが、このプロセスとこれらのクラスとメソッドについては明確にする必要があります。後のニーズに便利で、デバッグにも利用可能
  • インターフェース (カスタム UserDetailsS​​ervice)を実装しUserDetailsService、実装クラスのインスタンスを Spring コンテナー管理に渡すことで、デフォルトの実装ではなくカスタム実装を使用することができます。
  • UserDetails は、ユーザー名、パスワード、権限などの情報をカプセル化するユーザー詳細オブジェクトです。これは UserDetailsS​​ervice の戻り値でもあり、カスタマイズできます。

おすすめ

転載: blog.csdn.net/qq_63691275/article/details/130925783