Spring Security Bigcommerce OAuth2 调试记录

(1)断点切入点

public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException, IOException, ServletException {
        ...
        OAuth2LoginAuthenticationToken authenticationResult =
                (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
        ...
    }

(2)进入this.getAuthenticationManager().authenticate(authenticationRequest)方法

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    ...
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            ...

            while(itor.hasNext()) {
                AuthenticationProvider provider = (AuthenticationProvider) itor.next();
                if (provider.supports(toTest)) {
                    if (debug) {
                        logger.debug("Authentication attempt using " + provider.getClass().getName());
                    }

                    try {
                        result = provider.authenticate(authentication);

(3)进入provider.authenticate(authentication)方法

public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ...

        OAuth2AccessTokenResponse accessTokenResponse;
        try {
            OAuth2AuthorizationExchangeValidator.validate(
                    authorizationCodeAuthentication.getAuthorizationExchange());

            accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
                    new OAuth2AuthorizationCodeGrantRequest(
                            authorizationCodeAuthentication.getClientRegistration(),
                            authorizationCodeAuthentication.getAuthorizationExchange()));

(4)跳转到自定义的OAuth2AccessTokenResponseClient.getTokenResponse

public class BigcommerceAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {

    private DefaultAuthorizationCodeTokenResponseClient oAuth2AccessTokenResponseClient;
    ...

    @Override
    public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
        ....

        oAuth2AccessTokenResponseClient.setRequestEntityConverter(new OAuth2AuthorizationCodeGrantRequestEntityConverter() {
            @Override
            public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
                RequestEntity requestEntity = super.convert(authorizationCodeGrantRequest);
                MultiValueMap<String, String> body = (MultiValueMap<String, String>) requestEntity.getBody();
                body.add("context", shopName);
                body.add("scope", StringUtils.collectionToDelimitedString(currentRegistration.getScopes(), " "));
                return requestEntity;
            }
        });

        OAuth2AccessTokenResponse resp = oAuth2AccessTokenResponseClient.getTokenResponse(newGrantReq);

(5)进入oAuth2AccessTokenResponseClient.getTokenResponse(newGrantReq)方法

public final class DefaultAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {

    @Override
    public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");

        RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);

        ResponseEntity<OAuth2AccessTokenResponse> response;
        try {
            response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);

(6)进入this.requestEntityConverter.convert(authorizationCodeGrantRequest)方法

public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {

    /**
     * Returns the {@link RequestEntity} used for the Access Token Request.
     *
     * @param authorizationCodeGrantRequest the authorization code grant request
     * @return the {@link RequestEntity} used for the Access Token Request
     */
    @Override
    public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();

        HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
        MultiValueMap<String, String> formParameters = this.buildFormParameters(authorizationCodeGrantRequest);
        URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
                .build()
                .toUri();

        return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
    }

    /**
     * Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
     *
     * @param authorizationCodeGrantRequest the authorization code grant request
     * @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
     */
    private MultiValueMap<String, String> buildFormParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
        OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();

        MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
        formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
        formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
        formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri());
        if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
            formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
            formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
        }

        return formParameters;
    }
}

注意:正因为requestEntityConverter.convert并未转换contextscope字段,所以需要在复写OAuth2AccessTokenResponseClientgetTokenResponse方法中重新设置oAuth2AccessTokenResponseClient.setRequestEntityConverter

(7)另外,自定义的BigcommerceOAuth2AccessTokenResponseHttpMessageConverter设置的BigcommerceOAuth2AccessTokenResponseConverter如果实现的泛型是Converter<Map<String, String>, OAuth2AccessTokenResponse>,则无法转换内嵌JSON对象,导致Response反序列化失败,需要转换为Converter<Map<String, Object>, OAuth2AccessTokenResponse>

{
  "access_token": "ACCESS_TOKEN",
  "scope": "store_v2_orders",
  "user": {
    "id": 28888,
    "email": "bigcommerce@appblog.cn"
  },
  "context": "stores/STORE_HASH"
}
org.springframework.security.oauth2.core.OAuth2AuthenticationException: [invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: Error while extracting response for type [class org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse] and content type [application/json;charset=utf-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: An error occurred reading the OAuth 2.0 Access Token Response: JSON parse error: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
 at [Source: (PushbackInputStream); line: 1, column: 71] (through reference chain: java.util.LinkedHashMap["user"]); nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.lang.String` out of START_OBJECT token
 at [Source: (PushbackInputStream); line: 1, column: 71] (through reference chain: java.util.LinkedHashMap["user"])
public class BigcommerceOAuth2AccessTokenResponseHttpMessageConverter extends MyOAuth2AccessTokenResponseHttpMessageConverter {

    public CustomBigcommerceOAuth2AccessTokenResponseHttpMessageConverter() {
        super();
        super.setTokenResponseConverter(new BigcommerceOAuth2AccessTokenResponseConverter());
    }

    /**
     * Identical to the OAuth2AccessTokenResponseConverter provided in OAuth2AccessTokenResponseHttpMessageConverter.
     * The major difference is that this converter will not fail if "token_type" is not provided. It defaults to "bearer".
     */
    private static class BigcommerceOAuth2AccessTokenResponseConverter implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> {
        private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of(
                OAuth2ParameterNames.ACCESS_TOKEN,
                OAuth2ParameterNames.TOKEN_TYPE,
                OAuth2ParameterNames.EXPIRES_IN,
                OAuth2ParameterNames.REFRESH_TOKEN,
                OAuth2ParameterNames.SCOPE).collect(Collectors.toSet());

        @Override
        public OAuth2AccessTokenResponse convert(Map<String, Object> tokenResponseParameters) {
            String accessToken = (String) tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

            OAuth2AccessToken.TokenType accessTokenType = null;
            if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(
                    (String) tokenResponseParameters.get(OAuth2ParameterNames.TOKEN_TYPE))) {
                accessTokenType = OAuth2AccessToken.TokenType.BEARER;
            }

            if (accessTokenType == null) {
                accessTokenType = OAuth2AccessToken.TokenType.BEARER;
            }

            long expiresIn = 0;
            if (tokenResponseParameters.containsKey(OAuth2ParameterNames.EXPIRES_IN)) {
                try {
                    expiresIn = Long.valueOf((String) tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
                } catch (NumberFormatException ex) {
                }
            }

            Set<String> scopes = Collections.emptySet();
            if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
                String scope = (String) tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
                scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope, ",")).collect(Collectors.toSet());
            }

            String refreshToken = (String) tokenResponseParameters.get(OAuth2ParameterNames.REFRESH_TOKEN);

            Map<String, Object> additionalParameters = new LinkedHashMap<>();
            tokenResponseParameters.entrySet().stream()
                    .filter(e -> !TOKEN_RESPONSE_PARAMETER_NAMES.contains(e.getKey()))
                    .forEach(e -> additionalParameters.put(e.getKey(), e.getValue()));

            return OAuth2AccessTokenResponse.withToken(accessToken)
                    .tokenType(accessTokenType)
                    .expiresIn(expiresIn)
                    .scopes(scopes)
                    .refreshToken(refreshToken)
                    .additionalParameters(additionalParameters)
                    .build();
        }
    }

}
public class MyOAuth2AccessTokenResponseHttpMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessTokenResponse> {
    private static final Charset DEFAULT_CHARSET;
    private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE;
    private GenericHttpMessageConverter<Object> jsonMessageConverter = HttpMessageConverters.getJsonMessageConverter();
    protected Converter<Map<String, Object>, OAuth2AccessTokenResponse> tokenResponseConverter = new OAuth2AccessTokenResponseConverter();
    protected Converter<OAuth2AccessTokenResponse, Map<String, Object>> tokenResponseParametersConverter = new OAuth2AccessTokenResponseParametersConverter();

    public MyOAuth2AccessTokenResponseHttpMessageConverter() {
        super(DEFAULT_CHARSET, new MediaType[]{MediaType.APPLICATION_JSON, new MediaType("application", "*+json")});
    }

    protected boolean supports(Class<?> clazz) {
        return OAuth2AccessTokenResponse.class.isAssignableFrom(clazz);
    }

    protected OAuth2AccessTokenResponse readInternal(Class<? extends OAuth2AccessTokenResponse> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        try {
            Map<String, Object> tokenResponseParameters = (Map)this.jsonMessageConverter.read(PARAMETERIZED_RESPONSE_TYPE.getType(), (Class)null, inputMessage);
            return (OAuth2AccessTokenResponse)this.tokenResponseConverter.convert(tokenResponseParameters);
        } catch (Exception var4) {
            throw new HttpMessageNotReadableException("An error occurred reading the OAuth 2.0 Access Token Response: " + var4.getMessage(), var4, inputMessage);
        }
    }

    protected void writeInternal(OAuth2AccessTokenResponse tokenResponse, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        try {
            Map<String, String> tokenResponseParameters = (Map)this.tokenResponseParametersConverter.convert(tokenResponse);
            this.jsonMessageConverter.write(tokenResponseParameters, PARAMETERIZED_RESPONSE_TYPE.getType(), MediaType.APPLICATION_JSON, outputMessage);
        } catch (Exception var4) {
            throw new HttpMessageNotWritableException("An error occurred writing the OAuth 2.0 Access Token Response: " + var4.getMessage(), var4);
        }
    }

    public final void setTokenResponseConverter(Converter<Map<String, Object>, OAuth2AccessTokenResponse> tokenResponseConverter) {
        Assert.notNull(tokenResponseConverter, "tokenResponseConverter cannot be null");
        this.tokenResponseConverter = tokenResponseConverter;
    }

    public final void setTokenResponseParametersConverter(Converter<OAuth2AccessTokenResponse, Map<String, Object>> tokenResponseParametersConverter) {
        Assert.notNull(tokenResponseParametersConverter, "tokenResponseParametersConverter cannot be null");
        this.tokenResponseParametersConverter = tokenResponseParametersConverter;
    }

    static {
        DEFAULT_CHARSET = StandardCharsets.UTF_8;
        PARAMETERIZED_RESPONSE_TYPE = new ParameterizedTypeReference<Map<String, Object>>() {
        };
    }

    private static class OAuth2AccessTokenResponseParametersConverter implements Converter<OAuth2AccessTokenResponse, Map<String, Object>> {
        private OAuth2AccessTokenResponseParametersConverter() {
        }

        public Map<String, Object> convert(OAuth2AccessTokenResponse tokenResponse) {
            Map<String, Object> parameters = new HashMap();
            long expiresIn = -1L;
            if (tokenResponse.getAccessToken().getExpiresAt() != null) {
                expiresIn = ChronoUnit.SECONDS.between(Instant.now(), tokenResponse.getAccessToken().getExpiresAt());
            }

            parameters.put("access_token", tokenResponse.getAccessToken().getTokenValue());
            parameters.put("token_type", tokenResponse.getAccessToken().getTokenType().getValue());
            parameters.put("expires_in", String.valueOf(expiresIn));
            if (!CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
                parameters.put("scope", StringUtils.collectionToDelimitedString(tokenResponse.getAccessToken().getScopes(), " "));
            }

            if (tokenResponse.getRefreshToken() != null) {
                parameters.put("refresh_token", tokenResponse.getRefreshToken().getTokenValue());
            }

            if (!CollectionUtils.isEmpty(tokenResponse.getAdditionalParameters())) {
                tokenResponse.getAdditionalParameters().entrySet().stream().forEach((e) -> {
                    String var10000 = (String)parameters.put(e.getKey(), e.getValue().toString());
                });
            }

            return parameters;
        }
    }

    private static class OAuth2AccessTokenResponseConverter implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> {
        private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = (Set) Stream.of("access_token", "token_type", "expires_in", "refresh_token", "scope").collect(Collectors.toSet());

        private OAuth2AccessTokenResponseConverter() {
        }

        public OAuth2AccessTokenResponse convert(Map<String, Object> tokenResponseParameters) {
            String accessToken = (String)tokenResponseParameters.get("access_token");
            OAuth2AccessToken.TokenType accessTokenType = null;
            if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase((String)tokenResponseParameters.get("token_type"))) {
                accessTokenType = OAuth2AccessToken.TokenType.BEARER;
            }

            long expiresIn = 0L;
            if (tokenResponseParameters.containsKey("expires_in")) {
                try {
                    expiresIn = Long.valueOf((String)tokenResponseParameters.get("expires_in"));
                } catch (NumberFormatException var9) {
                }
            }

            Set<String> scopes = Collections.emptySet();
            String refreshToken;
            if (tokenResponseParameters.containsKey("scope")) {
                refreshToken = (String)tokenResponseParameters.get("scope");
                scopes = (Set) Arrays.stream(StringUtils.delimitedListToStringArray(refreshToken, " ")).collect(Collectors.toSet());
            }

            refreshToken = (String)tokenResponseParameters.get("refresh_token");
            Map<String, Object> additionalParameters = new LinkedHashMap();
            tokenResponseParameters.entrySet().stream().filter((e) -> {
                return !TOKEN_RESPONSE_PARAMETER_NAMES.contains(e.getKey());
            }).forEach((e) -> {
                additionalParameters.put(e.getKey(), e.getValue());
            });
            return OAuth2AccessTokenResponse.withToken(accessToken).tokenType(accessTokenType).expiresIn(expiresIn).scopes(scopes).refreshToken(refreshToken).additionalParameters(additionalParameters).build();
        }
    }
}

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/04/01/spring-security-bigcommerce-oauth2-debugging-record/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Spring Security Bigcommerce OAuth2 调试记录
(1)断点切入点 public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public Authentication attemptAuth……
<<上一篇
下一篇>>
文章目录
关闭
目 录