SpringCloud Security + OAuth2 集成之回调地址拓展

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,他的方法参数有 registeredRedirectUrisrequestedRedirect 这两个,其中 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 变量,这个变量在反序列化时,又会自动使用 Jackson2ArrayOrStringDeserializerJacksonArrayOrStringDeserializer 这两个类的 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() 这两个方法。第一个方法就是一个对字符串替换的函数,第二个则是对字符串根据某个特殊符号进行分割的函数。那么我们将如上代码进行解读:

    1. list = list.replaceAll("\\s+", ",");
      • 上述方法 \\s+ 这个是转义后的正则表达式,即使 \s+ 表示的就是对字符 list 中存在大于等于1个空格的地方,将被 ,来替换。
    2. 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
  • 注: 如上描述如有错误,烦请各位朋友指出,共同学习共同进步。

  • 文章原创,禁止转载。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值