BasicHttpAuthenticationFilter 认证流程

BasicHttpAuthenticationFilter 继承关系


从继承关系可以看到 BasicHttpAuthenticationFilter 继承自抽象类 OncePerRequestFilter。

OncePerRequestFilter 的字面意思是:Once Per Request,即每个请求只执行一次,原理见:

https://blog.csdn.net/qq_39291919/article/details/108288084

通过上面这篇文章提供的源码,我们可以知道 OncePerRequestFilter 已经实现了 doFilter,而且知道,真正的处理逻辑在 doFilterInternal() 方法中。然而,doFilterInternal() 方法是在子类 AdviceFilter 实现的,源码如下:

public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
    throws ServletException, IOException {

    Exception exception = null;

    try {

        boolean continueChain = preHandle(request, response);
        if (log.isTraceEnabled()) {
            log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");
        }

        if (continueChain) {
            executeChain(request, response, chain);
        }

        postHandle(request, response);
        if (log.isTraceEnabled()) {
            log.trace("Successfully invoked postHandle method");
        }

    } catch (Exception e) {
        exception = e;
    } finally {
        cleanup(request, response, exception);
    }
}

仔细看,可以发现它调用了两个方法 preHandle 和 postHandle,其实就是过滤器的两次方法调用,相关解释见:

https://blog.csdn.net/qq_39291919/article/details/89761121

请注意:在执行 preHandle 方法后还会返回 1 个 boolean 值,判断是否要继续执行过滤器链,这与下面介绍的方法返回值息息相关。

PreHandle


下面先看 preHandle,PathMatchingFilter 已经实现了 preHandle()。PathMatchingFilter,顾名思义,路径匹配过滤器,它的作用就是来根据路径匹配结果,调用相应过滤器(没匹配上的直接 return true,即继续执行过滤器链)。

path 匹配是通过 FilterChainDefinitionMap 注册的,比如设置了 “/login”, “anon”,那么如果本次请求的地址也是 /login,则会匹配上。注册原理见:

https://blog.csdn.net/qq_39291919/article/details/108299951

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

    if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
        if (log.isTraceEnabled()) {
            log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
        }
        return true;
    }

    for (String path : this.appliedPaths.keySet()) {
        // If the path does match, then pass on to the subclass implementation for
        // specific checks
        // (first match 'wins'):
        if (pathsMatch(path, request)) {
            log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
            Object config = this.appliedPaths.get(path);
            return isFilterChainContinued(request, response, path, config);
        }
    }

    // no path matched, allow the request to go through:
    return true;
}

如果 path 匹配成功,则会先执行 isFilterChainContinued(),isFilterChainContinued() 方法也是在 PathMatchingFilter 实现的。它的作用就是判断过滤器是否可用,如果可用就继续执行;否则,跳过,return true

private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
    String path, Object pathConfig) throws Exception {

    if (isEnabled(request, response, path, pathConfig)) { // isEnabled check added in 1.2
        if (log.isTraceEnabled()) {
            log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}].  " +
                "Delegating to subclass implementation for 'onPreHandle' check.",
                new Object[] { getName(), path, pathConfig });
        }
//The filter is enabled for this specific request, so delegate to subclass implementations
//so they can decide if the request should continue through the chain or not:
        return onPreHandle(request, response, pathConfig);
    }

    if (log.isTraceEnabled()) {
        log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}].  " +
            "The next element in the FilterChain will be called immediately.",
            new Object[] { getName(), path, pathConfig });
    }
//This filter is disabled for this specific request,
//return 'true' immediately to indicate that the filter will not process the request
//and let the request/response to continue through the filter chain:
    return true;
}

isEnabled 方法本质上是判断 enabled 是否为 true。其实几乎所有的过滤器都可以执行,因此 enabled 默认为 true,除非人为的去设置它的值:

onPreHandle

我们一路是从 preHandle() 走下来的,这里之所以起名为 onPreHandle(),是因为这才是真正的执行逻辑,之前的种种都是可以看作判断。

onPreHandle() 在 PathMatchingFilter 的子类 AccessControlFilter 有了新的实现,它的返回值依赖两个方法 isAccessAllowed()、onAccessDenied()。

这两个方法通过 || 运算符连接,其实有一个逻辑判断过程,先判断 isAccessAllowed(访问是否允许),如果允许则通过,如果不允许则进行 onAccessDenied(访问拒绝的处理逻辑)。

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

isAccessAllowed

BasicHttpAuthenticationFilter 实现了 isAccessAllowed() 方法。并不是纯粹覆盖。它也使用了父类的方法,因为源码中使用了 super.isAccessAllowed():

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    HttpServletRequest httpRequest = WebUtils.toHttp(request);
    String httpMethod = httpRequest.getMethod();

    // Check whether the current request's method requires authentication.
    // If no methods have been configured, then all of them require auth,
    // otherwise only the declared ones need authentication.

    Set<String> methods = httpMethodsFromOptions((String[]) mappedValue);
    boolean authcRequired = methods.size() == 0;
    for (String m : methods) {
        if (httpMethod.toUpperCase(Locale.ENGLISH).equals(m)) { // list of methods is in upper case
            authcRequired = true;
            break;
        }
    }

    if (authcRequired) {
        return super.isAccessAllowed(request, response, mappedValue);
    } else {
        return true;
    }
}

实际上,该方法先做了一个 HTTP Method 的比对,自定义 FilterChainDefinitionMap 的时候,可以设置一批 HTTP method 是需要认证的,比如:

如果当前使用 RESTful 风格请求。现有 [PUT] /project 用于更新,[GET] /project 用于获取全部数据,这两个请求 URL 都是一样的,但如何让 GET 请求通过,PUT 请求需要授权呢?答案就是使用 HTTP Method 方法过滤。

配置 /project = authcBasic[PUT]

那么,访问 /project 的时候,GET 方法是不用认证的。

所以现在知道,即使没有写 GET,依然也会走 BasicHttpAuthenticationFilter,只是认证直接跳过(return true)。

因此,如果 HTTP Method 属于这一类 Method,那么就调用了 super.isAccessAllowed 进行判断。

下面继续观察 super.isAccessAllowed() 方法到底做了什么?

首先,在继承链上,离 BasicHttpAuthenticationFilter 最近的 AuthenticationFilter 也实现了 isAccessAllowed() 方法,因此会调用它:

AuthenticatingFilter.isAccessAllowed(ServletRequest, ServletResponse, Object)

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    return super.isAccessAllowed(request, response, mappedValue) ||
            (!isLoginRequest(request, response) && isPermissive(mappedValue));
}

注意到,该方法也使用了 super 去调用父类方法,找到最近的有实现方法的父类 AuthenticationFilter,方法如下:

AuthenticationFilter.isAccessAllowed(ServletRequest, ServletResponse, Object)

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        return subject.isAuthenticated();
    }

获取 Subject,然后调用 isAuthenticated() 判断是否已经认证过了。

作用:判断是否认证过了,通俗来说,就是登陆了没

继续回到上面:

AuthenticatingFilter.isAccessAllowed(ServletRequest, ServletResponse, Object)

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    return super.isAccessAllowed(request, response, mappedValue) ||
            (!isLoginRequest(request, response) && isPermissive(mappedValue));
}

如果上面 isAuthenticated 为 false,那么需要继续判断,是否是 !isLoginRequest() 非登录请求且 isPermissive() 是放行的,也就是说判断是否是匿名访问路径

BasicHttpAuthenticationFilter 实现 isLoginRequest,本质上是通过特殊的请求头进行判断的,可参考如下博文:

https://blog.csdn.net/qq_39291919/article/details/108418488

onAccessDenied

该方法就是 isAccessAllowed 返回 false 之后执行的,即访问拒绝的逻辑

BasicHttpAuthenticationFilter 实现了自己的 onAccessDenied:

BasicHttpAuthenticationFilter.onAccessDenied

protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    boolean loggedIn = false; // false by default or we wouldn't be in this method
    if (isLoginAttempt(request, response)) {
        loggedIn = executeLogin(request, response);
    }
    if (!loggedIn) {
        sendChallenge(request, response);
    }
    return loggedIn;
}

isLoginAttempt() ,该方法前面已经出现,通过请求头判断是否为尝试登陆,如果 true,则执行登录逻辑;反之,sendChallenge。

BasicHttpAuthenticationFilter 是没有实现 executeLogin() 的,因此将调用父类 AuthenticatingFilter 的 executeLogin() 方法。

protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    AuthenticationToken token = createToken(request, response);
    if (token == null) {
        String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
            "must be created in order to execute a login attempt.";
        throw new IllegalStateException(msg);
    }
    try {
        Subject subject = getSubject(request, response);
        subject.login(token);
        return onLoginSuccess(token, subject, request, response);
    } catch (AuthenticationException e) {
        return onLoginFailure(token, e, request, response);
    }
}

createToken(),该方法又是 BasicHttpAuthenticationFilter 来实现的,其实也就是从 Authorization 的 Request Header 提取base64 编码的用户名和密码,然后解析,最终会实例化 UsernamePasswordToken。

protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
    String authorizationHeader = getAuthzHeader(request);
    if (authorizationHeader == null || authorizationHeader.length() == 0) {
        // Create an empty authentication token since there is no
        // Authorization header.
        return createToken("", "", request, response);
    }

    log.debug("Attempting to execute login with auth header");

    String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request);
    if (prinCred == null || prinCred.length < 2) {
        // Create an authentication token with an empty password,
        // since one hasn't been provided in the request.
        String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0];
        return createToken(username, "", request, response);
    }

    String username = prinCred[0];
    String password = prinCred[1];

    return createToken(username, password, request, response);
}

在 createToken 之后,会 getSubject,执行 login()。后面会执行 securityManager 的 login 方法,在 securityManager 中会对 token 进行验证,本质上就是调用 Realm 方法验证,如果验证过程中没有异常抛出,则顺利执行,

login() 具体执行逻辑见:

https://blog.csdn.net/qq_39291919/article/details/108289019

如果认证过程没有异常抛出,最终会走到 onLoginSuccess(),如果有异常抛出则执行 onLoginFailure()。一般也就是在 Realm 的执行逻辑中抛出异常。

  • 6
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罐装面包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值