跳到主要内容

08、Spring Security 实战 - oauth2认证:ClientCredentialsTokenEndpointFilter 过滤器

第一步

Spring Security对于获取TOKEN的请求(/oauth/token),需要认证client_id和client_secret。认证client_id和client_secret可以有2种方式,一种是通过 ClientCredentialsTokenEndpointFilter,另一种是通过BasicAuthenticationFilter。

ClientCredentialsTokenEndpointFilter 继承自 AbstractAuthenticationProcessingFilter,调用授权接口获取token值的请求(/oauth/token)需要认证client_id和client_secret。该请求会被 AbstractAuthenticationProcessingFilter 过滤器拦截,执行父类的doFilter() 方法:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
    implements ApplicationEventPublisherAware, MessageSourceAware {
   
     

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

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 判断是否需要认证:判断url是否为与配置的获取access token的url进行匹配
        if (!requiresAuthentication(request, response)) {
   
     
            chain.doFilter(request, response);
            return;
        }

        Authentication authResult;
        try {
   
     
            // 调用实现类中的attemptAuthentication方法认证
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
   
     
                return;
            }
            // session存储
            sessionStrategy.onAuthentication(authResult, request, response);
        }catch (InternalAuthenticationServiceException failed) {
   
     
            //  认证失败处理
            unsuccessfulAuthentication(request, response, failed);
            return;
        }catch (AuthenticationException failed) {
   
     
            // Authentication failed
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        // Authentication success
        if (continueChainBeforeSuccessfulAuthentication) {
   
     
            chain.doFilter(request, response);
        }
        // 回调认证成功的自定义处理逻辑
        successfulAuthentication(request, response, chain, authResult);
    }

    protected boolean requiresAuthentication(HttpServletRequest request,
                                             HttpServletResponse response) {
   
     
        return requiresAuthenticationRequestMatcher.matches(request);
    }
}

requiresAuthentication(request, response) 最终会调用 ClientCredentialsTokenEndpointFilter 中内部类 ClientCredentialsRequestMatcher#matches 方法:

@Deprecated
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
   
     
    // ...
    
    // 内部类
    protected static class ClientCredentialsRequestMatcher implements RequestMatcher {
   
     
        
        // /oauth/token
        private String path;

        public ClientCredentialsRequestMatcher(String path) {
   
     
            this.path = path;
        }

        // 判断请求的 url 与获取 access token 的默认 url(/oauth/token)是否一致
        // 判断 client_id 是否为空
        public boolean matches(HttpServletRequest request) {
   
     
            // 请你路径uri : /ngsoc/AUTH/oauth/token
            String uri = request.getRequestURI();
            int pathParamIndex = uri.indexOf(59);
            if (pathParamIndex > 0) {
   
     
                uri = uri.substring(0, pathParamIndex);
            }
			// ngsoc
            String clientId = request.getParameter("client_id");
            if (clientId == null) {
   
     
                return false;
            } else {
   
     
                // 服务路径 request.getContextPath() : /ngosc/AUTH
                // path : /oauth/token
                return "".equals(request.getContextPath()) ? uri.endsWith(this.path) : uri.endsWith(request.getContextPath() + this.path);
            }
        }
    }
}

第二步

当认证请求需要被认证时,authResult = attemptAuthentication(request, response); 就会调用子类ClientCredentialsTokenEndpointFilter#attemptAuthentication 方法,在方法中将参数中的client_id和client_sercet封装成Authentication对象UsernamePasswordAuthenticationToken,然后交给AuthenticationManager的实现类去认证。

@Deprecated
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
   
     
    
    private AuthenticationEntryPoint authenticationEntryPoint;
    private boolean allowOnlyPost;
    
    public Authentication attemptAuthentication(HttpServletRequest request, 
                                                HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
   
     
        // 如果不是post请求,抛出异常
        if (this.allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
   
     
            throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[]{
   
     "POST"});
        } else {
   
     
            // 从请求参数client_id中获取clientId 
            String clientId = request.getParameter("client_id"); // ngsoc
            // 从请求参数client_secret中获取clientSecret
            String clientSecret = request.getParameter("client_secret"); // ngsoc
            
            // 从SecurityContextHolder中获取认证成功的认证用户信息,如果不为空,直接返回
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null && authentication.isAuthenticated()) {
   
     
                return authentication;
            } else if (clientId == null) {
   
     
                throw new BadCredentialsException("No client credentials presented");
            } else {
   
     
                if (clientSecret == null) {
   
     
                    clientSecret = "";
                }
                clientId = clientId.trim();
                // 将clientId和clientSecret封装为UsernamePasswordAuthenticationToken对象
                UsernamePasswordAuthenticationToken authRequest 
                    = new UsernamePasswordAuthenticationToken(clientId, clientSecret);
                // 将UsernamePasswordAuthenticationToken交给AuthenticationManager的子类认证
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
    }
}

第三步

AuthenticationManager接口源码:

public interface AuthenticationManager {
   
     
   // 身份认证
   // 请求数据:待认证的 Authentication
   // 响应数据:认证成功的 Authentication
   Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager接口的默认实现类是ProviderManager,因此this.getAuthenticationManager().authenticate(authRequest) 最终会调用ProviderManager#authenticate方法完成认证。

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
   
     

    // AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
    // 父类的 AuthenticationManager
    private AuthenticationManager parent;

    public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
   
     
        Class<? extends Authentication> toTest = authentication.getClass();
        Authentication result = null;
        Authentication parentResult = null;
        // 遍历AuthenticationProvider列表
        for (AuthenticationProvider provider : getProviders()) {
   
     
            // 判断当前遍历 AuthenticationProvider 是否支持Authentication对象的认证
            if (!provider.supports(toTest)) {
   
     
                continue;
            }

            try {
   
     
                // 如果支持,将使用该AuthenticationProvider完成认证
                result = provider.authenticate(authentication);
                if (result != null) {
   
     
                    copyDetails(authentication, result);
                    break;
                }
            }
			// ....
        }

        if (result == null && parent != null) {
   
     
            // Allow the parent to try.
            try {
   
     
                result = parentResult = parent.authenticate(authentication);
            } catch (ProviderNotFoundException e) {
   
     
            }catch (AuthenticationException e) {
   
     
                lastException = parentException = e;
            }
        }
		// ...
    }
}

说明:

在Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。

多个AuthenticationProvider将组成⼀个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个AuthenticationProvider列表,在ProviderManager中遍历列表中的每⼀个AuthenticationProvider去执⾏身份认证,最终得到认证结果。

ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为parent,这样当ProviderManager 认证失败之后,就可以进⼊到 parent 中再次进⾏认证。理论上来说, ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的⻆⾊,也就是 ProviderManager 是ProviderManager 的 parent。

默认情况下,ProviderManager的AuthenticationProvider列表中包含两个实现类:AnoymousAuthenticationProvider 和DaoAuthenticationProvider。

for循环内第一次得到AnoymousAuthenticationProvider,执行AnonymousAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果不支持,代码如下:

public class AnonymousAuthenticationProvider implements AuthenticationProvider,
      MessageSourceAware {
   
     

   public boolean supports(Class<?> authentication) {
   
     
      return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
   }
}

for循环内第二次得到DaoAuthenticationProvider,该类继承自AbstractUserDetailsAuthenticationProvider类,会调用AbstractUserDetailsAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果支持。

public abstract class AbstractUserDetailsAuthenticationProvider implements
    AuthenticationProvider, InitializingBean, MessageSourceAware {
   
     
    
    public boolean supports(Class<?> authentication) {
   
     
        return (UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication));
    }
}

因此 result = provider.authenticate(authentication) 最终会调用AbstractUserDetailsAuthenticationProvider#authenticate方法对UsernamePasswordAuthenticationToken对象完成认证,在该方法中根据clientId获取数据源中存储的用户user,然后判断user是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。

public abstract class AbstractUserDetailsAuthenticationProvider implements
    AuthenticationProvider, InitializingBean, MessageSourceAware {
   
     

    public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
   
     
        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
            : authentication.getName();

        boolean cacheWasUsed = true;
        // 从缓存中根据clientId获取UserDetails对象
        UserDetails user = this.userCache.getUserFromCache(username);

        // 如果缓存中获取不到
        if (user == null) {
   
     
            cacheWasUsed = false;

            try {
   
     
                // 从数据源中获取
                user = retrieveUser(username,
                                    (UsernamePasswordAuthenticationToken) authentication);
            }catch (UsernameNotFoundException notFound) {
   
     
                // 如果根据clientId从数据源中获取UserDetails用户详情,如果为空,认证失败抛出异常
                logger.debug("User '" + username + "' not found");
                if (hideUserNotFoundExceptions) {
   
     
                    throw new BadCredentialsException(messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.badCredentials",
                        "Bad credentials"));
                }
                else {
   
     
                    throw notFound;
                }
            }
        }
        try {
   
     
            // 检查账户是否锁定、启用、过期等
            pr eAuthenticationChecks.check(user);
            //检查凭据[密码]是否非空、以及存储密码与输入密码是否一致
            additionalAuthenticationChecks(user,
               (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
   
     
            if (cacheWasUsed) {
   
     
                cacheWasUsed = false;
                user = retrieveUser(username,
                                    (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                          (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
   
     
                throw exception;
            }
        }
		// 检查凭据是否未过期
        postAuthenticationChecks.check(user);
		// 将查询到到的用户放入缓存中
        if (!cacheWasUsed) {
   
     
            this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (forcePrincipalAsString) {
   
     
            principalToReturn = user.getUsername();
        }

        // 创建Authentication[身份认证信息]
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
}

到这儿认证流程就结束了,但是我们可以继续往下看一下,底层如何根据username获取客户端用户信息的。

第四步

retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 获取用户信息会调用DaoAuthenticationProvider#retrieveUser方法,该方法中,会调用UserDetailsService接口实现类的loadUserByUsername方法根据clientId获取客户端详情信息,代码如下:

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
   
     
    
    protected final UserDetails retrieveUser(String username,
 	UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
   
     
        prepareTimingAttackProtection();
        try {
   
     
            // 调用UserDetailsService接口实现类的loadUserByUsername方法根据username获取用户信息
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
   
     
                throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
   
     
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
   
     
            throw ex;
        }
        catch (Exception ex) {
   
     
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }
    
	protected UserDetailsService getUserDetailsService() {
   
     
		return userDetailsService;
	}
}

public interface UserDetailsService {
   
     
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

第五步

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 会调用ClientDetailsUserDetailsService#loadUserByUsername方法,获取客户端详情信息:

@Deprecated
public class ClientDetailsUserDetailsService implements UserDetailsService {
   
     
    private final ClientDetailsService clientDetailsService;
    private String emptyPassword = "";

    public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
   
     
        this.clientDetailsService = clientDetailsService;
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
   
     
        this.emptyPassword = passwordEncoder.encode("");
    }

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
   
     
        ClientDetails clientDetails;
        try {
   
     
            // 虎获取客户端详情
            clientDetails = this.clientDetailsService.loadClientByClientId(username);
        } catch (NoSuchClientException var4) {
   
     
            throw new UsernameNotFoundException(var4.getMessage(), var4);
        }

        String clientSecret = clientDetails.getClientSecret();
        if (clientSecret == null || clientSecret.trim().length() == 0) {
   
     
            clientSecret = this.emptyPassword;
        }
		// 将客户端信息 ClientDetails 封装成UserDetails并返回
        return new User(username, clientSecret, clientDetails.getAuthorities());
    }
}

第六步

在该方法中会调用ClientDetailsService接口实现类BaseClientDetails#loadClientByClientId方法获取ClientDetails信息:

@Deprecated
public interface ClientDetailsService {
   
     
    ClientDetails loadClientByClientId(String var1) throws ClientRegistrationException;
}

public interface ClientDetails extends Serializable {
   
     

	String getClientId();//客户端id
	
	Set<String> getResourceIds();//此客户端可以访问的资源。如果为空,则调用者可以忽略
	
	boolean isSecretRequired();//验证此客户端是否需要secret
	
	String getClientSecret();//获取客户端的secret
	
	boolean isScoped();//此客户端是否仅限于特定范围
	
	Set<String> getScope();//此客户端的范围。如果客户端未确定作用域,则为空
	
	Set<String> getAuthorizedGrantTypes();//此客户端被授权的授权类型
	
	Set<String> getRegisteredRedirectUri();//此客户端的预定义重定向redirect_url
	
	Collection<GrantedAuthority> getAuthorities();//权限集合
	
	Integer getAccessTokenValiditySeconds();//访问令牌有效期
	
	Integer getRefreshTokenValiditySeconds();//刷新令牌有效期

	boolean isAutoApprove(String scope);//测试客户端是否需要特定范围的用户批准
    
	Map<String, Object> getAdditionalInformation();//额外的信息
}

BaseClientDetails [
    clientId=ngsoc, 
    clientSecret={
   
     bcrypt}$2a$10$Dcn01QtYjhoXeeX0LPsn/.DBiiosgsFFKHVC1/tQiWk5dht1TgtKy, 
    scope=[], 
    resourceIds=[], 
    authorizedGrantTypes=[password, refresh_token, client_credentials], 
    registeredRedirectUris=[], 
    authorities=[], 
    accessTokenValiditySeconds=43200, 
    refreshTokenValiditySeconds=86400, 
    additionalInformation={
   
     }
]