前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >升级springboot 2.6.x springcloud 2021.0.x 导致oauth2sso客户端登录失败

升级springboot 2.6.x springcloud 2021.0.x 导致oauth2sso客户端登录失败

作者头像
路过君
发布2022-06-15 12:44:47
1.4K0
发布2022-06-15 12:44:47
举报
文章被收录于专栏:路过君BLOG from CSDN

现象

oauth2客户端,授权服务器依赖版本升级 spring-boot:2.5.5升级到2.6.8 spring-cloud:2020.0.4升级到2021.0.3 授权服务器使用spring-cloud-starter-oauth2:2.2.5搭建 客户端申请访问令牌失败,授权服务器产生客户端证书错误异常事件

原因

spring-boot:2.5.5 对应spring-security:5.5.2 spring-boot 2.6.8 对应spring-scurity:5.6.5 客户端申请访问令牌时,使用Basic Authentication方式认证,将客户端证书信息通过Authorization请求头部传递给授权服务器。spring-scurity-oauth2-client:5.6.5中客户端证书信息编码格式发生了变化,而授权服务器spring-scurity:5.6.5没有对BasicAuthentication认证信息进行正确解码,导致授权服务校验客户端失败

源码分析

spring-security-oauth2-client:5.6.5

  1. 尝试获取认证令牌 org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider
代码语言:javascript
复制
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken)this.authorizationCodeAuthenticationProvider.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange()));
}
  1. 从认证服务器跳转返回时,携带有效状态码,则尝试获取访问令牌 org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider
代码语言:javascript
复制
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken)authentication;
    OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationResponse();
    if (authorizationResponse.statusError()) {
        throw new OAuth2AuthorizationException(authorizationResponse.getError());
    } else {
        OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest();
        if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
            OAuth2Error oauth2Error = new OAuth2Error("invalid_state_parameter");
            throw new OAuth2AuthorizationException(oauth2Error);
        } else {
            OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange()));
            OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken(), accessTokenResponse.getAdditionalParameters());
            authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
            return authenticationResult;
        }
    }
}
  1. 向授权服务器申请访问令牌 org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient
代码语言:javascript
复制
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
    Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
    RequestEntity<?> request = (RequestEntity)this.requestEntityConverter.convert(authorizationCodeGrantRequest);
    ResponseEntity<OAuth2AccessTokenResponse> response = this.getResponse(request);
    OAuth2AccessTokenResponse tokenResponse = (OAuth2AccessTokenResponse)response.getBody();
    if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
        tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse).scopes(authorizationCodeGrantRequest.getClientRegistration().getScopes()).build();
    }
    return tokenResponse;
}
  1. 授权码请求实体转换器,组装访问令牌申请请求 org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequestEntityConverter
代码语言:javascript
复制
private Converter<T, HttpHeaders> headersConverter = (authorizationGrantRequest) -> {
    return OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(authorizationGrantRequest.getClientRegistration());
};
public RequestEntity<?> convert(T authorizationGrantRequest) {
    HttpHeaders headers = (HttpHeaders)this.getHeadersConverter().convert(authorizationGrantRequest);
    MultiValueMap<String, String> parameters = (MultiValueMap)this.getParametersConverter().convert(authorizationGrantRequest);
    URI uri = UriComponentsBuilder.fromUriString(authorizationGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()).build().toUri();
    return new RequestEntity(parameters, headers, HttpMethod.POST, uri);
}
  1. 根据客户端注册信息获取token请求头 此实现将客户端认证信息使用urlencode转码,导致如果客户端ID,密码中的某些特殊字符被转换为url编码格式。认证服务器对客户端进行认证时产生错误证书错误异常。 org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationGrantRequestEntityUtils
代码语言:javascript
复制
static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
    HttpHeaders headers = new HttpHeaders();
    headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
    if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod()) || ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
        String clientId = encodeClientCredential(clientRegistration.getClientId());
        String clientSecret = encodeClientCredential(clientRegistration.getClientSecret());
        headers.setBasicAuth(clientId, clientSecret);
    }

    return headers;
}
private static String encodeClientCredential(String clientCredential) {
    try {
        return URLEncoder.encode(clientCredential, StandardCharsets.UTF_8.toString());
    } catch (UnsupportedEncodingException var2) {
        throw new IllegalArgumentException(var2);
    }
}
  • 对比spring-boot:2.5.5(spring-security-oauth2-client:5.5.2) 处理请求头时并没有将认证信息进行url编码
代码语言:javascript
复制
static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
	HttpHeaders headers = new HttpHeaders();
	headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
	if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod())
			|| ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
		headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
	}
	return headers;
}
  1. 授权服务器拦截处理BasicAuthentication认证信息 org.springframework.security.web.authentication.www.BasicAuthenticationFilter
代码语言:javascript
复制
private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter();
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request);
        ...
}
  1. 转换BasicAuthentication请求 默认的转换器只对请求Authorization头部信息做base64解码,并没有进行urldecode,导致证书信息没有正确还原,校验失败。 org.springframework.security.web.authentication.www.BasicAuthenticationConverter
代码语言:javascript
复制
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) {
    String header = request.getHeader("Authorization");
    if (header == null) {
        return null;
    } else {
        header = header.trim();
        if (!StringUtils.startsWithIgnoreCase(header, "Basic")) {
            return null;
        } else if (header.equalsIgnoreCase("Basic")) {
            throw new BadCredentialsException("Empty basic authentication token");
        } else {
            byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8);
            byte[] decoded = this.decode(base64Token);
            String token = new String(decoded, this.getCredentialsCharset(request));
            int delim = token.indexOf(":");
            if (delim == -1) {
                throw new BadCredentialsException("Invalid basic authentication token");
            } else {
                UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim), token.substring(delim + 1));
                result.setDetails(this.authenticationDetailsSource.buildDetails(request));
                return result;
            }
        }
    }
}

解决

通过请求头传递BasicAuthentication认证信息时会进行base64编码,因此无需进行urlencode,自定义HeadersConverter替换默认实现 oauth2sso客户端安全配置修改如下

代码语言:javascript
复制
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
    	// 自定义converter
        OAuth2AuthorizationCodeGrantRequestEntityConverter converter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
        converter.setHeadersConverter((authorizationGrantRequest) -> {
            ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
            HttpHeaders headers = new HttpHeaders();
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
            MediaType contentType = MediaType.valueOf("application/x-www-form-urlencoded;charset=UTF-8");
            headers.setContentType(contentType);
            if (ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod()) || ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
                headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
            }
            return headers;
        });
        DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
        client.setRequestEntityConverter(converter);
        http.oauth2Client().authorizationCodeGrant().accessTokenResponseClient(client);
        http.oauth2Login().tokenEndpoint().accessTokenResponseClient(client);
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 现象
  • 原因
  • 源码分析
  • 解决
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档