《SpringSecurityOauth2源码分析》1.SpringSecurityOauth2源码分析之单点登陆SSO

OAuth2ClientContextFilter

处理UserRedirectRequiredException异常,做浏览器跳转。当用户跳转到本地login时,如果发现UserRedirectRequiredException异常,跳转认证中心 oauth/authorize接口,传参数response_type,client_id,redirect_uri,scope。 (很熟悉吧)

认证中心发现未登录转到登陆页面。 登陆完之后会跳转redirect_uri对应页面(客户端登录APi),这个请求带code参数的,

然后走过滤器链,发现OAuth2ClientAuthenticationProcessingFilter认证通过了。

转向缓存的请求(就是ExceptionTranslationFilter里面的requestCache.saveRequest(request, response);)。

OAuth2ClientAuthenticationProcessingFilter

继承AbstractAuthenticationProcessingFilter实现attemptAuthentication方法 。和UsernamePasswordFilter是一类。

	//在OAuth2ClientAuthenticationProcessingFilter中
     @Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {

		OAuth2AccessToken accessToken;
		try {
			accessToken = restTemplate.getAccessToken();
		} catch (OAuth2Exception e) {
			BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
			publish(new OAuth2AuthenticationFailureEvent(bad));
			throw bad;			
		}
		try {
			OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
			if (authenticationDetailsSource!=null) {
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
				result.setDetails(authenticationDetailsSource.buildDetails(request));
			}
			publish(new AuthenticationSuccessEvent(result));
			return result;
		}
		catch (InvalidTokenException e) {
			BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
			publish(new OAuth2AuthenticationFailureEvent(bad));
			throw bad;			
		}

	}

accessToken = restTemplate.getAccessToken(); 访问认证服务器获取token的方法。 如果发现没有code抛出UserRedirectRequiredException异常。  追了一下代码,在AuthorizationCodeAccessTokenProvider中抛出的UserRedirectRequiredException.

	   //在AuthorizationCodeAccessTokenProvider中
      public String obtainAuthorizationCode(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
			throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
			OAuth2AccessDeniedException {

		AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;

		HttpHeaders headers = getHeadersForAuthorizationRequest(request);
		MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>();
		if (request.containsKey(OAuth2Utils.USER_OAUTH_APPROVAL)) {
			form.set(OAuth2Utils.USER_OAUTH_APPROVAL, request.getFirst(OAuth2Utils.USER_OAUTH_APPROVAL));
			for (String scope : details.getScope()) {
				form.set(scopePrefix + scope, request.getFirst(OAuth2Utils.USER_OAUTH_APPROVAL));
			}
		}
		else {
			form.putAll(getParametersForAuthorizeRequest(resource, request));
		}
		authorizationRequestEnhancer.enhance(request, resource, form, headers);
		final AccessTokenRequest copy = request;

		final ResponseExtractor<ResponseEntity<Void>> delegate = getAuthorizationResponseExtractor();
		ResponseExtractor<ResponseEntity<Void>> extractor = new ResponseExtractor<ResponseEntity<Void>>() {
			@Override
			public ResponseEntity<Void> extractData(ClientHttpResponse response) throws IOException {
				if (response.getHeaders().containsKey("Set-Cookie")) {
					copy.setCookie(response.getHeaders().getFirst("Set-Cookie"));
				}
				return delegate.extractData(response);
			}
		};
		// Instead of using restTemplate.exchange we use an explicit response extractor here so it can be overridden by
		// subclasses
		ResponseEntity<Void> response = getRestTemplate().execute(resource.getUserAuthorizationUri(), HttpMethod.POST,
				getRequestCallback(resource, form, headers), extractor, form.toSingleValueMap());

		if (response.getStatusCode() == HttpStatus.OK) {
			// Need to re-submit with approval...
			throw getUserApprovalSignal(resource, request);
		}

		URI location = response.getHeaders().getLocation();
		String query = location.getQuery();
		Map<String, String> map = OAuth2Utils.extractMap(query);
		if (map.containsKey("state")) {
			request.setStateKey(map.get("state"));
			if (request.getPreservedState() == null) {
				String redirectUri = resource.getRedirectUri(request);
				if (redirectUri != null) {
					request.setPreservedState(redirectUri);
				}
				else {
					request.setPreservedState(new Object());
				}
			}
		}

		String code = map.get("code");
		if (code == null) {
			throw new UserRedirectRequiredException(location.toString(), form.toSingleValueMap());
		}
		request.set("code", code);
		return code;

	}

有点乱, 我们来模拟一下这个流程

 首先给拦截器排个顺序

OAuth2ClientContextFilter-->OAuth2ClientAuthenticationProcessingFilter-->ExceptionTranslationFilter-->FilterSecurityInterceptor

1. 客户端访问首页, 过滤器FilterSecurityInterceptor, 抛出异常,被ExceptionTranslationFilter处理,跳转客户端登录接口. 缓存原始请求.

2. 客户端登录,经过过滤器链, OAuth2ClientAuthenticationProcessingFilter 发现没code,抛出UserRedirectRequiredException异常. 异常被OAuth2ClientContextFilter处理,跳转认证服务器 oauth/authorize接口,传参数包括redirect_uri. 发现没有登录, 转到登录页面.

3. 登录完成之后, 认证服务器回调客户端redirect_uri(就是客户端登陆接口). 客户端登录,经过过滤器链, OAuth2ClientAuthenticationProcessingFilter根据code获取到token了,根据token获取了用户认证信息,然后结合token生成OAuth2Authentication证书.

4. 登录成功后拿出缓存的请求,执行.来到了客户端首页.

认证服务器中

AuthorizationEndpoint 负责发放code,

1.如果没有认证过,抛出InsufficientAuthenticationException异常,  异常被ExceptionTranslationFilter捕获,跳转服务端登录页面。

2.如果认证过,直接发送code。  (这也是单点登陆精髓之所在,上述模拟流程的第2步)

	//在AuthorizationEndpoint中
    @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));
				}
			}

			// Place auth request into the model so that it is stored in the session
			// for approveOrDeny to use. That way we make sure that auth request comes from the session,
			// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
			model.put("authorizationRequest", authorizationRequest);

			return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

		}
		catch (RuntimeException e) {
			sessionStatus.setComplete();
			throw e;
		}

	}

服务端如何判断是否认证呢?  还记得SecurityContextPersistenceFilter 吗?

SecurityContextPersistenceFilter

当一个请求来的时候,它会将session中的值传入到该线程中,当请求返回的时候,它会判断该请求线程是否有SecurityContext,如果有它会将其放入到session中,因此保证了请求结果可以在不同的请求之间共享。

客户端浏览器存了sessionId,所以能和认证服务器保持通信。httpsession以SPRING_SECURITY_CONTEXT为key,保存了用户认证信息,保证了认证信息请求之间共享。

总结:  SpringSecurityOauth2的sso用的 Authorization Code授权方式.    

          继承AbstractAuthenticationProcessingFilter我们也可以自定义认证流程.

猜你喜欢

转载自blog.csdn.net/kaige8312/article/details/83149951