[7] ClientCredentialsTokenEndpointFilter

ClientCredentialsTokenEndpointFilter

介绍

Spring Security对于获取TOKEN的请求(默认是"/oauth/token"),需要认证client_id和client_secret。认证client_id和client_secret可以有2种方式,一种是通过本节讲的ClientCredentialsTokenEndpointFilter,另一种是通过BasicAuthenticationFilter。ClientCredentialsTokenEndpointFilter首先比对请求URL是否是TOKEN请求路径以及请求参数中是否包含client_id,如果满足以上条件,再调用ProviderManager认证client_id和client_secret是否与配置的一致。如果通过认证,会把身份认证信息保存打SecurityContext上下文中。

代码分析

步骤1

Spring Security建议用BasicAuthenticationFilter替换ClientCredentialsTokenEndpointFilter,因此默认ClientCredentialsTokenEndpointFilter是不被开启的,我们为了讲解这个过滤器,可以按照如下方式开启过滤,配置如下:

@RefreshScope
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {  
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
    }
}

步骤2

ClientCredentialsTokenEndpointFilter继承自AbstractAuthenticationProcessingFilter,doFilter()方法在父类当中。请求经过过滤器时先判断url是否为与配置的获取access token的url进行匹配,并且请求参数中client_id不能为空时,然后将参数中的client_id和client_sercet与内存或者数据库(取决于ClientDetailService的实现方式)的client_id和client_sercet进行匹配,匹配成功则验证成功,反之则验证失败,代码如下:

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

    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
	//判断请求是否需要进行认证:1)url与获取token的url一致 2)请求参数中client_id不能为空
    if (!requiresAuthentication(request, response)) {
        chain.doFilter(request, response);

        return;
    }

    if (logger.isDebugEnabled()) {
        logger.debug("Request is to process authentication");
    }

    Authentication authResult;

    try {
        //进行client_id和client_sercet认证
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
		...
        //处理认证失败
        unsuccessfulAuthentication(request, response, failed);
        return;
    }
    catch (AuthenticationException failed) {
        //处理认证失败
        unsuccessfulAuthentication(request, response, failed);
        return;
    }

    // Authentication success
    //是否在认证成功处理之前调用下一级过滤链
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
	//认证成功处理,1:SecurityContext上下文存储身份认证信息 2:rememberMeServices处理登录成功 3:发布登录成功Spring事件
    successfulAuthentication(request, response, chain, authResult);
}

requiresAuthentication()最终会调用ClientCredentialsTokenEndpointFilter#matches()

protected boolean requiresAuthentication(HttpServletRequest request,
        HttpServletResponse response) {
    //requiresAuthenticationRequestMatcher是一个ClientCredentialsRequestMatcher
    return requiresAuthenticationRequestMatcher.matches(request);
}
@Override
public boolean matches(HttpServletRequest request) {
    String uri = request.getRequestURI();
    int pathParamIndex = uri.indexOf(';');

    if (pathParamIndex > 0) {
        // strip everything after the first semi-colon
        uri = uri.substring(0, pathParamIndex);
    }
	//参数中必须要含有client_id参数
    String clientId = request.getParameter("client_id");

    if (clientId == null) {
        // Give basic auth a chance to work instead (it's preferred anyway)
        return false;
    }

    if ("".equals(request.getContextPath())) {
        return uri.endsWith(path);
    }

    //请求路径一定要是获取token的URL,一般为"/oauth/token"
    return uri.endsWith(request.getContextPath() + path);
}

步骤3

若上下无有效的身份认证信息,attemptAuthentication()最终会调用ProviderManager#authenticate()方法进行验证,authenticationManager中包含1.AnoymousAuthenticationProvider 2.DaoAuthenticationProvider,实际上只有DaoAuthenticationProvider在起作用,DaoAuthenticationProvider#authenticate()根据client_id获取client详情,然后判断client是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。截图和代码如下:

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException, IOException, ServletException {
    ...
    //取client_id
    String clientId = request.getParameter("client_id");
    //取client_secret
    String clientSecret = request.getParameter("client_secret");

    // If the request is already authenticated we can assume that this
    // filter is not needed
    //从SpringSecurity上下文中取一下身份认证信息,看看已经认证过的就无需重复认证了
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null && authentication.isAuthenticated()) {
        return authentication;
    }
	
    if (clientId == null) {
        throw new BadCredentialsException("No client credentials presented");
    }

    if (clientSecret == null) {
        clientSecret = "";
    }

    clientId = clientId.trim();
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
            clientSecret);
	//进行认证
    return this.getAuthenticationManager().authenticate(authRequest);

}

image.png

public Authentication authenticate(Authentication authentication)
        throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();
	//providers包含2个Provider:1.AnoymousAuthenticationProvider 2.DaoAuthenticationProvider
    for (AuthenticationProvider provider : getProviders()) {
        //这里的support逻辑很简单,就是判断一下authentication.getClass()与Provider支持的类类型是否一致或父子继承关系
        //AnoymousAuthenticationProvider是不支持的,DaoAuthenticationProvider支持
        if (!provider.supports(toTest)) {
            continue;
        }

        try {
            //provider实例是DaoAuthenticationProvider
            result = provider.authenticate(authentication);

            if (result != null) {
                //将认证成功的身份认证信息复制到另外一个实例
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException | InternalAuthenticationServiceException e) {
            prepareException(e, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw e;
        } catch (AuthenticationException e) {
            lastException = e;
        }
    }

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

    boolean cacheWasUsed = true;
    //默认是NullUserCache,即不加缓存
    UserDetails user = this.userCache.getUserFromCache(username);
	//缓存中没用命中
    if (user == null) {
        //设置命中标识
        cacheWasUsed = false;

        try {
            //调用ClientDetailsUserDetailsService根据client_id获取用户
            user = retrieveUser(username,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
 			...
        }
		...
    }

    try {
        //检查账户是否锁定、启用、过期等
        preAuthenticationChecks.check(user);
        //检查凭据[密码]是否非空、以及存储密码与输入密码是否一致
        additionalAuthenticationChecks(user,
                (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
		...
    }
	//检查凭据是否未过期
    postAuthenticationChecks.check(user);

    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;

    if (forcePrincipalAsString) {
        principalToReturn = user.getUsername();
    }
	//创建Authentication[身份认证信息]
    return createSuccessAuthentication(principalToReturn, authentication, user);
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值