从继承关系可以看到 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,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,除非人为的去设置它的值:
我们一路是从 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);
}
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 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
该方法就是 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 的执行逻辑中抛出异常。