当前位置:   article > 正文

SpringSecurity OAuth2授权端点AuthorizationEndpoint、授权码AuthorizationCodeServices 详解

authorizationendpoint

1、前言

  在《授权服务器是如何实现授权的呢?》中,我们可以了解到服务端实现授权的流程,同时了解"/oauth/authorize"授权接口在AuthorizationEndpoint类中定义。这一节,我们将详细分析AuthorizationEndpoint类的实现。

2、AbstractEndpoint抽象类

  AuthorizationEndpoint授权端点继承自AbstractEndpoint抽象类,这里我们先分析一下AbstractEndpoint抽象类的实现逻辑。

  AbstractEndpoint抽象类实现了InitializingBean接口,这样在afterPropertiesSet()方法中就可以实现一些字段的校验和初始化工作了。主要实现了TokenGranter、ClientDetailsService、OAuth2RequestFactory和WebResponseExceptionTranslator等对象初始化校验工作,保证了tokenGranter和clientDetailsService不能为空,oAuth2RequestFactory如果为空,则使用默认的DefaultOAuth2RequestFactory对象。同时还提供了这些属性字段的get、set等方法。

public class AbstractEndpoint implements InitializingBean {

	protected final Log logger = LogFactory.getLog(getClass());

	private WebResponseExceptionTranslator<OAuth2Exception> providerExceptionHandler = new DefaultWebResponseExceptionTranslator();

	private TokenGranter tokenGranter;

	private ClientDetailsService clientDetailsService;

	private OAuth2RequestFactory oAuth2RequestFactory;

	private OAuth2RequestFactory defaultOAuth2RequestFactory;

	public void afterPropertiesSet() throws Exception {
		Assert.state(tokenGranter != null, "TokenGranter must be provided");
		Assert.state(clientDetailsService != null, "ClientDetailsService must be provided");
		defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(getClientDetailsService());
		if (oAuth2RequestFactory == null) {
			oAuth2RequestFactory = defaultOAuth2RequestFactory;
		}
	}

	//省略……
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

3、AuthorizationEndpoint 授权

  AuthorizationEndpoint是OAuth2规范中的授权端点的实现。接收授权请求,处理用户授权,该请求需要经过认证的用户才能正常访问。在该类中,定义了"/oauth/authorize"接口,用于处理客户端的授权请求。

3.1、"/oauth/authorize"接口 客户端授权

  当重定向到授权服务器的"/oauth/authorize"请求时,比如“http://localhost:8080/oauth/authorize?response_type=code&client_id=resource1&scope=all&state=XXX&redirect_uri=XXX”,这个时候访问的就是"/oauth/authorize",对应的是AuthorizationEndpoint类的authorize()方法,实现逻辑如下:

@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
		SessionStatus sessionStatus, Principal principal) {
	//通过OAuth2RequestFactory创建授权请求对象AuthorizationRequest,其中OAuth2RequestFactory对象在抽象类中已经完成了初始化
	AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
	//获取responseTypes类型参数并进行判断,仅支持token或code两种
	Set<String> responseTypes = authorizationRequest.getResponseTypes();
	if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
		throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
	}
	//校验clientId,不能为空
	if (authorizationRequest.getClientId() == null) {
		throw new InvalidClientException("A client id must be provided");
	}

	try {
		//校验principal,首先principal对象必须是Authentication类型的,同时必须是已经授权的用户信息,所在在进行用户登录授权时,第一次访问该方法时,因为用户未经授权,所以会抛出InsufficientAuthenticationException异常,然后就会跳转到统一的登录页面
		if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
			throw new InsufficientAuthenticationException(
					"User must be authenticated with Spring Security before authorization can be completed.");
		}
		//通过ClientDetailsService获取客户端详细信息-ClientDetails对象,其中,ClientDetailsService对象在抽象类中已经完成了初始化
		ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

		//解析实际跳转路径地址resolvedRedirect参数,并保存到authorizationRequest对象中。其中,通过redirectResolver.resolveRedirect()方法,验证grantTypes类型,只支持implicit 或 authorization_code,同时验证请求中的redirectUri链接是否已经在客户端信息中进行注册,没有注册就会抛出RedirectMismatchException异常。
		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);

		//校验请求中的scope和客户端中注册的scope是否匹配,不匹配就抛出InvalidScopeException。
		oauth2RequestValidator.validateScope(authorizationRequest, client);

		//检验用户是否授权,默认使用的是DefaultUserApprovalHandler对象,实现逻辑等同于是直接判断authorizationRequest对象的approved属性
		authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
				(Authentication) principal);
		boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
		authorizationRequest.setApproved(approved);

		// 如果已经经过用户授权,如果responseTypes包含token这调用getImplicitGrantResponse()方法,进行跳转,如果responseTypes包含code,则调用getAuthorizationCodeResponse()方法,进行跳转。
		if (authorizationRequest.isApproved()) {
			if (responseTypes.contains("token")) {
				return getImplicitGrantResponse(authorizationRequest);
			}
			if (responseTypes.contains("code")) {
				return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
						(Authentication) principal));
			}
		}

		// 如果没有经过用户授权,就会保存authorizationRequest对象,然后调用getUserApprovalPageResponse()方法,跳转到"forward:/oauth/confirm_access";即,登录授权确认页面,该地址在WhitelabelApprovalEndpoint类中定义,并提供了授权确认的接口"/oauth/confirm_access"。
		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;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
3.2、"/oauth/authorize" POST接口 用户同意授权

  当重定向到授权确认页面时,用户选择同意或者不同意进行登录授权,这个时候会提交“http://localhost:8080/oauth/authorize” POST请求,对应AuthorizationEndpoint类的approveOrDeny()方法,参数如下(下面参数是同意授权):
在这里插入图片描述
  这个时候,就会请求到AuthorizationEndpoint提供的"/oauth/authorize" POST 请求,即用户同意授权请求,该方法处理逻辑如下:

@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model,
		SessionStatus sessionStatus, Principal principal) {
	//验证principal参数是否是Authentication类型,如果不是直接抛出InsufficientAuthenticationException异常
	if (!(principal instanceof Authentication)) {
		sessionStatus.setComplete();
		throw new InsufficientAuthenticationException(
				"User must be authenticated with Spring Security before authorizing an access token.");
	}
	//通过model参数获取AuthorizationRequest对象,该参数在上一步骤("/oauth/authorize")中,被存储到了参数中进行传递。如果该参数没有获取到,直接抛出InvalidRequestException异常。然后再比较authorizationRequest参数是否发生变化,如果监测到变化,就抛出InvalidRequestException异常。
	AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get(AUTHORIZATION_REQUEST_ATTR_NAME);
	if (authorizationRequest == null) {
		sessionStatus.setComplete();
		throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
	}
	// 判断authorizationRequest参数是否发生变化,如果发生变化,说明请求发生了变化就会抛出InvalidRequestException异常
	@SuppressWarnings("unchecked")
	Map<String, Object> originalAuthorizationRequest = (Map<String, Object>) model.get(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME);
	if (isAuthorizationRequestModified(authorizationRequest, originalAuthorizationRequest)) {
		throw new InvalidRequestException("Changes were detected from the original authorization request.");
	}

	try {
		//获取当前授权请求的responseTypes参数
		Set<String> responseTypes = authorizationRequest.getResponseTypes();
		//检验用户是否授权,默认使用的是DefaultUserApprovalHandler对象,实现逻辑等同于是直接判断authorizationRequest对象的approved属性,并进行保存
		authorizationRequest.setApprovalParameters(approvalParameters);
		authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest,
				(Authentication) principal);
		boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
		authorizationRequest.setApproved(approved);
		//判断当前授权请求redirectUri是否为空,如果为空,则抛出InvalidRequestException异常
		if (authorizationRequest.getRedirectUri() == null) {
			sessionStatus.setComplete();
			throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
		}
		//判断当前授权请求是否已经被授权,如果未被授权,说明出现了异常,调用getUnsuccessfulRedirect()方法,并返回RedirectView对象。
		if (!authorizationRequest.isApproved()) {
			return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
					new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")),
					false, true, false);
		}
		//如果responseTypes包含token这调用getImplicitGrantResponse()方法,进行跳转
		if (responseTypes.contains("token")) {
			return getImplicitGrantResponse(authorizationRequest).getView();
		}
		//否则,调用getAuthorizationCodeResponse()方法,进行跳转。即跳转地址为“http://localhost:8100/login/oauth2/code/qriver?code=afGuXC&state=XXX”,这个方法中会调用generateCode()方法,生成授权码字符串,实际上是由authorizationCodeServices创建并管理的。
		return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
	}
	finally {
		sessionStatus.setComplete();
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
3.3、getUserApprovalPageResponse() 方法

  在"/oauth/authorize"接口对应authorize()方法中,调用了getUserApprovalPageResponse() 方法,实现了创建向授权同意页面跳转的视图。默认跳转地址为:“forward:/oauth/confirm_access”。

private ModelAndView getUserApprovalPageResponse(Map<String, Object> model,
	AuthorizationRequest authorizationRequest, Authentication principal) {
	if (logger.isDebugEnabled()) {
		logger.debug("Loading user approval page: " + userApprovalPage);
	}
	model.putAll(userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal));
	return new ModelAndView(userApprovalPage, model);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  其中,“/oauth/confirm_access”接口对应定义在WhitelabelApprovalEndpoint类中,提供了默认的同意授权视图页面,通过重新配置userApprovalPage地址,可以实现自定义页面的配置。

@FrameworkEndpoint
@SessionAttributes("authorizationRequest")
public class WhitelabelApprovalEndpoint {

	@RequestMapping("/oauth/confirm_access")
	public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
		final String approvalContent = createTemplate(model, request);
		if (request.getAttribute("_csrf") != null) {
			model.put("_csrf", request.getAttribute("_csrf"));
		}
		View approvalView = new View() {
			@Override
			public String getContentType() {
				return "text/html";
			}

			@Override
			public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
				response.setContentType(getContentType());
				response.getWriter().append(approvalContent);
			}
		};
		return new ModelAndView(approvalView, model);
	}

	protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
		//省略……
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
3.4、getImplicitGrantResponse()方法

  当responseType=token时,该方法会被调用,主要用于implicit授权方式。在该方法中,调用getAccessTokenForImplicitGrant()方法获取accessToken,然后调用appendAccessToken()方法封装视图参数,最后返回ModelAndView()视图。

private ModelAndView getImplicitGrantResponse(AuthorizationRequest authorizationRequest) {
try {
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(authorizationRequest, "implicit");
		OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
		OAuth2AccessToken accessToken = getAccessTokenForImplicitGrant(tokenRequest, storedOAuth2Request);
		if (accessToken == null) {
			throw new UnsupportedResponseTypeException("Unsupported response type: token");
		}
		return new ModelAndView(new RedirectView(appendAccessToken(authorizationRequest, accessToken), false, true,
				false));
	}
	catch (OAuth2Exception e) {
		return new ModelAndView(new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, true), false,
				true, false));
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

  getAccessTokenForImplicitGrant()方法的实现逻辑如下,主要通过调用TokenGranter的grant()方法生成对应的accessToken,这里不再展开分析,后期再Token端点的学习中再展开分析。

private OAuth2AccessToken getAccessTokenForImplicitGrant(TokenRequest tokenRequest,
		OAuth2Request storedOAuth2Request) {
	OAuth2AccessToken accessToken = null;
	// These 1 method calls have to be atomic, otherwise the ImplicitGrantService can have a race condition where
	// one thread removes the token request before another has a chance to redeem it.
	synchronized (this.implicitLock) {
		accessToken = getTokenGranter().grant("implicit",
				new ImplicitTokenRequest(tokenRequest, storedOAuth2Request));
	}
	return accessToken;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
3.5、getAuthorizationCodeResponse()方法

  当使用授权码模式时,该方法才会被调用,在getImplicitGrantResponse()方法中会直接返回token,而这里则是生成对应的code进行返回,然后再由客户端拿着code来兑换对应的token。具体实现如下:

private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) {
try {
		return new RedirectView(getSuccessfulRedirect(authorizationRequest,
				generateCode(authorizationRequest, authUser)), false, true, false);
	}
	catch (OAuth2Exception e) {
		return new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, false), false, true, false);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  在getAuthorizationCodeResponse()方法中,首先调用generateCode()方法生成对应code,然后再调用getSuccessfulRedirect()方法,封装返回视图需要的参数。

  generateCode()方法的实现如下,在该方法中,主要通过调用authorizationCodeServices的createAuthorizationCode()方法实现了授权码code的生成。

private String generateCode(AuthorizationRequest authorizationRequest, Authentication authentication)
		throws AuthenticationException {

	try {
		OAuth2Request storedOAuth2Request = getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);

		OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, authentication);
		String code = authorizationCodeServices.createAuthorizationCode(combinedAuth);
		return code;
	}
	catch (OAuth2Exception e) {
		if (authorizationRequest.getState() != null) {
			e.addAdditionalInformation("state", authorizationRequest.getState());
		}
		throw e;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

  getSuccessfulRedirect()方法,实现授权码code的封装,实现如下:

private String getSuccessfulRedirect(AuthorizationRequest authorizationRequest, String authorizationCode) {

	if (authorizationCode == null) {
		throw new IllegalStateException("No authorization code found in the current request scope.");
	}

	Map<String, String> query = new LinkedHashMap<String, String>();
	query.put("code", authorizationCode);

	String state = authorizationRequest.getState();
	if (state != null) {
		query.put("state", state);
	}

	return append(authorizationRequest.getRedirectUri(), query, false);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4、AuthorizationCodeServices 授权码管理

  在前面generateCode()方法中,实现了授权码的产生,这里我们详细分析一下AuthorizationCodeServices接口及其实现类,看看授权码是如何产生和被消费的。

  AuthorizationCodeServices 用来管理授权码,提供了创建授权码、消费授权码等方法。框架提供了InMemoryAuthorizationCodeServices和JdbcAuthorizationCodeServices两个实现类,默认使用InMemoryAuthorizationCodeServices实例对象。
在这里插入图片描述

AuthorizationCodeServices接口
public interface AuthorizationCodeServices {
	//创建授权码
	String createAuthorizationCode(OAuth2Authentication authentication);
	//消费授权码
	OAuth2Authentication consumeAuthorizationCode(String code)
			throws InvalidGrantException;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

  在AuthorizationCodeServices接口中定义了两个方法:第一个是创建授权码code方法createAuthorizationCode(),第二个方法是消费授权码方法consumeAuthorizationCode()方法。

抽象实现类RandomValueAuthorizationCodeServices

  引入了RandomValueStringGenerator对象,用于创建授权码。同时,增加了store()和remove()两个抽象方法,分别用于存储授权码和移除授权码。

public abstract class RandomValueAuthorizationCodeServices implements AuthorizationCodeServices {

	private RandomValueStringGenerator generator = new RandomValueStringGenerator();

	protected abstract void store(String code, OAuth2Authentication authentication);

	protected abstract OAuth2Authentication remove(String code);

	public String createAuthorizationCode(OAuth2Authentication authentication) {
		String code = generator.generate();
		store(code, authentication);
		return code;
	}

	public OAuth2Authentication consumeAuthorizationCode(String code)
			throws InvalidGrantException {
		OAuth2Authentication auth = this.remove(code);
		if (auth == null) {
			throw new InvalidGrantException("Invalid authorization code: " + code);
		}
		return auth;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
默认实现类InMemoryAuthorizationCodeServices

  默认实现类InMemoryAuthorizationCodeServices,继承自抽象类RandomValueAuthorizationCodeServices,实现了store()、remove()两个方法,这里定义了一个authorizationCodeStore变量(ConcurrentHashMap<String, OAuth2Authentication>类型),用于存储授权码。

public class InMemoryAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {

	protected final ConcurrentHashMap<String, OAuth2Authentication> authorizationCodeStore = new ConcurrentHashMap<String, OAuth2Authentication>();

	@Override
	protected void store(String code, OAuth2Authentication authentication) {
		this.authorizationCodeStore.put(code, authentication);
	}

	@Override
	public OAuth2Authentication remove(String code) {
		OAuth2Authentication auth = this.authorizationCodeStore.remove(code);
		return auth;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

  JdbcAuthorizationCodeServices实现类,和InMemoryAuthorizationCodeServices类似,只不过是存储到了数据库中,这里不再重复贴出代码了。

  在这里,我们提到了产生授权码的时机,也分析了授权码管理类的实现方式,那么授权码是什么时候被消费的呢?这里涉及到了AuthorizationCodeTokenGranter类,我们在后续的章节中再专门分析该类的实现和使用方式。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/84080
推荐阅读
相关标签
  

闽ICP备14008679号