基于SpringSecurity OAuth2实现单点登录——授权服务器是如何实现授权的呢?

1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》

1、前言

  在前面的几篇博文中,我们分析了实现单点登录过程中是如何实现从访问http://localhost:8082/index重定向到http://localhost:8082/login,然后又重定向到授权服务器http://localhost:8080/oauth/authorize地址上的。到目前为止,我们的请求已经从应用A到授权服务器了,那授权服务器是如何实现登录认证的呢?我们下面开始跟着代码进行分析。

2、重定向到授权登录页

  当从应用A重定向到授权服务器http://localhost:8080/oauth/authorize地址时,这时候就需要从授权服务器的角度来进行分析了。

  首先,当我们访问http://localhost:8080/oauth/authorize地址时,这个时候,也会经过一系列的过滤器,不过最终还是进入到了/oauth/authorize对应的方法中,代码如下:

//AuthorizationEndpoint.java
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
		SessionStatus sessionStatus, Principal principal) {
		
	//省略 …… 
	
	try {

		if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
			throw new InsufficientAuthenticationException(
					"User must be authenticated with Spring Security before authorization can be completed.");
		}
	//省略 …… 
}

  这个时候,请求中的principal参数为空,即没有认证信息,这个时候就会抛出InsufficientAuthenticationException异常,然后被SpringSecurity过滤器链中的异常过滤器拦截,并重定向到授权服务器的登录地址(和重定向到应用A的登录地址类似),因为这里是授权服务器,所以最终会向用户呈现出统一登录的界面。

3、授权服务器 如何进行登录认证?

  在统一登录页,用户输入用户名密码后,点击“登录”按钮后,会向授权服务器发送一个http://localhost:8080/login请求(POST类型),这个时候会携带用户名密码到授权服务器,而授权服务器端的SpringSecurity过滤器链中有如下的过滤器,需要注意,和应用A过滤器链有一些区别,如下所示:
在这里插入图片描述
  在登录验证这一步,授权服务器其实和普通的SpringSecurity应用是没有区别的,就是通过UsernamePasswordAuthenticationFilter过滤器验证用户名密码的有效性,如果登录成功就执行后续successfulAuthentication()方法,具体实现如下:

//AbstractAuthenticationProcessingFilter.java
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
	throws IOException, ServletException {

	// 省略 ……

	Authentication authResult;
	try {
		//实际调用了实现类UsernamePasswordAuthenticationFilter中的实现
		authResult = attemptAuthentication(request, response);
		if (authResult == null) {
			return;
		}
		sessionStrategy.onAuthentication(authResult, request, response);
	}catch (InternalAuthenticationServiceException failed) {
		logger.error(
				"An internal error occurred while trying to authenticate the user.",
				failed);
		unsuccessfulAuthentication(request, response, failed);
		return;
	}catch (AuthenticationException failed) {
		// Authentication failed
		unsuccessfulAuthentication(request, response, failed);
		return;
	}
	// Authentication success
	if (continueChainBeforeSuccessfulAuthentication) {
		chain.doFilter(request, response);
	}
	successfulAuthentication(request, response, chain, authResult);
}

  如果输入正确的用户名密码,attemptAuthentication()方法会正常返回,最后开始执行successfulAuthentication()方法,用于处理登录成功的后续逻辑。

  在successfulAuthentication()方法中,又调用了SavedRequestAwareAuthenticationSuccessHandler的onAuthenticationSuccess()方法,而onAuthenticationSuccess()方法中实现了地址(localhost:8080/oauth/authorize)的重定向,具体如下:

//SavedRequestAwareAuthenticationSuccessHandler.java
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
		HttpServletResponse response, Authentication authentication)
		throws ServletException, IOException {
	SavedRequest savedRequest = requestCache.getRequest(request, response);
	
	// 省略 …… 
	
	String targetUrl = savedRequest.getRedirectUrl();
	logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
	//targetUrl 为 localhost:8080/oauth/authorize
	getRedirectStrategy().sendRedirect(request, response, targetUrl);
}

  经过上述方法onAuthenticationSuccess()内实现的重定向,又重新回到了前面提到的授权地址(AuthorizationEndpoint的authorize()方法),在前面提到因为没有认证,所以直接抛出异常,最后跳转到了登录页,而这个时候我们已经进行了认证,所以就可以执行后续的代码了,这里主要实现了重定向到应用A,具体如下:

@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
		SessionStatus sessionStatus, Principal principal) {

	//省略 ……
	
	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());

		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);

		oauth2RequestValidator.validateScope(authorizationRequest, client);
		
		authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
				(Authentication) principal);
		boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
		authorizationRequest.setApproved(approved);

		if (authorizationRequest.isApproved()) {
			if (responseTypes.contains("token")) {
				return getImplicitGrantResponse(authorizationRequest);
			}
			if (responseTypes.contains("code")) {
				//重定向到 应用A的登录地址,不过这个时候,就会携带code参数值
				return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
						(Authentication) principal));
			}
		}
		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;
	}
}

4、应用A请求获取accessToken

  在授权服务器完成了登录认证后,就会重定向到了应用A上了,不过这次携带了code参数,这个参数也是前面经历这么多过程为了实现的目的。后续,我们可以使用这个code来换取accessToken了。

  回到应用A后,重定向的地址是http://localhost:8082/login?code=A2LWAK&state=25p7td,即应用A的登录地址,不过这次会携带code参数。

  访问http://localhost:8082/login时,又重新开始经过SpringSecurity的过滤器,当经过OAuth2ClientAuthenticationProcessingFilter过滤器时,会调用doFilter()方法,进而调用attemptAuthentication()方法进行登录验证(抽象类中定义的方法,然后在OAuth2ClientAuthenticationProcessingFilter类中实现)。

  在attemptAuthentication()方法中,又调用了OAuth2RestTemplate的getAccessToken()方法获取accessToken,而在getAccessToken()方法中,又调用了acquireAccessToken()方法获取accessToken,前面在《应用A是如何重定向到授权服务器的授权地址呢?》中,已经分析了该过程,这里不再重复贴出代码了。

  在acquireAccessToken()方法中,又调用了AuthorizationCodeAccessTokenProvider的obtainAccessToken()方法获取accessToken,具体实现如下:

//AuthorizationCodeAccessTokenProvider.java
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
		throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
		OAuth2AccessDeniedException {

	AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;
	//不携带code参数时,通过obtainAuthorizationCode()方法去获取授权码
	if (request.getAuthorizationCode() == null) {
		if (request.getStateKey() == null) {
			throw getRedirectForAuthorization(resource, request);
		}
		obtainAuthorizationCode(resource, request);
	}
	//携带code参数时,通过retrieveToken()方法获取accessToken
	return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
			getHeadersForTokenRequest(request));
}

  在上一篇博文中,因为没有携带code参数值,所以调用了obtainAuthorizationCode()方法,而这时候,因为携带了code参数值,所以会直接调用retrieveToken()方法获取accessToken。而retrieveToken()方法的实现如下:

protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
	MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {

	try {
		// 省略 ……
		
		return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),
				getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());

	}
	// 省略 catch代码块……

}

  在retrieveToken()方法中,又会通过getRestTemplate().execute()方法去授权服务器获取accessToken。这个时候,向授权服务器发送的请求地址是http://localhost:8080/oauth/token,即在配置文件中配置的access-token-uri参数值。

5、授权服务器生成 accessToken

  在应用A执行retrieveToken()方法后,就回到了授权服务器进行验证,并生成应用A需要的accessToken。

  首先进入了/oauth/token对应的接口方法,即TokenEndpoint类的postAccessToken()方法,实现如下:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

	//省略 ……
	
	//生成 accessToken
	OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
	if (token == null) {
		throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
	}
	return getResponse(token);
}

  在postAccessToken()方法中,通过grant()方法生成accessToken,并返回到了应用A。

6、应用A 获取 accessToken

  经过前面的流程,把code换成了accessToken,当应用A获取到accessToken后,然后会通过UserInfoTokenServices.loadAuthentication()方法,使用accessToken换取具体的用户信息,其中loadAuthentication()方法会根据配置中的security.oauth2.resource.user-info-uri请求资源服务器中的用户信息,实现如下:

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

	OAuth2AccessToken accessToken;
	try {
		//获取accessToken
		accessToken = restTemplate.getAccessToken();
	} catch (OAuth2Exception e) {
		BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
		publish(new OAuth2AuthenticationFailureEvent(bad));
		throw bad;			
	}
	try {
		//请求资源服务器,获取用户信息,即根据配置中的security.oauth2.resource.user-info-uri值获取用户信息
		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;			
	}
}

  经过上述attemptAuthentication()方法,首先完成了通过code换取accessToken,然后又根据accessToken获取用户信息。这个时候,实现了应用A的认证登录,然后就会继续执行OAuth2ClientAuthenticationProcessingFilter的doFilter()方法(实际上在父类AbstractAuthenticationProcessingFilter中定义)中后续的代码,最终通过successfulAuthentication()方法实现了最终访问地址的重定向,即重定向到了最初访问的http://localhost:8082/index地址上。

7、写在最后

  至此,我们就完成了应用A,从访问、授权服务器认证,然后跳转到访问地址的全部流程。后续,我们将继续分析,如果这个时候访问应用B又会发生什么呢?敬请期待!!!

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姠惢荇者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值