[6] OAuth2AuthenticationProcessingFilter之Bearer Token验证流程

OAuth2AuthenticationProcessingFilter

简介

授权认证服务通过认证后会返回Access Token,该token可用于请求资源服务(业务系统)的接口。我们需要把特定的信息放到请求头中,例如在请求头中写入Authorization: Bearer !xBYUEBY0N3o234N,Authorization为key,Bearer !xBYUEBY0N3o234N为value,!xBYUEBY0N3o234N是Access Token。请求经过OAuth2AuthenticationProcessingFilter后,过滤器中的认证管理器会调用配置的授权认证服务的check token接扣校验token是否有效,token有效则可以继续访问了。需要注意的是,对于资源认证服务Spring Security会把这个过滤器加入到过滤链里,但授权认证认证服务却不能。

源码分析

步骤1

笔者的Spring Cloud项目分ums[用户管理]服务以及auth[授权认证]服务,ums是一个业务系统,ums借助授权认证认证服务需要配置client-id,client-secret,token-info-uri等,token-info-uri即auth[授权认证]服务校验token的url,配置如下:

security:
  oauth2:
    client:
      client-id: carp
      client-secret: carp
    resource:
      id: carp-ums
      token-info-uri: http://carp-auth:8002/oauth2/token/check
      loadBalanced: true

步骤2

我们doFilter()方法中的代码进行分析,tokenExtractor.extract(request)从请求中取Bearer Token幷包装成Authentication,然后调用authenticationManager.authenticate()向授权认证服务发起请求,对token进行认证。
认证成功后,发送认证成功事件,并把身份认证信息写入上下文,代码如下:

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 {
        //此处代码逻辑很简单
        //Header中取出Authorization中的access token 幷包装成Authentication,Authentication的principal就是token
        Authentication authentication = tokenExtractor.extract(request);
        
        //authentication不存在,清空上下文
        if (authentication == null) {
            if (stateless && isAuthenticated()) {
                SecurityContextHolder.clearContext();
            }
        }
        else {
            //在request属性中写入OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE:xxxxx xxxx是token
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            //对于Bearer Token而言,authentication是PreAuthenticatedAuthenticationToken的实例,所以会执行到if分支里
            if (authentication instanceof AbstractAuthenticationToken) {
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                //buildDetails其实就是创建一个OAuth2AuthenticationDetails并放入remoteAddress、sessionId、tokenType、tokenValue信息
                needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
            }
            //调用RemoteTokenServices向授权认证服务发起请求,进行token校验。
            Authentication authResult = authenticationManager.authenticate(authentication);
            //发布一个身份认证成功的事件
            eventPublisher.publishAuthenticationSuccess(authResult);
            //上下文中写入身份认证信息
            SecurityContextHolder.getContext().setAuthentication(authResult);

        }
    }
    catch (OAuth2Exception failed) {
        //清空上下文
        SecurityContextHolder.clearContext();
        //发布一个身份认证失败的事件
        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);
}

authenticationDetailsSource**.**buildDetails()最终会执行到下面OAuth2AuthenticationDetails的构造方法,代码逻辑也十分简单,其实就是取出request中的相关信息,进行了一次包装,如下:

public OAuth2AuthenticationDetails(HttpServletRequest request) {
    this.tokenValue = (String) request.getAttribute(ACCESS_TOKEN_VALUE);
    this.tokenType = (String) request.getAttribute(ACCESS_TOKEN_TYPE);
    this.remoteAddress = request.getRemoteAddr();

    HttpSession session = request.getSession(false);
    this.sessionId = (session != null) ? session.getId() : null;
    StringBuilder builder = new StringBuilder();
    if (remoteAddress!=null) {
        builder.append("remoteAddress=").append(remoteAddress);
    }
    if (builder.length()>1) {
        builder.append(", ");
    }
    if (sessionId!=null) {
        builder.append("sessionId=<SESSION>");
        if (builder.length()>1) {
            builder.append(", ");
        }
    }
    if (tokenType!=null) {
        builder.append("tokenType=").append(this.tokenType);
    }
    if (tokenValue!=null) {
        builder.append("tokenValue=<TOKEN>");
    }
    this.display = builder.toString();
}

authenticationManager是OAuth2AuthenticationManager的实例,其中tokenServices是RemoteTokenServices的实例,如果授权认证服务checkToken返回的数据结构体被自定过,与Spring Security原来返回的不一致,则需要对RemoteTokenServices进行改造,并替换默认实例。因为RemoteTokenServices就是发送一个http请求,解析返回结果,进行数据包装。当然,Spring Security只是一个授权认证框架,如果我们使用的Dubbo Rpc,也可以进行改造,注入Dubbo Rpc接口实现ResourceServerTokenServices接口即可。有些变量命名比较抽象,截图以及的OAuth2AuthenticationManager代码如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    if (authentication == null) {
        throw new InvalidTokenException("Invalid token (token not found)");
    }
    String token = (String) authentication.getPrincipal();
    //默认是调用RemoteTokenService向授权认证服务的checkToken接口发起http Token验证请求
    //验证通过后包装成OAuth2Authentication
    OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    //token无效
    if (auth == null) {
        throw new InvalidTokenException("Invalid token: " + token);
    }
	//判断从接口获取的授权信息中的资源id,是否包含业务系统配置的资源id
    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 + ")");
    }
	//校验客户端id
    checkClientDetails(auth);
	//进行details拷贝
    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());
        }
    }
    //将details写入到身份认证信息中
    auth.setDetails(authentication.getDetails());
    //认账状态设置为已认证
    auth.setAuthenticated(true);
    return auth;

}

private void checkClientDetails(OAuth2Authentication auth) {
    if (clientDetailsService != null) {
        ClientDetails client;
        try {
            //调用clientDetailsService获取客户端详情
            client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
        }
        catch (ClientRegistrationException e) {
            throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
        }
        //判断客户端配置的scope 是否包含http request的中scope参数值
        Set<String> allowed = client.getScope();
        for (String scope : auth.getOAuth2Request().getScope()) {
            if (!allowed.contains(scope)) {
                throw new OAuth2AccessDeniedException(
                        "Invalid token contains disallowed scope (" + scope + ") for this client");
            }
        }
    }
}

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章