shiro相关Filter

0前言

shiro中的filer是使用servlet中的filter接口扩展的,但是shiro代理了servlet 原生的filter,请求到了后会先执行shiro的filter,再执行原生的filter。

前面文章项目启动时shiro加载过程中介绍过,shiro只配置了shiroFilter一个拦截器入口,那么shiro是怎样执行内部那么多filter的?shiro内部访问控制filter又是如何实现的呢?

带着上述两个问题,我们梳理下shiro的filter实现和工作流程。

filter看翻译是过滤器的意思,但是经常有人把它翻译成拦截器,SpringMVC 中的 Interceptor才是拦截器。我也经常搞错,但是这里我们严谨亿点点。

 

1shiro的filter实现

这里照搬一下zhangkaitao博客中的图。

 

 (1)Filter

servlet的filter,不熟悉的可以看一下servlet规范。

(2)AbstractFilter

shiro实现,主要用来配置FilterConfig,init()方法里提供了一个模板方法onFilterConfigSet()供子类实现。

(3)NameableFilter

给filter起名字的,如shiro自带的anon、authc,名字要求唯一,不然会有问题。

(4)OncePerRequestFilter

用于防止多次执行 Filter 的;也就是说一次请求只会走一次过滤器链;另外提供 enabled 属性,表示是否开启该过滤器实例,默认 enabled=true 表示开启,如果不想让某个过滤器工作,可以设置为 false 即可。可以简单看一下dofilter实现,源码107行

    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
//首先检查一下有没有执行过这个filter,通过在request里面设置attribute实现
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation
//在判断一下这个过滤器需不需要执行enable属性为true才需要执行 if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", getName());
//不需要执行就到过滤器链的下一个过滤器 filterChain.doFilter(request, response); }
else { // Do invoke this filter... log.trace("Filter '{}' not yet executed. Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try {
//重点,shiro的过滤器都走这个逻辑,这是一个模板方法,具体实现交给子类实现 doFilterInternal(request, response, filterChain); }
finally { // Once the request has finished, we're done and we don't // need to mark as 'already filtered' any more. request.removeAttribute(alreadyFilteredAttributeName); } } }

OncePerRequestFilter算是一个比较重要的Filter实现,shiro的所有filter只有这一次dofilter实现。OncePerRequestFilter子类分成了两个分支:

一个是AdviceFilter,shiro内部授权和验证的filter实现都是在这一分支。

一个是AbstractShiroFilter,也就是web.xml中配置的shiro入口filter。

下面先分析一下AdviceFilter这条线路

 (5)AdviceFilter

 该类提供了Filter的Aop风格支持,类似于 SpringMVC 中的 Interceptor:

//在过滤器执行链前执行,异常会中断过滤器链后续执行(类似于aop的前置增强)
boolean
preHandle(ServletRequest request, ServletResponse response) throws Exception

//在过滤器执行链后执行(类似aop的后置增强)
void postHandle(ServletRequest request, ServletResponse response) throws Exception

//最终执行,可以用于资源清理(类似于aop的后置最终增强)
void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;

前面说过OncePerRequestFilter的dofilter调用了一个模板方法doFilterInternal,AdviceFilter正是通过实现doFilterInternal实现类似aop功能,AdviceFilter类源码124行

    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里面调用了afterCompletion方法 cleanup(request, response, exception); } }

(6)PathMatchingFilter

PathMatchingFilter 提供了基于 Ant 风格的请求路径匹配功能及过滤器参数解析的功能.。

PathMatchingFilter重写了父类AdviceFilter的preHandle方法,PathMatchingFilter类源码163行

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

//appliedPath就是配置中指定的需要拦截的url
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 (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; }

主要执行的内容交给isFilterChainContinued,PathMatchingFilter类源码192行

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

//isEnabled判断这个过滤器生不生效,这里会调用OncePerRequestFilter的实现
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}); }
//重点要关注的方法 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; }

onPreHandle方法处理匹配路径时需要处理的过滤器逻辑,PathMatchingFilter这里默认返回true,主要交给子类实现。

AnonymousFilter,也就是配置里的anon,就是实现PathMatchingFilter,我们可以看到anon的onPreHandle方法啥也没干(没有拦截逻辑),直接返回true。

public class AnonymousFilter extends PathMatchingFilter {

    /**
     * Always returns <code>true</code> allowing unchecked access to the underlying path or resource.
     *
     * @return <code>true</code> always, allowing unchecked access to the underlying path or resource.
     */
    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) {
        // Always return true since we allow access to anyone
        return true;
    }

}

(7)AccessControlFilter

AccessControlFilter 提供了访问控制的基础功能,比如是否允许访问/当访问拒绝时如何处理等。

这是比较重要的过滤器实现,shiro自己内部访问控制Filter都是基于该Filter实现,我们自己定义的Filter也可以直接扩展该类,(不需要复杂访问控制逻辑自定义Filter也可以直接扩展PathMatchingFilter )

AccessControlFilter重写了父类PathMatchingFilter的onPreHandle方法,相当于该过滤器的入口。AccessControlFilter类源码161行。

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

isAccessAllowed方法返回true表示通过访问控制验证,否则才会执行后面的onAccessDenied,onAccessDenied表示访问拒绝时需要执行的逻辑。

isAccessAllowed和onAccessDenied都是需要子类扩展的。这里以authc为例看看怎么实现的。authc表示FormAuthenticationFilter。

FormAuthenticationFilter继承AuthenticatingFilter,AuthenticatingFilter又继承AuthenticationFilter,最后AuthenticationFilter继承AccessControlFilter

首先isAccessAllowed实现

//AuthenticatingFilter第121行

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


//
AuthenticatingFilter父类AuthenticationFilter第79行,判断有没有登陆
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
 

如果登陆了,直接返回true,否则看看是不是请求的登陆url或者配置了许可的请求,满足其一就返回true

然后是onAccessDenied实现

onAccessDenied没有那么多弯弯道道,直接由FormAuthenticationFilter实现,源码148行

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //如果请求路径是登陆请求,有没有显示的配置loginUrl,没有的话默认/login.jsp
        if (isLoginRequest(request, response)) {
//登陆请求必须是post的
if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); }
//执行login逻辑,这里用户可以不用自己写登陆入口,前提是需要配置loginUrl
return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } //其他请求会保留现场,并重定向到登陆页面 saveRequestAndRedirectToLogin(request, response); return false; } }

下面看一下authc的登陆实现executeLogin,源代码AuthenticatingFilter类第44行

    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);
//登陆成功后会重定向到登陆前请求的页面,successUrl只是个备选重定向地址
return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) {
//失败的话会在request的attribute里面记录失败异常,请求会继续回到当前页
return onLoginFailure(token, e, request, response); } }

若不是登陆请求saveRequestAndRedirectToLogin则会存下request,并重定向到登陆页面loginUrl

 

如果不想用shiro提供的登陆逻辑呢。而且现在项目基本都是前后端分离,使用异步请求,路由的逻辑一般都会交给前端处理,重定向不是那么友好。

我想到的有三个思路:

1)第一个是authc不拦截登陆url

自己在controller里实现登陆的逻辑,但是authc拦截器会拦截其他的url,没有登陆的话请求后端其他接口还是会重定向到loginUrl。

这里后端可以配一个假的loginUrl,重定向到“前端host/noauth”,然后前端要起一个代理服务打到“后端服务/noauth”,后端response返回一个401给前端,前端接收到401响应自己路由到登陆页面。

这样的话前端要起一个代理服务,并且后端还是重定向了,只能说没有走authc的executeLogin。可以但是没有必要

2)自定义扩展AccessControlFilter 

可以参考FormAuthenticationFilter实现,没有认证时不重定向,直接response设置401即可。授权模块的AuthorizationFilter就是这么做的,参考源码AuthorizationFilter类106行

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {

        Subject subject = getSubject(request, response);
        // If the subject isn't identified, redirect to login URL
        if (subject.getPrincipal() == null) {
            saveRequestAndRedirectToLogin(request, response);
        } else {
            // If subject is known but not authorized, redirect to the unauthorized URL if there is one
            // If no unauthorized URL is specified, just return an unauthorized HTTP status code
            String unauthorizedUrl = getUnauthorizedUrl();
            //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:
            if (StringUtils.hasText(unauthorizedUrl)) {
                WebUtils.issueRedirect(request, response, unauthorizedUrl);
            } else {
                WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
            }
        }
        return false;
    }

配置了unauthorizedUrl就走重定向逻辑,没有配置就直接返回response 401

3)干脆不用shiro的访问控制Filter

自己写一个Filter或者实现SpringMVC 中的 Interceptor,自己做登陆拦截或者权限拦截也很nice

 

到此,我们回答了一个问题:shiro内部访问控制filter又是如何实现的

这些Filter都没有配置到web.xml,spring也没有显示处理这部分filter配置。那么这些filter是如何生效的,我们知道web项目唯一配置的Filter是ShiroFilter,

也许我们可以从shiro Filter的另一个实现分支AbstractShiroFilter中找到答案。

 

2shiro的Filter工作流程

先看入口ShiroFilter,shiroFilter只是提供了init方法的实现,用于绑定SecurityManager和FilterChainResolver,主要的功能还是在其父类AbstractShiroFilter实现的。

AbstractShiroFilter直接继承了OncePerRequestFilter,所以我们还是从模板方法doFilterInternal突破,请求会到这里来,源代码AbstractShiroFilter第350行。

    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

        Throwable t = null;

        try {
//HttpServletRequest会被包装为ShiroHttpServletRequest
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);

//HttpServletResponse会被包装为ShiroHttpServletResponse
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
//为没有请求/响应对创建一个WebSubject实例,这个实例会在整个请求响应执行过程中使用
final Subject subject = createSubject(request, response); //noinspection unchecked subject.execute(new Callable() { public Object call() throws Exception {
//更新session访问时间,只会管理shiro自己的session(HttpSession不会管)
//通过调用session.touch()实现 updateSessionLastAccessTime(request, response);

//执行过滤器逻辑 executeChain(request, response, chain);
return null; } }); } catch (ExecutionException ex) { t = ex.getCause(); } catch (Throwable throwable) { t = throwable; } if (t != null) { if (t instanceof ServletException) { throw (ServletException) t; } if (t instanceof IOException) { throw (IOException) t; } //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one: String msg = "Filtered request failed."; throw new ServletException(msg, t); } }

next

//源代码AbstractShiroFilter第446行
protected
void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException {

//注意,这里会代理FilterChain FilterChain chain
= getExecutionChain(request, response, origChain); chain.doFilter(request, response); }


//源代码AbstractShiroFilter第406行
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;

//获取初始化时注入的FilterChainResolver
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}

//重点,生成代理的FilterChain
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
log.trace("Resolved a configured FilterChain for the current request.");
chain = resolved;
} else {
log.trace("No FilterChain configured for the current request. Using the default.");
}

return chain;
}
 

 源代码PathMatchingFilterChainResolver类第93行

    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {

//(1)FilterChainManager维护了shiro默认Filter和用户自定义Filter,
//还维护了filterChains表示用户配置的pathPattern到多个filter的对应关系 FilterChainManager filterChainManager
= getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } String requestURI = getPathWithinApplication(request); //the 'chain names' in this implementation are actually path patterns defined by the user. We just use them //as the chain name for the FilterChainManager's requirements for (String pathPattern : filterChainManager.getChainNames()) { // If the path does match, then pass on to the subclass implementation for specific checks:

//如果请求路径和pathPattern匹配 if (pathMatches(pathPattern, requestURI)) { if (log.isTraceEnabled()) { log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " + "Utilizing corresponding filter chain..."); }

//代理原生的FilterChain
return filterChainManager.proxy(originalChain, pathPattern); } } return null; }

这里说明下FilterChainManager 中维护的filterChains,表示用户配置的pathPattern到多个filter的对应关系。pathPattern是用户配置的url,如/login、/role/*等,filter表示这些路径请求需要执行的过滤器如authc、anon等。

下面跟进filterChainManager是如何代理的,

//源代码DefaultFilterChainManager类第318行
public
FilterChain proxy(FilterChain original, String chainName) { NamedFilterList configured = getChain(chainName); if (configured == null) { String msg = "There is no configured chain under the name/key [" + chainName + "]."; throw new IllegalArgumentException(msg); }
//
return configured.proxy(original); }


//源代码SimpleNamedFilterList类,第78行
public FilterChain proxy(FilterChain orig) {
return new ProxiedFilterChain(orig, this);
}
 

最终找到了代理类ProxiedFilterChain

public class ProxiedFilterChain implements FilterChain {

    //TODO - complete JavaDoc

    private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);

    private FilterChain orig;

//filters表示本次请求路径匹配的pathPattern对应的所有Filter
private List<Filter> filters; private int index = 0; public ProxiedFilterChain(FilterChain orig, List<Filter> filters) { if (orig == null) { throw new NullPointerException("original FilterChain cannot be null."); } this.orig = orig; this.filters = filters; this.index = 0; } public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.filters == null || this.filters.size() == this.index) { //we've reached the end of the wrapped chain, so invoke the original one: if (log.isTraceEnabled()) { log.trace("Invoking original filter chain."); }

//如果匹配到shiro的Filter为空,或者匹配到shiro的filter都执行完了,才会执行servlet原本的Filter逻辑
this.orig.doFilter(request, response); } else { if (log.isTraceEnabled()) { log.trace("Invoking wrapped filter at index [" + this.index + "]"); }

//这里开始一个个按顺序执行shiro自己的Filter逻辑
this.filters.get(this.index++).doFilter(request, response, this); } } }

这就是ShiroFilter整个代理执行过程。

 

posted @ 2021-05-20 20:45  OUYM  阅读(717)  评论(0编辑  收藏  举报