SpringSecurity——OAuth2框架鉴权实现源码分析

Posted 叶不修233

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringSecurity——OAuth2框架鉴权实现源码分析相关的知识,希望对你有一定的参考价值。

SpringSecurity——OAuth2框架鉴权实现源码分析

一、ManagedFilter迭代过滤器链

ManagedFilter管理这六条过滤器链,如图所示:
其中的第五条过滤器链springSecurityFilterChain是本文要讨论的对象。

0.characterEncodingFilter
1.WebMvcMetricsFilter
2.formContentFilter
3.requestContextFilter
4.springSecurityFilterChain
5.corsFilter

ManagedFilter.class -> doFilter()

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
    if (this.servletContext.getDeployment().getDeploymentState() != State.STARTED) 
        throw UndertowServletMessages.MESSAGES.deploymentStopped(this.servletContext.getDeployment().getDeploymentInfo().getDeploymentName());
     else 
        if (!this.started) 
            this.start();
        

        this.getFilter().doFilter(request, response, chain);//迭代下一条过滤器链
    

1.4 springSecurityFilterChain

在springSecurityFilterChain过滤器链中,首先初始化一个FilterChainProxy过滤器链代理对象,在这个过滤器链代理对象中有一个过滤器链集合,每一个过滤器链都有一组过滤器来处理不同的请求。
其中的第八条过滤器链OAuth2AuthenticationProcessingFilter是本文要讨论的对象,用于处理请求的鉴权。

FilterChainProxy.class --> doFilter()

public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException 
    if (this.currentPosition == this.size) 
        if (FilterChainProxy.logger.isDebugEnabled()) 
            FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " reached end of additional filter chain; proceeding with original chain");
        

        this.firewalledRequest.reset();
        this.originalChain.doFilter(request, response);
     else 
        ++this.currentPosition;
        Filter nextFilter = (Filter)this.additionalFilters.get(this.currentPosition - 1);//new了一个过滤器链代理对象
        if (FilterChainProxy.logger.isDebugEnabled()) 
            FilterChainProxy.logger.debug(UrlUtils.buildRequestUrl(this.firewalledRequest) + " at position " + this.currentPosition + " of " + this.size + " in additional filter chain; firing Filter: '" + nextFilter.getClass().getSimpleName() + "'");
        

        nextFilter.doFilter(request, response, this);	//迭代过滤器
    


1.4.7 OAuth2AuthenticationProcessingFilter

OAuth2鉴权主要用到了OAuth2AuthenticationProcessingFilter过滤器。这个过滤器下涉及到的类有以下12个类,大致作用是从请求中提取authentication,从authentication中获取token,判断是否为空,是否有效,是否过期。具体主要包含1-66个步骤的跳转和调用,详见代码中的注释。

【1】OAuth2AuthenticationProcessingFilter.class

这里是鉴权开始执行的起点,鉴权的大致步骤为以下内容:从请求中提取authentication --> 判断是否为空 --> 清除SpringSecurity上下文中的内容(如果有) --> 存到request的attributte中 --> 由authenticationManager进行鉴权操作 --> 将鉴权通过的事件进行广播 --> 将鉴权结果存储到SpringSecurity上下文中。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
    boolean debug = logger.isDebugEnabled();
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    try 
        //1.从请求中提取authentication,跳到【2】
        Authentication authentication = this.tokenExtractor.extract(request);
        //23.判断authentication是否为空,如果为空,也就是这个请求未携带token
        if (authentication == null) 
            //24.如果this.stateless等于true,同时	SecurityContextHolder.getContext().getAuthentication()中的authentication不为空而且不属于匿名用户的authentication
            if (this.stateless && this.isAuthenticated()) 
                if (debug) 
                    logger.debug("Clearing security context.");
                
                SecurityContextHolder.clearContext();
            

            if (debug) 
                logger.debug("No token in request, will continue chain.");
            
        //25.如果请求中提取的authentication不为空
         else 
          //26.把OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE作为key,authentication.getPrincipal()作为value存到request的attributte中
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            //27.判断authentication的对象是否是AbstractAuthenticationToken的实例
            if (authentication instanceof AbstractAuthenticationToken) 
                //28.如果是,Authentication authentication向下转型为AbstractAuthenticationToken needsDetails
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                //29.为needsDetails的details属性赋值,跳到【4】
                needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
            
			//31.跳到【5】,由authenticationManager进行鉴权后返回authResult,从【5】返回这里并开始下一行
            Authentication authResult = this.authenticationManager.authenticate(authentication);
            if (debug) 
                logger.debug("Authentication success: " + authResult);
            
			//64.将鉴权通过的事件进行广播
            this.eventPublisher.publishAuthenticationSuccess(authResult);
            //65.将authResult存到SecurityContextHolder的context属性中
            SecurityContextHolder.getContext().setAuthentication(authResult);
        
     catch (OAuth2Exception var9) 
        SecurityContextHolder.clearContext();
        if (debug) 
            logger.debug("Authentication request failed: " + var9);
        

        this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
        this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
        return;
    
	//66.放行
    chain.doFilter(request, response);


private boolean isAuthenticated() 
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    return authentication != null && !(authentication instanceof AnonymousAuthenticationToken);

【2】CookieTokenExtractor.class(我们自己重写的方法)

【2】~ 【3】这两个类里主要是从请求中获取authentication的具体实现步骤:大致上分为从SpringSecurity上下文中获取、从cookie中获取、从请求头中获取。从任意一个位置获取之后就把authentication返回到【1】。

@Override
public Authentication extract(HttpServletRequest request) 
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) //2.从SecurityContextHolder中提取authentication
        return authentication;//3.如果拿到了,直接返回提取到的authentication
    
    //4.如果没拿到,调用父类extract(request)方法,跳到【3】
    return super.extract(request);
    //22.带着token跳到【1】

@Override
protected String extractToken(HttpServletRequest request) 
    String result;
    //6.从cookie中提取authentication
    Cookie accessTokenCookie = OAuth2CookieHelper.getAccessTokenCookie(request);
    //7.如果拿到了,直接返回提取到的authentication
    if (accessTokenCookie != null) 
        result = accessTokenCookie.getValue();
     else 
        //8.如果没拿到,调用父类extractToken(request)方法,跳到【3】
        result = super.extractToken(request);
    
    //19.带着token,跳到C中的extract(HttpServletRequest request)
    return result;

【3】BearerTokenExtractor.class -> extract()

public Authentication extract(HttpServletRequest request) 
    //5.调用子类中的extractToken(request),从请求中获取token
    String tokenValue = this.extractToken(request);
    //20.如果获取到了,包装token
    if (tokenValue != null) 
        PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
        //21.返回包装后的authentication,跳到【2】
        return authentication;
     else 
        return null;
    


protected String extractToken(HttpServletRequest request) 
    //9.调用本类中extractHeaderToken(request),从请求中获取token
    String token = this.extractHeaderToken(request);
    if (token == null) 
        logger.debug("Token not found in headers. Trying request parameters.");
        token = request.getParameter("access_token");
        if (token == null) 
            logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
         else 
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
        
    
	//18.带着token,跳到【2】
    return token;


protected String extractHeaderToken(HttpServletRequest request) 
    //10.通过请求头的key【Authorization】获取对应的值
    Enumeration<String> headers = request.getHeaders("Authorization");

    String value;
    do //11.遍历获取到的值
        if (!headers.hasMoreElements()) 
            return null;
        

        value = (String)headers.nextElement();
        //12.找到一个以"Bearer"开头的value,把这个value转换成全小写字母
     while(!value.toLowerCase().startsWith("Bearer".toLowerCase()));
	//13.把这个value前面的"Bearer"裁掉并且去空格处理,存储到authHeaderValue中
    String authHeaderValue = value.substring("Bearer".length()).trim();
    //14.把OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE作为key,"Bearer"作为value存到request的attributte中
    request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim());
    //15.找到数字44在authHeaderValue中的下标
    int commaIndex = authHeaderValue.indexOf(44);
    if (commaIndex > 0) 
        //16.如果下标>0,也就是存在这个数字,就剪切开头到下标44所在的位置的值,重新赋给authHeaderValue
        authHeaderValue = authHeaderValue.substring(0, commaIndex);
    
	//17.否则直接返回authHeaderValue,跳到本类中的extractToken(HttpServletRequest request)
    return authHeaderValue;

【4】OAuth2AuthenticationDetailsSource.class

这里创建了OAuth2AuthenticationDetails对象,把request存放进去

@Deprecated
public class OAuth2AuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, OAuth2AuthenticationDetails> 
    public OAuth2AuthenticationDetailsSource() 
    

    public OAuth2AuthenticationDetails buildDetails(HttpServletRequest context) 
        //30.new 了一个OAuth2AuthenticationDetails对象,把request存放进去,然后返回【1】
        return new OAuth2AuthenticationDetails(context);
    

【5】OAuth2AuthenticationManager.class

这里是鉴权的核心处理器:从authentication中获取token后,解析token,并对解析后的token分别执行了是否为空判断、有效性和是否过期判断

public Authentication authenticate(Authentication authentication) throws AuthenticationException 
    //32.判断authentication是否为空,为空就抛异常
    if (authentication == null) 
        throw new InvalidTokenException("Invalid token (token not found)");
     else 
        //33.从authentication中获取token
        String token = (String)authentication.getPrincipal();
        //34.校验token的有效性和是否过期,跳到【6】
        OAuth2Authentication auth = this.tokenServices.loadAuthentication(token);
        if (auth == null) 
            throw new InvalidTokenException("Invalid token: " + token);
         else 
            Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
            if (this.resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(this.resourceId)) 
                throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + this.resourceId + ")");
             else 
                //61.调用本类中checkClientDetails(auth)方法
                this.checkClientDetails(auth);
                if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) 
                    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
                    if (!details.equals(auth.getDetails())) 
                        details.setDecodedDetails(auth.getDetails());
                    
                

                auth.setDetails(authentication.getDetails());
                auth.setAuthenticated(true);
                //63.返回处理之后的auth,跳到【1】
                return auth;
            
        
    


private void checkClientDetails(OAuth2Authentication auth) 
    //62.Oauth客户端如果为null ,不检查,如果配置了ClientDetailsService的实现类,会进行检查
    if (this.clientDetailsService != null) 
        ClientDetails client;
        try 
            //获取客户端
            client = this.clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
         catch (ClientRegistrationException var6) 
            throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
        
		//获取客户端的授权范围
        Set<String> allowed = client.getScope();
        Iterator var4 = auth.getOAuth2Request().getScope().iterator();
		//迭代授权范围,如果没有当前授权,就抛异常
        while(var4.hasNext()) 
            String scope = (String)var4.next();
            if (!allowed.contains(scope)) 
                throw new OAuth2AccessDeniedException("Invalid token contains disallowed scope (" + scope + ") for this client");
            
        
    


【6】DefaultTokenServices.class -> loadAuthentication(String accessTokenValue)

这里做两件事情:

1.是解析token,即把token解析为可以阅读的键值对

2.是对于token的为空判断、有效性和是否过期判断的具体步骤

public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException 
    //35.从解析token,跳到【7】
    OAuth2AccessToken accessToken = this.tokenStore.readAccessToken(accessTokenValue);
    //51.判断token是否为空,为空就抛异常
    if (accessToken == null) 
        throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        //53.判断token是否过期,过期就从tokenStore中移除token并且抛异常,跳到【11】
     else if (accessToken.isExpired()) 
        this.tokenStore.removeAccessToken(accessToken);
        throw以上是关于SpringSecurity——OAuth2框架鉴权实现源码分析的主要内容,如果未能解决你的问题,请参考以下文章

五分钟带你玩转oauth2引子

五分钟带你玩转oauth2引子

OAuth2.0 - 使用 SpringGateWay 网关实现统一鉴权

OAuth2.0 - 使用 SpringGateWay 网关实现统一鉴权

搭建SpringCloud微服务框架:Spring-Security-OAuth 服务接口鉴权

搭建SpringCloud微服务框架:Spring-Security-OAuth 服务接口鉴权