Spring Security+OAuth2资源服务之令牌校验源码分析

我们知道 Spring Security 采用 IoC 和 AOP思想,基于 Servlet 过滤器实现的安全框架。

传送门:Spring Security过滤器链加载执行流程分析:https://blog.csdn.net/qq_42402854/article/details/122205790

OAuth2AuthenticationProcessingFilter是OAuth2受保护资源的身份验证过滤器。重点查看它。

一、OAuth2AuthenticationProcessingFilter

public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {

	private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);

	private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();

	private AuthenticationManager authenticationManager;

	private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();

	private TokenExtractor tokenExtractor = new BearerTokenExtractor();

	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

	private boolean stateless = true;
    
    ...
    }

1、查看 doFilter方法

查看 OAuth2AuthenticationProcessingFilter类的 doFilter方法。

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

		final boolean debug = logger.isDebugEnabled();
		final HttpServletRequest request = (HttpServletRequest) req;
		final HttpServletResponse response = (HttpServletResponse) res;

		try {

            // 1. 从标头中提取OAuth令牌
			Authentication authentication = tokenExtractor.extract(request);
			
			if (authentication == null) {
				if (stateless && isAuthenticated()) {
					if (debug) {
						logger.debug("Clearing security context.");
					}
					SecurityContextHolder.clearContext();
				}
				if (debug) {
					logger.debug("No token in request, will continue chain.");
				}
			}
			else {
                // request设置属性
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                // 转为AbstractAuthenticationToken并设置Details
				if (authentication instanceof AbstractAuthenticationToken) {
					AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
					needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
				}
                
                // 2. 认证管理器进行认证
				Authentication authResult = authenticationManager.authenticate(authentication);

				if (debug) {
					logger.debug("Authentication success: " + authResult);
				}

                // 发布认证成功
				eventPublisher.publishAuthenticationSuccess(authResult);
                // 4. 设置SecurityContext
				SecurityContextHolder.getContext().setAuthentication(authResult);

			}
		}// 5. 异常处理
		catch (OAuth2Exception failed) {
			SecurityContextHolder.clearContext();

			if (debug) {
				logger.debug("Authentication request failed: " + failed);
			}
			eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
					new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

			authenticationEntryPoint.commence(request, response,
					new InsufficientAuthenticationException(failed.getMessage(), failed));

			return;
		}

		chain.doFilter(request, response);
	}

下面基于 JdbcTokenStore查看分析源码。

2、从标头中提取OAuth令牌

调用 BearerTokenExtractor的 extract方法,从标头中提取OAuth令牌。

最后封装返回 PreAuthenticatedAuthenticationToken对象,它是一个预认证对象的Authentication,它的 principal属性包含了当前令牌。

	@Override
	public Authentication extract(HttpServletRequest request) {
		String tokenValue = extractToken(request);
		if (tokenValue != null) {
            // 创建PreAuthenticatedAuthenticationToken
			PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
			return authentication;
		}
		return null;
	}

2.1 extractToken方法如下:

	protected String extractToken(HttpServletRequest request) {
		// 1. 检查消息头中的令牌
		String token = extractHeaderToken(request);

		// 2. 消息头没有检查请求参数中是否有access_token 
		if (token == null) {
			logger.debug("Token not found in headers. Trying request parameters.");
			token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
			if (token == null) {
				logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
			}
			else {
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
			}
		}
		// 3.返回token
		return token;
	}

注意: 先从请求头中提取令牌,如果没有,再从请求参数中提取令牌。所以这也是通过令牌访问资源服务器的两种方式。

2.2 extractHeaderToken方法如下:

	protected String extractHeaderToken(HttpServletRequest request) {
        // 1.获取Authorization消息头内容 Bearer xxxtoken
		Enumeration<String> headers = request.getHeaders("Authorization");
        
        // 2. 循环消息头内容
		while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
			String value = headers.nextElement();
			if 
                // 3. 如果已Bearer 开头((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                // 4. 截取令牌 
				String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
				// Add this here for the auth details later. Would be better to change the signature of this method.
            
            	// 5. request对象添加属性
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
						value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
				int commaIndex = authHeaderValue.indexOf(',');
				if (commaIndex > 0) {
                    // 6.如果存在逗号,则重新截取
					authHeaderValue = authHeaderValue.substring(0, commaIndex);
				}
            	// 7. 返回token
				return authHeaderValue;
			}
		}

		return null;
	}

3、调用认证管理器进行认证

查看认证管理器 OAuth2AuthenticationManager的 authenticate方法。

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // Authentication预认证对象为null,抛出Invalid token (token not found)异常。
		if (authentication == null) {
			throw new InvalidTokenException("Invalid token (token not found)");
		}
        // 获取token 
		String token = (String) authentication.getPrincipal();
        
        // 1. JdbcTokenStore这里调用DefaultTokenServices
		OAuth2Authentication auth = tokenServices.loadAuthentication(token);
		if (auth == null) {
			throw new InvalidTokenException("Invalid token: " + token);
		}

		Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
		if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
			throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
		}

        // 2. 检查ClientDetails
		checkClientDetails(auth);

        // 填充认证信息并返回Authention
		if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
			OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
			// Guard against a cached copy of the same details
			if (!details.equals(auth.getDetails())) {
				// Preserve the authentication details from the one loaded by token services
				details.setDecodedDetails(auth.getDetails());
			}
		}
		auth.setDetails(authentication.getDetails());
		auth.setAuthenticated(true);
		return auth;

	}

3.1 调用DefaultTokenServices

查看 DefaultTokenServices类的 loadAuthentication方法。

	public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
			InvalidTokenException {
        //1. 查库,通过令牌获取token_id
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
		if (accessToken == null) {
			throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
		}
		else if (accessToken.isExpired()) {
			tokenStore.removeAccessToken(accessToken);
			throw new InvalidTokenException("Access token expired: " + accessTokenValue);
		}

        //2. 查库,通过token_id获取授权信息
		OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
		if (result == null) {
			// in case of race condition
			throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
		}
		if (clientDetailsService != null) {
			String clientId = result.getOAuth2Request().getClientId();
			try {
				clientDetailsService.loadClientByClientId(clientId);
			}
			catch (ClientRegistrationException e) {
				throw new InvalidTokenException("Client not valid: " + clientId, e);
			}
		}
		return result;
	}

在这里插入图片描述
在这里插入图片描述

3.2 检查ClientDetails

查看 checkClientDetails方法

	private void checkClientDetails(OAuth2Authentication auth) {
        // 1. Oauth客户端如果为null ,不检查,配置了ClientDetailsService的实现类,会进行检查
		if (clientDetailsService != null) {
			ClientDetails client;
			try {
                // 2. 获取客户端
				client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
			}
			catch (ClientRegistrationException e) {
				throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
			}
            
            // 3. 获取配置的客户端授权范围
			Set<String> allowed = client.getScope();
			for (String scope : auth.getOAuth2Request().getScope()) {
                // 4. 如果没有当前授权,则抛出异常OAuth2AccessDeniedException
				if (!allowed.contains(scope)) {
					throw new OAuth2AccessDeniedException(
							"Invalid token contains disallowed scope (" + scope + ") for this client");
				}
			}
		}
	}

4、设置SecurityContext

认证管理器进行认证通过,将用户授权信息放到 SecurityContextHolder上下文中。

	eventPublisher.publishAuthenticationSuccess(authResult);
	SecurityContextHolder.getContext().setAuthentication(authResult);

5、异常处理

		catch (OAuth2Exception failed) {
            // 清理SecurityContext
			SecurityContextHolder.clearContext();

			if (debug) {
				logger.debug("Authentication request failed: " + failed);
			}
            // 发布认证失败
			eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
					new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

            // 调用OAuth2AuthenticationEntryPoint返回ResponseEntity
			authenticationEntryPoint.commence(request, response,
					new InsufficientAuthenticationException(failed.getMessage(), failed));

			return;
		}

		chain.doFilter(request, response);
}

在这里插入图片描述

通过查看源码,对OAuth2令牌的身份验证有了一定认识。

– 求知若饥,虚心若愚。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值