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
并未转换context
和scope
字段,所以需要在复写OAuth2AccessTokenResponseClient
的getTokenResponse
方法中重新设置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全栈技术分享
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论