SpringOauth2.0源码分析之认证流程分析(一)

1.概述

本专题的源码分析均以用户名密码认证模式进行叙述。在此先了解下用户名密码认证的协议流程说明:

在这里插入图片描述
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
(B)步骤:客户端发出https请求。

从用户名密码认证方式可以看出,在获取access_token的过程中,将用户名和密码完全交给客户端,然后客户端向服务器认证,这种方式是对客户端充分的信任的情况下才能使用。用户名密码认证,是将code码认证中的资源拥有者确认授权的过程进行了整合。减少了授权的操作步骤,当然也带来了一定的不安全性。

2.代码实现

用户名密码认证的实现方式,请参考: SpringOauth2.0 用户名密码验证方式(二)

3.流程说明:
3.1 客户端请求,采用PostMan的方式
3.1.1 请求参数:

首先需要添加客户端的基础认证方式,这里Type选择:Base Auth ,用户名和密码为客户端用户名和密码。
在这里插入图片描述
在请求参数中,需要输入,认证类型:password,scope类型:all,用户的用户名和密码
在这里插入图片描述

3.2 SpringOauth2.0 过滤链

当请求:http://localhost:8080/oauth/token 接口,并不会直接请求到这个接口。而是先经过SpringSecurity的一系列的过滤器,其中具体使用哪个过滤器,通过FilterChainProxy类进行控制。SpringSecurity的过滤器链默认是12个,如下:
在这里插入图片描述
可以发现所有的过滤器都被存放到一个additionalFilters的List集合中,默认的大小是12个。其中绝大部分都是SpringSecurity自身带的过滤器。使用哪个过滤器是通过FilterChainProxy进行代理实现:核心代码如下:

@Override
public void doFilter(ServletRequest request, ServletResponse response)
		throws IOException, ServletException {
	// currentPosition 当前过滤器的偏移量
	if (currentPosition == size) {
		if (logger.isDebugEnabled()) {
			logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
					+ " reached end of additional filter chain; proceeding with original chain");
		}
		// Deactivate path stripping as we exit the security filter chain
		this.firewalledRequest.reset();
		originalChain.doFilter(request, response);
	}
	else {
	    // 每次调用过滤器,都会自增偏移量
		currentPosition++;
		// 获取下一个过滤链
		Filter nextFilter = additionalFilters.get(currentPosition - 1);
		if (logger.isDebugEnabled()) {
			logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
					+ " at position " + currentPosition + " of " + size
					+ " in additional filter chain; firing Filter: '"
					+ nextFilter.getClass().getSimpleName() + "'");
		}
		nextFilter.doFilter(request, response, this);
	}
}
}

从FilterChainProxy的源码可以看出,其内部维护了一个偏移量:private int currentPosition = 0 记录当前过滤链的位置,获取下一个过滤链,通过 Filter nextFilter = additionalFilters.get(currentPosition - 1) 获取。过滤器的顺序,就是上面所说的additionalFilters 集合中的顺序。过滤器的实现细节分析,后续文章会详细说明。

3.3 SpringOauth2.0 客户端认证

在用户名密码模式认证中,首先需要进行客户端认证。客户端的认证逻辑是通过:BasicAuthenticationFilter 进行具体的客户端认证:

@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain)
					throws IOException, ServletException {
		final boolean debug = this.logger.isDebugEnabled();
		// 获取客户端请求的:Authorization basic 中的参数。客户端用户名和密码,进行解码
		String header = request.getHeader("Authorization");
		if (header == null || !header.toLowerCase().startsWith("basic ")) {
			chain.doFilter(request, response);
			return;
		}
		try {
			String[] tokens = extractAndDecodeHeader(header, request);
			assert tokens.length == 2;
			String username = tokens[0];
			if (debug) {
				this.logger
						.debug("Basic Authentication Authorization header found for user '"
								+ username + "'");
			}

			if (authenticationIsRequired(username)) {
				UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
						username, tokens[1]);
				authRequest.setDetails(
						this.authenticationDetailsSource.buildDetails(request));
				// 通过认证管理器,进行客户端用户名密码认证
				Authentication authResult = this.authenticationManager
						.authenticate(authRequest);
				if (debug) {
					this.logger.debug("Authentication success: " + authResult);
				}
				SecurityContextHolder.getContext().setAuthentication(authResult);
				this.rememberMeServices.loginSuccess(request, response, authResult);
				onSuccessfulAuthentication(request, response, authResult);
			}

		}
		catch (AuthenticationException failed) {
		// 客户端用户名密码认证失败,抛出异常
			SecurityContextHolder.clearContext();
			if (debug) {
				this.logger.debug("Authentication request for failed: " + failed);
			}
			this.rememberMeServices.loginFail(request, response);
			onUnsuccessfulAuthentication(request, response, failed);
			if (this.ignoreFailure) {
				chain.doFilter(request, response);
			}
			else {
				this.authenticationEntryPoint.commence(request, response, failed);
			}
			return;
		}
		chain.doFilter(request, response);
	}

BasicAuthenticationFilter 通过解析前端传递过来的客户端的用户名和密码,进行反序列化,获取明文。然后通过AuthenticationManager进行认证。这里的认证其实就是查询当前系统中,是否存在当前客户端。实现原理后期分析。认证通过以后,会把当前信息写入到SecurityContextHolder.getContext().setAuthentication()中。进行保存。

3.3 获取 access_token

经过前面所有的过滤器以后,才真正的请求到客户端发起的请求地址:/oauth/token。这个请求地址,是SpringOauth2.0给封装好的。具体的实现代码在类:TokenEndpoint 中。具体核心代码如下:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
	// 判断当前客户端是否认证成功
		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					"There is no client authentication. Try adding an appropriate authentication filter.");
		}
		String clientId = getClientId(principal);
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
		// 封装TokenRequest对象
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
		if (clientId != null && !clientId.equals("")) {
			// Only validate the client details if a client authenticated during this
			// request.
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException("Given client ID does not match authenticated client");
			}
		}
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
			throw new InvalidRequestException("Missing grant type");
		}
		if (tokenRequest.getGrantType().equals("implicit")) {
			throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
		}
		if (isAuthCodeRequest(parameters)) {
			// The scope was requested or determined during the authorization step
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug("Clearing scope of incoming token request");
				tokenRequest.setScope(Collections.<String> emptySet());
			}
		}
		if (isRefreshTokenRequest(parameters)) {
			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
		}
		// 获取授权Token。封装在OAuth2AccessToken 中
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}
		return getResponse(token);
	}

首先验证当前的client是否已经认证成功,如果认证失败的话,直接抛出异常。认证通过,将当前的客户端信息和参数信息封装成TokenRequest对象。根据TokenRequest对象,调用授权方法,最终生成OAuth2AccessToken对象。其中封装了当前用户的权限认证和access_token信息。


4.结语

至此,SpringOauth2.0的用户名密码认证模式的大致流程已经走完。通过过滤器:BasicAuthenticationFilter,验证客户端用户名密码。过滤器链验证通过之后,会请求到真正的接口:/oauth/token。在这个接口里面,主要实现用户名密码认证,以及access_token的生成存储等。

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值