Pig4Cloud之登陆验证(一)客户端认证处理

前端登陆

handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.$store
              .dispatch("LoginByUsername", this.loginForm)
              .then(() => {
                this.$router.push({path: this.tagWel.value});
              })
              .catch(() => {
                this.refreshCode();
              });
        }
      });
    }

看一下LoginByUsername,在/src/store/modules/user.js中

const scope = 'server'

export const loginByUsername = (username, password, code, randomStr) => {
  const grant_type = 'password'
  let dataObj = qs.stringify({'username': username, 'password': password})

  let basicAuth = 'Basic ' + window.btoa(website.formLoginClient)

  // 保存当前选中的 basic 认证信息
  setStore({
    name: 'basicAuth',
    content: basicAuth,
    type: 'session'
  })

  return request({
    url: '/auth/oauth2/token',
    headers: {
      isToken: false,
      Authorization: basicAuth
    },
    method: 'post',
    params: {randomStr, code, grant_type, scope},
    data: dataObj
  })
}

客户端认证

当访问 OAuth2 相关接口时(/oauth2/token/oauth2/introspect/oauth2/revoke),授权服务器需要进行客户端认证。
Spring Authorization Server 截至目前支持如下五种客户端认证方式:client_secret_basicclient_secret_postclient_secret_jwtprivate_key_jwtnone (针对公共客户端)

1. OAuth2ClientAuthenticationFilter

实现客户端认证的拦截器就是 OAuth2ClientAuthenticationFilter。它继承了OncePerRequestFilter

1.1 OncePerRequestFilter

OncePerRequestFilter是Spring Boot里面的一个过滤器抽象类,其同样在Spring Security里面被广泛用到,这个过滤器抽象类通常被用于继承实现并在每次请求时只执行一次过滤。OncePerRequestFilter可以保证一次外部请求,只执行一次过滤方法,对于服务器内部之间的forward等请求,不会再次执行过滤方法。

public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            HttpServletRequest httpRequest = (HttpServletRequest)request;
            HttpServletResponse httpResponse = (HttpServletResponse)response;
            String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
            boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
            if (!this.skipDispatch(httpRequest) && !this.shouldNotFilter(httpRequest)) {
                if (hasAlreadyFilteredAttribute) {
                    if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
                        this.doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
                        return;
                    }

                    filterChain.doFilter(request, response);
                } else {
                    request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

                    try {
                        this.doFilterInternal(httpRequest, httpResponse, filterChain);
                    } finally {
                        request.removeAttribute(alreadyFilteredAttributeName);
                    }
                }
            } else {
                filterChain.doFilter(request, response);
            }

        } else {
            throw new ServletException("OncePerRequestFilter just supports HTTP requests");
        }
    }
  • 调用 getAlreadyFilteredAttributeName()
    方法如下。本质上就是通过 Class 对象再加上一个后缀形成一个 attribute 字符串返回。这个字符串很重要,它将作为每次执行的唯一标识符。
 protected String getAlreadyFilteredAttributeName() {
        String name = this.getFilterName();
        if (name == null) {
            name = this.getClass().getName();
        }

        return name + ".FILTERED";
    }
  • 请求判断
    判断 request 的 attribute 是否存在 alreadyFilteredAttributeName,存在则处理逻辑是 chain.doFilter(),也就是跳过该过滤器,继续执行其他过滤器链,不存在则说明 request 需要过滤,那么会将 alreadyFilteredAttributeName 填入 request 中,接着执行本过滤器的过滤逻辑。
  • 最终处理
    request.removeAttribute(alreadyFilteredAttributeName);
    去掉增加的无用属性,回归原始请求,防止干扰后续参数获取等操作。

1.2 doFilterInternal

OAuth2AuthorizationEndpointFilter覆写了OncePerRequestFilterdoFilterInternal方法

image

其核心代码如下:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

	if (!this.requestMatcher.matches(request)) {
		filterChain.doFilter(request, response);
		return;
	}

	try {
		Authentication authenticationRequest = this.authenticationConverter.convert(request);
		if (authenticationRequest instanceof AbstractAuthenticationToken) {
			((AbstractAuthenticationToken) authenticationRequest).setDetails(
					this.authenticationDetailsSource.buildDetails(request));
		}
		if (authenticationRequest != null) {
			Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
			this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
		}
		filterChain.doFilter(request, response);

	} catch (OAuth2AuthenticationException ex) {
		this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
	}
}

其核心逻辑就是通过 authenticationConverter 从 request 中解析出客户端认证信息,构建成 Authentication,再通过 authenticationManager 对 Authentication 进行认证。

image

1.3 DelegatingAuthenticationConverter

authenticationConverter 的类型实际上是 DelegatingAuthenticationConverter,它持有一个 AuthenticationConverter 列表(不同的认证请求,其参数不同,所以会有不同的AuthenticationConverter实现类)。

public final class DelegatingAuthenticationConverter implements AuthenticationConverter {
	private final List<AuthenticationConverter> converters;

	/**
	 * Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters.
	 *
	 * @param converters a {@code List} of {@link AuthenticationConverter}(s)
	 */
	public DelegatingAuthenticationConverter(List<AuthenticationConverter> converters) {
		Assert.notEmpty(converters, "converters cannot be empty");
		this.converters = Collections.unmodifiableList(new LinkedList<>(converters));
	}

	@Nullable
	@Override
	public Authentication convert(HttpServletRequest request) {
		Assert.notNull(request, "request cannot be null");
		for (AuthenticationConverter converter : this.converters) {
			Authentication authentication = converter.convert(request);
			if (authentication != null) {
				return authentication;
			}
		}
		return null;
	}
}

DelegatingAuthenticationConverter 在解析请求时会遍历 AuthenticationConverter 列表,当某个 AuthenticationConverter 解析成功时,立即返回,这也能确定此请求是什么认证方式,后续再执行对应的认证逻辑。

代理转发到ClientSecretBasicAuthenticationConverter
image

对header进行解析,获取凭证credentials
image

再次解析credentials,获得clientIDclientSecret,然后构建OAuth2ClientAuthenticationToken返回认证对象。
image

1.4 buildDetails

设置认证对象的detail,调用的是WebAuthenticationDetailsSourcebuildDetails方法,设置请求的上下文。
image

public class WebAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    public WebAuthenticationDetailsSource() {
    }

    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new WebAuthenticationDetails(context);
    }
}

1.5 validateClientIdentifier验证客户端标识符

image

private static void validateClientIdentifier(Authentication authentication) {
		if (!(authentication instanceof OAuth2ClientAuthenticationToken)) {
			return;
		}

		// As per spec, in Appendix A.1.
		// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#appendix-A.1
		// The syntax for client_id is *VSCHAR (%x20-7E):
		// -> Hex 20 -> ASCII 32 -> space
		// -> Hex 7E -> ASCII 126 -> tilde

		OAuth2ClientAuthenticationToken clientAuthentication = (OAuth2ClientAuthenticationToken) authentication;
		String clientId = (String) clientAuthentication.getPrincipal();
		for (int i = 0; i < clientId.length(); i++) {
			char charAt = clientId.charAt(i);
			if (!(charAt >= 32 && charAt <= 126)) {
				throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST);
			}
		}
	}

1.6 this.authenticationManager.authenticate认证

image

image

可以看到authenticationManager 的类型实际上是 ProviderManager,它持有一个 AuthenticationProvider 列表(不同的认证方式,其认证逻辑不同,所以会有不同的AuthenticationProvider实现类)。

AuthenticationManager 接口的默认实现为 ProviderManager

image

authenticationManager调用authenticate实际上是调用ProviderManagerauthenticate

image

调用 AuthenticationProvider 中的 supports(Class<?> authentication)`` 方法,判断是否支持当前的 Authentication请求。只有支持当前Authentication请求的AuthenticationProvider` 才会继续后续逻辑处理。

然后调用 AuthenticationProvider 中的 authenticate 方法进行身份认证。因为在1.3中返回的是OAuth2ClientAuthenticationToken对象,此处的provider为ClientSecretAuthenticationProvider

image

ClientSecretAuthenticationProvider中调用RegisteredClientRepository,通过clientId去数据库查询client
image

image

RegisteredClientRepositoryfindByClientId方法
image

YouxinRemoteRegisteredClientRepository类为我们自己实现的方法,它实现了RegisteredClientRepository接口,通过clientid来查询数据库,注册客户端并返回Builder实例。

如果判断无误,则返回OAuth2ClientAuthenticationToken对象。
image

1.7 copyDetails拷贝逻辑

如果认证成功且返回的结果不为 null,则执行 authentication details 的拷贝逻辑。
image

try {
    result = provider.authenticate(authentication);

    if (result != null) {
        copyDetails(authentication, result);
        break;
    }
}

......

private void copyDetails(Authentication source, Authentication dest) {
    if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
        AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;

        token.setDetails(source.getDetails());
    }
}

如果result不为null,则返回result
image

则又回到了1.6的authenticate方法。
image

如果发生 AccountStatusExceptionInternalAuthenticationServiceException 异常,则会通过Spring事件发布器AuthenticationEventPublisher 发布异常事件。

catch (AccountStatusException e) {
    prepareException(e, authentication);
    // SEC-546: Avoid polling additional providers if auth failure is due to
    // invalid account status
    throw e;
}
catch (InternalAuthenticationServiceException e) {
    prepareException(e, authentication);
    throw e;
}

......

private void prepareException(AuthenticationException ex, Authentication auth) {
    eventPublisher.publishAuthenticationFailure(ex, auth);
}

如果异常为其它类型的 AuthenticationException,则将此异常设置为lastException并返回。

catch (AuthenticationException e) {
    lastException = e;
}

如果认证结果为 null,且存在父 AuthenticationManager,则调用父 AuthenticationManager 进行同样的身份认证操作,其处理逻辑基本同上。

if (result == null && parent != null) {
    // Allow the parent to try.
    try {
        result = parentResult = parent.authenticate(authentication);
    }
    catch (ProviderNotFoundException e) {
        // ignore as we will throw below if no other exception occurred prior to
        // calling parent and the parent
        // may throw ProviderNotFound even though a provider in the child already
        // handled the request
    }
    catch (AuthenticationException e) {
        lastException = parentException = e;
    }
}

如果认证结果不为 null,同时,此时的 eraseCredentialsAfterAuthentication 参数为 true,且此时认证后的Authentication 实现了 CredentialsContainer 接口,那么即调用 CredentialsContainer 接口的凭据擦除方法,即eraseCredentials,擦除相关凭据信息。

if (result != null) {
    if (eraseCredentialsAfterAuthentication
        && (result instanceof CredentialsContainer)) {
        // Authentication is complete. Remove credentials and other secret data
        // from authentication
        ((CredentialsContainer) result).eraseCredentials();
    }

    // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
    // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
    if (parentResult == null) {
        eventPublisher.publishAuthenticationSuccess(result);
    }
    return result;
}

其中,有一个防止重复发布 AuthenticationSuccessEvent 事件的处理,即 parentResult 为空。如果 parentResult为 null,则代表父 AuthenticationManager 不存在或者没有身份认证成功,也即没有发布过 AuthenticationSuccessEvent 事件。此时,便由此处发布 AuthenticationSuccessEvent 事件。

如果lastException 为 null,则代表当前的 Authentication 并没有对应支持的 Provider。此时,便会抛出相应异常。

if (lastException == null) {
    lastException = new ProviderNotFoundException(messages.getMessage(
        "ProviderManager.providerNotFound",
        new Object[] { toTest.getName() },
        "No AuthenticationProvider found for {0}"));
}

如同防止重复发布 AuthenticationSuccessEvent 事件的处理一样,也有一个防止 AbstractAuthenticationFailureEvent 事件重复发布的逻辑处理。如果 parentException 为 null,则代表父AuthenticationManager 不存在、没有进行身份认证或者发布过 AbstractAuthenticationFailureEvent 事件,此时,便由此处发布 AbstractAuthenticationFailureEvent 事件。

if (parentException == null) {
    prepareException(lastException, authentication);
}

throw lastException;

最后,抛出 lastException。

1.8 认证成功onAuthenticationSuccess

认证成功,执行代码YouxinAuthenticationSuccessEventHandler

@SneakyThrows
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) {

		// 写入登录成功的日志
		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;
		Map<String, Object> map = accessTokenAuthentication.getAdditionalParameters();
		if (MapUtil.isNotEmpty(map)) {
			sendSuccessEventLog(request, accessTokenAuthentication, map);
		}

		// 清除账号历史锁定次数
		clearLoginFailureTimes(map);

		// 输出token
		sendAccessTokenResponse(response, authentication);
	}
	......
	
	private void sendAccessTokenResponse(HttpServletResponse response, Authentication authentication)
			throws IOException {

		OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) authentication;

		OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
		OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
		Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();

		OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
				.tokenType(accessToken.getTokenType()).scopes(accessToken.getScopes());
		if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
			builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
		}
		if (refreshToken != null) {
			builder.refreshToken(refreshToken.getTokenValue());
		}
		if (!CollectionUtils.isEmpty(additionalParameters)) {
			builder.additionalParameters(additionalParameters);
		}
		OAuth2AccessTokenResponse accessTokenResponse = builder.build();
		ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);

		// 无状态 注意删除 context 上下文的信息
		SecurityContextHolder.clearContext();
		this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
	}

然后继续向下执行
image

正式接收登录请求

2. OAuth2TokenEndpointFilter

OAuth2TokenEndpointFilter 会接收通过上文 OAuth2ClientAuthenticationFilter 客户端认证的请求
image

image

2.1组装认证对象

image
AuthenticationConverter 会根据请求中的参数和授权类型组装成对应的授权认证对象,此处同客户端认证中的1.3,最终指向了OAuth2ResourceOwnerBaseAuthenticationConverter
image

image

数据校验无误后返回一个OAuth2ResourceOwnerPasswordAuthenticationToken对象。

2.2认证管理器进行认证

image
逻辑同客户端认证中的1.6
image

此时请求来到了OAuth2ResourceOwnerBaseAuthenticationProviderauthenticate方法。
image

image

2.2.1 构建UsernamePasswordAuthenticationToken

image

image

	public UsernamePasswordAuthenticationToken buildToken(Map<String, Object> reqParameters) {
		String username = (String) reqParameters.get(OAuth2ParameterNames.USERNAME);
		String password = (String) reqParameters.get(OAuth2ParameterNames.PASSWORD);
		return new UsernamePasswordAuthenticationToken(username, password);
	}

类的关系图
image

2.2.2 交由spring security认证

image
认证逻辑同客户端认证的1.6,此处调用的是AbstractUserDetailsAuthenticationProvider

image

然后对header进行校验,提取token,校验token并返回UsernamePasswordAuthenticationToken
image

查询用户,通过数据库查询并返回UserDetails
image

2.2.3 认证成功后调用generatAuthenticationToken生成新的令牌

@NotNull
	private OAuth2AccessTokenAuthenticationToken generatAuthenticationToken(T resouceOwnerBaseAuthentication,
			OAuth2ClientAuthenticationToken clientPrincipal, RegisteredClient registeredClient,
			Set<String> authorizedScopes, Authentication usernamePasswordAuthentication) {
		// @formatter:off
		DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
				.registeredClient(registeredClient)
				.principal(usernamePasswordAuthentication)
				.authorizationServerContext(AuthorizationServerContextHolder.getContext())
				.authorizedScopes(authorizedScopes)
				.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
				.authorizationGrant(resouceOwnerBaseAuthentication);
		// @formatter:on

		OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
				.principalName(usernamePasswordAuthentication.getName())
				.authorizationGrantType(resouceOwnerBaseAuthentication.getAuthorizationGrantType())
				// 0.4.0 新增的方法
				.authorizedScopes(authorizedScopes);

		// ----- Access token -----
		OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
		OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
		if (generatedAccessToken == null) {
			OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
					"The token generator failed to generate the access token.", ERROR_URI);
			throw new OAuth2AuthenticationException(error);
		}
		OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
				generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
				generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
		if (generatedAccessToken instanceof ClaimAccessor) {
			authorizationBuilder.id(accessToken.getTokenValue())
					.token(accessToken,
							(metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
									((ClaimAccessor) generatedAccessToken).getClaims()))
					// 0.4.0 新增的方法
					.authorizedScopes(authorizedScopes)
					.attribute(Principal.class.getName(), usernamePasswordAuthentication);
		}
		else {
			authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
		}

		// ----- Refresh token -----
		OAuth2RefreshToken refreshToken = null;
		if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
		// Do not issue refresh token to public client
				!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {

			if (this.refreshTokenGenerator != null) {
				Instant issuedAt = Instant.now();
				Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getRefreshTokenTimeToLive());
				refreshToken = new OAuth2RefreshToken(this.refreshTokenGenerator.get(), issuedAt, expiresAt);
			}
			else {
				tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
				OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
				if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
					OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
							"The token generator failed to generate the refresh token.", ERROR_URI);
					throw new OAuth2AuthenticationException(error);
				}
				refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
			}
			authorizationBuilder.refreshToken(refreshToken);
		}

		OAuth2Authorization authorization = authorizationBuilder.build();

		this.authorizationService.save(authorization);

		LOGGER.debug("returning OAuth2AccessTokenAuthenticationToken");

		return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,
				Objects.requireNonNull(authorization.getAccessToken().getClaims()));
	}

认证成功,调用认证成功方法,并输出token

image

posted @ 2022-11-23 18:17  leepandar  阅读(208)  评论(0编辑  收藏  举报