SpringCloud Security + OAuth2 集成之回调地址拓展
技术栈:
springCloud (Finchley.SR2)
springBoot(2.0.4)
OAuth (2.3.3.RELEASE)
Security(2.3.3.RELEASE)
1.问题引出
在实际应用中,单调登录有验证 前端请求中携带的回调地址 与 数据库所存对应客户端ID所允许的回调地址 做验证的过程,那么就会存在一个 clientID 会对应多个 web_server_redirect_uri 服务回调地址。抱着试一下的心态,试着用 ;
来拼接多个 回调地址,会出现下面错误。Invalid redirect: does not match one of the registered values
,那么接下来来分析如何拓展回调地址。
2.问题分析
2.1.找到相关接口
- 验证客户端的相关接口为 :
/oauth/authorize
2.2.定位到相关的依赖和类
-
定位到依赖包:
spring-security-oauth2-2.2.3.RELEASE.jar
-
所在的类:
org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
2.3.定位到相关细节
-
所在的方法:
authorize
-
具体方法如下 :
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Store authorizationRequest AND an immutable Map of authorizationRequest in session
// which will be used to validate against in approveOrDeny()
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
- 如上,找到下面这条语句,便是对 重定向地址的处理。
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
- 进入
resolveRedirect
这个方法,方法内容如下:
public String resolveRedirect(String requestedRedirect, ClientDetails client) throws OAuth2Exception {
Set<String> authorizedGrantTypes = client.getAuthorizedGrantTypes();
if (authorizedGrantTypes.isEmpty()) {
throw new InvalidGrantException("A client must have at least one authorized grant type.");
}
if (!containsRedirectGrantType(authorizedGrantTypes)) {
throw new InvalidGrantException(
"A redirect_uri can only be used by implicit or authorization_code grant types.");
}
Set<String> registeredRedirectUris = client.getRegisteredRedirectUri();
if (registeredRedirectUris == null || registeredRedirectUris.isEmpty()) {
throw new InvalidRequestException("At least one redirect_uri must be registered with the client.");
}
return obtainMatchingRedirect(registeredRedirectUris, requestedRedirect);
}
- 再看 重定向定制的匹配函数
obtainMatchingRedirect
,他的方法参数有registeredRedirectUris
、requestedRedirect
这两个,其中requestedRedirect
是前端请求时携带的回调地址,registeredRedirectUris
这个则是我们注册到 OAuth2 允许对应客户端的回调地址。然后我们再接着看obtainMatchingRedirect
这个方法,代码如下 :
private String obtainMatchingRedirect(Set<String> redirectUris, String requestedRedirect) {
Assert.notEmpty(redirectUris, "Redirect URIs cannot be empty");
if (redirectUris.size() == 1 && requestedRedirect == null) {
return redirectUris.iterator().next();
}
for (String redirectUri : redirectUris) {
if (requestedRedirect != null && redirectMatches(requestedRedirect, redirectUri)) {
return requestedRedirect;
}
}
throw new RedirectMismatchException("Invalid redirect: " + requestedRedirect
+ " does not match one of the registered values: " + redirectUris.toString());
}
-Invalid redirect: " + requestedRedirect + " does not match one of the registered values
,看到这个就很亲切了,因为这个是我之前刚开始学的时候,经常能遇到的问题。
-
redirectMatches 这个就是回调地址具体的匹配函数,此处暂时不做详细介绍了,有兴趣的同学可以自己去研究。
-
回到我们的背景,我们的需求实时 看能不能实现单个 clientID 对应多个 web_server_redirect_uri (回调地址)。回到 resolveRedirect 这个方法,我们关注如何获取已注册的回调地址,代码如下:
Set<String> registeredRedirectUris = client.getRegisteredRedirectUri();
- 再接着进入
client.getRegisteredRedirectUri()
,方法如下:
@org.codehaus.jackson.annotate.JsonIgnore
@com.fasterxml.jackson.annotation.JsonIgnore
public Set<String> getRegisteredRedirectUri() {
return registeredRedirectUris;
}
@org.codehaus.jackson.annotate.JsonProperty("redirect_uri")
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = JacksonArrayOrStringDeserializer.class)
@com.fasterxml.jackson.annotation.JsonProperty("redirect_uri")
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = Jackson2ArrayOrStringDeserializer.class)
private Set<String> registeredRedirectUris;
- 我们看到上述的
getRegisteredRedirectUri
这个方法,其实就是获取 BaseClientDetails 这个对象的registeredRedirectUris
变量,这个变量在反序列化时,又会自动使用Jackson2ArrayOrStringDeserializer
和JacksonArrayOrStringDeserializer
这两个类的deserialize
方法,我们再看deserialize
这个方法,代码如下:
public class Jackson2ArrayOrStringDeserializer extends StdDeserializer<Set<String>> {
public Jackson2ArrayOrStringDeserializer() {
super(Set.class);
}
@Override
public JavaType getValueType() {
return SimpleType.construct(String.class);
}
@Override
public Set<String> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
JsonToken token = jp.getCurrentToken();
if (token.isScalarValue()) {
String list = jp.getText();
list = list.replaceAll("\\s+", ",");
return new LinkedHashSet<String>(Arrays.asList(StringUtils.commaDelimitedListToStringArray(list)));
}
return jp.readValueAs(new TypeReference<Set<String>>() {
});
}
}
-
如上,主要的函数就是
String.replaceAll()
和StringUtils.commaDelimitedListToStringArray()
这两个方法。第一个方法就是一个对字符串替换的函数,第二个则是对字符串根据某个特殊符号进行分割的函数。那么我们将如上代码进行解读:list = list.replaceAll("\\s+", ",");
- 上述方法
\\s+
这个是转义后的正则表达式,即使\s+
表示的就是对字符list
中存在大于等于1个空格的地方,将被,
来替换。
- 上述方法
StringUtils.commaDelimitedListToStringArray(list))
- 显而易见,这个防范就是对字符
list
进行按,
分割,将分割后的字符放入一个String[]
数组中。
- 显而易见,这个防范就是对字符
2.4.分析小结
OAuth 2.3.3 这个版本是支持单个CliendID 对应多个 web_server_redirect_uri
的,其中对 web_server_redirect_uri
的格式要求就是对 多个回调地址之间用 逗号 进行一个隔开即可。
3.问题解决
-
在
oauth_client_details
表中,对web_server_redirect_uri
这个字段存储多个回调地址时,在多个回调地址之间用英文小写逗号隔开。- 例如:
http://000.000.000.229:1234/login,http://000.000.0.01:9123/login
- 例如:
-
注: 如上描述如有错误,烦请各位朋友指出,共同学习共同进步。
-
文章原创,禁止转载。