背景
oauth2.0 授权码模式登录,需要对重定向 uri 进行白名单校验
默认规则:SpingSecurity 关于重定向 uri 白名单的校验是使用”等于“,即重定向 uri 需要完全等于白名单内其中一个 uri 才能通过检测
需求:重定向 uri 的域名等于白名单内其中一个 uri 即可通过检测
分析
经过百度和谷歌一通搜索,并没有找到相关的文章,而 SpingSecurity 的官网解析得也比较难懂,所以决定通过 debug 寻找切入点
- 授权码登录请求 /oauth/authorize,类名为 AuthorizationEndpoint,对重定向 uri 的校验位于 redirectResolver.resolveRedirect 方法
-
进入接口 RedirectResolver
-
接口默认实现类为 DefaultRedirectResolver,校验方法为 obtainMatchingRedirect(registeredRedirectUris, requestedRedirect)
-
进入 obtainMatchingRedirect 方法,发现校验关键方法为 redirectMatches(requestedRedirect, redirectUri)
-
进入 redirectMatches,可以看到需要域名、端口号、路径、请求参数都一致才能校验通过
分析到这里,已经找到关键点了,下面就是如何修改了
优化
- 创建新类 RedirectResolver,实现 RedirectResolver 接口
- 重写 redirectMatches 方法,修改为只对重定向 uri 域名的校验
- 重写 obtainMatchingRedirect 方法
- AuthorizationServerEndpointsConfigurer 载入 RedirectResolver
redirectMatches 方法,只保留对 host 域名、port 端口的校验
@Override
protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();
boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
boolean portMatch = matchPorts ? registeredRedirectUri.getPort() == requestedRedirectUri.getPort() : true;
return schemeMatch && userInfoMatch && hostMatch && portMatch;
}
obtainMatchingRedirect 方法,重定向 uri 修改为请求 uri
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.");
}
RedirectResolver 全貌
注意: obtainMatchingRedirect 方法为 private,不能重写,所以需要重写 resolveRedirect 方法
@Configuration
public class RedirectResolver extends DefaultRedirectResolver {
/**
* 重定向
*
* @param redirectUris 白名单
* @param requestedRedirect 请求uri
* @return
*/
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.");
}
/**
* 判断重定向 uri 是否符合规则
*
* @param requestedRedirect 请求uri
* @param redirectUri 白名单
* @return
*/
@Override
protected boolean redirectMatches(String requestedRedirect, String redirectUri) {
UriComponents requestedRedirectUri = UriComponentsBuilder.fromUriString(requestedRedirect).build();
UriComponents registeredRedirectUri = UriComponentsBuilder.fromUriString(redirectUri).build();
boolean schemeMatch = isEqual(registeredRedirectUri.getScheme(), requestedRedirectUri.getScheme());
boolean userInfoMatch = isEqual(registeredRedirectUri.getUserInfo(), requestedRedirectUri.getUserInfo());
boolean hostMatch = hostMatches(registeredRedirectUri.getHost(), requestedRedirectUri.getHost());
boolean portMatch = matchPorts ? registeredRedirectUri.getPort() == requestedRedirectUri.getPort() : true;
return schemeMatch && userInfoMatch && hostMatch && portMatch;
}
/**
* 父类函数
*/
private boolean matchPorts = true;
private Collection<String> redirectGrantTypes = Arrays.asList("implicit", "authorization_code");
private boolean matchSubdomains = false;
private boolean isEqual(String str1, String str2) {
if (StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)) {
return true;
} else if (!StringUtils.isEmpty(str1)) {
return str1.equals(str2);
} else {
return false;
}
}
@Override
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);
}
/**
* @param grantTypes some grant types
* @return true if the supplied grant types includes one or more of the redirect types
*/
private boolean containsRedirectGrantType(Set<String> grantTypes) {
for (String type : grantTypes) {
if (redirectGrantTypes.contains(type)) {
return true;
}
}
return false;
}
}
AuthorizationServerEndpointsConfigurer 载入 RedirectResolver
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
RedirectResolver redirectResolver;
...
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
...
endpoints.redirectResolver(redirectResolver);
...
}
}