Shiro是如何拦截未登录请求的(一)

问题描述

之前在公司搭项目平台的时候权限框架采用的是shiro,由于系统主要面向的是APP端的用户,PC端仅仅是公司内部人员在使用,而且考虑到系统的可用性和扩展性,服务端首先基于shiro做了一些改造以支持多数据源认证和分布式会话(关于分布式session可查看{% post_link SpringBoot集成Shiro实现多数据源认证授权与分布式会话(一)%}).我们知道在web环境下http是一种无状态的通讯协议,要想记录和校验用户的登录状态必须通过session的机制来实现,浏览器是通过cookie中存储的sessionid来确定用户的session数据的,shiro默认也是采用这种机制.而对于移动端用户来讲,则可以使用token的方式来进行身份鉴权,原理跟浏览器使用cookie传输是一样的.
token身份鉴权的流程
1.服务端在用户正常登录之后,通过特定算法生成一个全局唯一的字符串token并返回给客户端.
2.客户端在接下来的请求都会在请求头中携带token,服务端拦截token并对用户做身份鉴权.
3.token带有自动失效的机制,当用户主动退出或者失效时间一到则服务端删除会话信息.
遇到的问题
网上查了一下我们知道shiro也是通过携带cookie中的sessionid来做鉴权的,既然移动端使用的是token的机制,那么要想使shiro能够支持这套机制就必须改造shiro的鉴权方式.之前在搭框架的时候为了解决这个问题曾经草草的翻了一下shiro的源码(这货的代码量真心大啊,看的人一头雾水),找了很久也没找到它是在何处处理的,当时因为时间关系只好放弃,用了一种很笨的方法在请求头header中存储以键为Cookie,值为token=web_session_key-xxx的键值对的方式来确保shiro能通过解析校验,这样app端是能够正常交互的,但是对于后面增加的h5应用或者小程序则不行,首先是跨域问题(关于跨域可查看{% post_link 前后端分离之CORS跨域请求踩坑总结%}),由于是前后端分离的应用,浏览器的同源策略不允许js访问跨域的cookie,这样每次请求shiro获取的cookie都为空,过滤器会拦截下这个请求并作出如下响应:

image.png

为了h5应用能够与服务端正常交互只好想办法绕过shiro的拦截校验,既然无法传输cookie,只好在header中传一个token,并在自定义的过滤器(继承自shiro的FormAuthenticationFilter)中覆写它的isAccessAllowed方法,此方法返回值若为true则说明shiro鉴权通过,否则执行redirectToLogin方法跳转到登录页面.

    @Override
    protected boolean isAccessAllowed(ServletRequest request,
                                      ServletResponse response, Object mappedValue) {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        boolean isLogin;
        String device = httpRequest.getHeader("device");
        // 如果是客户端是H5
        if (StringHelpUtils.isNotBlank(device) && device.equals("H5")) {
            String h5Token = httpRequest.getHeader("token");
            Cookie[] cookies = httpRequest.getCookies();
            if (null != cookies) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals("token")) {
                        cookie.setValue(h5Token);
                    }
                }
            }
            isLogin = isH5Login(h5Token);//绕过shiro,直接到redis中校验token
        } else {
            // 如果是APP或者PC端
            Subject subject = getSubject(request, response);
            isLogin = subject.isAuthenticated();
        }
        return isLogin;
    }

到这里基本上shiro的登录校验是绕过去了,其实这里并不是真的绕过,因为shiro该做的事情还是会照做,只不过是我们再到redis中去匹配一次而已,但是却带来了一个新的问题,那就是服务端通过SecurityUtils.getSubject().getSession();取到的用户session对象与之前登录时产生的session对象并不是同一个,原因是shiro本身在执行校验时由于无法获取到cookie中的token,所以它把这个请求当成是一个新的请求,每次调用都会创建一个新的session,但这个新session里面并不存在我们需要的用户相关登录信息,而由于app与小程序是同一套接口,这样就影响到了原先已写好的业务代码了...虽然解决方法还是有的,但是总觉得整个过程下来,代码和功能的实现都让人觉得很别扭,因此本文想从源码的角度去逐步剖析shiro是如何拦截未登录请求的,从根源上来寻求解决方案,同时又不会对已对接好的业务接口造成影响.

源码跟踪

在开始跟源码之前,我们先来看看下面的异常堆栈图


image.png

之所以要帖这个图是因为shiro的代码实在太多,全部去看不太现实,因此在程序里面造了个异常,从异常的底部开始一步步跟下去总可以发现根源的,我们知道shiro的入口是个过滤器shiroFilter,因此不用怀疑先从过滤器找起,首先是ApplicationFilterChain,看名字和包路径就知道这个不是shiro的实现类,大概查了一下知道它是tomcat实现的过滤器链.其采用了责任链的设计模式,我们在idea中打开这个类,在它的internalDoFilter方法上加断点.


image.png

由这行代码ApplicationFilterConfig filterConfig = this.filters[this.pos++];可知this.filters是一个ApplicationFilterConfig集合,这个集合存储了ApplicationFilterChain里面的所有过滤器,如下图.
QQ截图20180711145037.png

其中ApplicationFilterConfig是个filter容器,我们来看看它的定义:
org.apache.catalina.core.ApplicationFilterConfig
Implementation of a javax.servlet.FilterConfig useful in managing the filter instances instantiated when a web application is first started.
大致意思是说当web应用一开始启动时,会将工程中的所有的实例化的filter实例加载到此容器中.下面来看看容器启动后加载了哪些filter实例.

  • name=characterEncodingFilter
  • name=hiddenHttpMethodFilter
  • name=httpPutFormContentFilter
  • name=requestContextFilter
  • name=corsFilter
  • name=shiroFilter
  • name=Tomcat WebSocket(JSR356) Filter

其中characterEncodingFilter(用于处理编码问题)、hiddenHttpMethodFilter(隐藏HTTP函数)、httpPutFormContentFilter(form表单处理)、requestContextFilter(请求上下文)等是springboot自动添加的一些常用过滤器(注意这几个filter的完整限定类名为Ordered开头的,如OrderedCharacterEncodingFilter继承自CharacterEncodingFilter,里面多了个order属性,用于确定该filter的执行顺序).
corsFilter是我们后端统一使用cors来解决跨域问题的.
wsFilter这个filter应该是用来处理WebSocket的.
最后一个shiroFilter是我们要关注的重点,它是整个shiro框架的入口,注意它的filterClass是ShiroFilterFacotoryBean$SpringShiroFilter,看名字应该是spring生成的一个代理类了,先不管它是怎么生成的,继续往下看会发现上述列出来的过滤器中除了wsFilter和shiroFilter之外其他的filter都继承自org.springframework.web.filter.OncePerRequestFilter.而spring的OncePerRequestFilter是一个抽象过滤器类,其中定义的抽象方法doFilterInternal是由其子类来实现的,doFilter方法则是final的子类只能继承不能覆写.比如CharacterEncodingFilter继承了OncePerRequestFilter的doFilter方法且实现了doFilterInternal方法.
所以从上面的异常堆栈图中我们可以看出每次ApplicationFilterChain执行链中filter的doFilter方法时都会先执行它的父类OncePerRequestFilter的doFilter方法然后再执行这个filter实现的doFilterInternal方法,一直到shiro自己的OncePerRequestFilter(注意shiro自己实现的这个filter跟spring的不是同一个)为止.你一定会很奇怪为什么这里会执行shiro的OncePerRequestFilter,按道理springboot默认的filter和跨域的filter都执行过去了,那么接下来要执行的应该是shiro的入口shiroFilter才对.所以到目前为止我们一共有两个疑惑:

1.ShiroFilterFacotoryBean$SpringShiroFilter是怎么来的.
2.shiro的执行入口为什么是其内部实现的OncePerRequestFilter.
带着这些问题我们继续往下看,首先是项目中shiroFilter的配置.
ShiroFilterFactoryBean

    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager());
         ........
        return shiroFilterFactoryBean;
    }

从上述配置可知使用了ShiroFilterFactoryBean来创建shiroFilter,所以重点在于ShiroFilterFactoryBean这个类.它的主要源代码如下:

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
    private static final transient Logger log = LoggerFactory.getLogger(ShiroFilterFactoryBean.class);
    private SecurityManager securityManager;
    private Map<String, Filter> filters = new LinkedHashMap();
    private Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
    private String loginUrl;
    private String successUrl;
    private String unauthorizedUrl;
    private AbstractShiroFilter instance;
    ......
    }

从类的定义中可知ShiroFilterFactoryBean实现了接口FactoryBean和BeanPostProcessor.
BeanPostProcessor接口的作用是在Spring容器启动时,容器中所有的bean在初始化的前后都会调用这个接口的方法postProcessBeforeInitialization并在这个方法中判断当前的bean是否为Filter,若是则装载进Map集合filters中.

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof Filter) {
            log.debug("Found filter chain candidate filter '{}'", beanName);
            Filter filter = (Filter)bean;
            this.applyGlobalPropertiesIfNecessary(filter);
            this.getFilters().put(beanName, filter);
        } else {
            log.trace("Ignoring non-Filter bean '{}'", beanName);
        }

        return bean;
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

而ShiroFilterFactoryBean本身又实现了另外一个接口FactoryBean,FactoryBean的作用是在执行getBean("shiroFilter")时会调用其getObject方法来获取一个代理实例,看源码可知其调用的是this.createInstance()函数,返回的是一个SpringShiroFilter实例.代码如下:

    public Object getObject() throws Exception {
        if (this.instance == null) {
            this.instance = this.createInstance();
        }

        return this.instance;
    }

    public Class getObjectType() {
        return ShiroFilterFactoryBean.SpringShiroFilter.class;
    }

    protected AbstractShiroFilter createInstance() throws Exception {
        log.debug("Creating Shiro Filter instance.");
        SecurityManager securityManager = this.getSecurityManager();
        String msg;
        if (securityManager == null) {
            msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        } else if (!(securityManager instanceof WebSecurityManager)) {
            msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        } else {
            FilterChainManager manager = this.createFilterChainManager();
            PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
            chainResolver.setFilterChainManager(manager);
            return new ShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver);
        }
    }

SpringShiroFilter是ShiroFilterFactoryBean类的静态内部类,继承自shiro的AbstractShiroFilter.

    private static final class SpringShiroFilter extends AbstractShiroFilter {
        protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            } else {
                this.setSecurityManager(webSecurityManager);
                if (resolver != null) {
                    this.setFilterChainResolver(resolver);
                }

            }
        }

跟到这里也就明白了ShiroFilterFacotoryBean$SpringShiroFilter是怎么来的了,那么第一个问题已解答,至于第二个问题的答案则要通过AbstractShiroFilter的源码来找了.

public abstract class AbstractShiroFilter extends OncePerRequestFilter {
}

通过源码可知AbstractShiroFilter是一个抽象过滤器,继承自shiro的抽象filter-OncePerRequestFilter,而OncePerRequestFilter中的抽象方法doFilterInternal则由其实现类AbstractShiroFilter负责实现,我们来看看doFilterInternal的具体代码:

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

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    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);
        }
    }

回到最初的问题:shiro是如何拦截未登录请求的,根据前面的贴出来异常堆栈图的指示,我们重点来看看上面第10行代码中的createSubject方法,其实现如下:

    protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
        return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
    }

此处调用的buildWebSubject方法的实现是在接口WebSubject的内部静态类Builder中.

        public WebSubject buildWebSubject() {
            Subject subject = super.buildSubject();
            if (!(subject instanceof WebSubject)) {
                String msg = "Subject implementation returned from the SecurityManager was not a " +
                        WebSubject.class.getName() + " implementation.  Please ensure a Web-enabled SecurityManager " +
                        "has been configured and made available to this builder.";
                throw new IllegalStateException(msg);
            }
            return (WebSubject) subject;
        }

这个方法主要用来创建shiro的主体subject,在静态类Builder中并没有相关实现的代码,而是在其父类Subject中.

        public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
        }

this.securityManager是shiro的安全管理器,管理着所有的Subject,且负责进行认证和授权、及会话、缓存等的管理,在实例化ShiroFilterFactoryBean时由springboot配置中注入过来的,我们来看看项目中的配置:

  shiroFilterFactoryBean.setSecurityManager(getDefaultWebSecurityManager());
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager() {
        logger.info("注入Shiro的Web过滤器-->securityManager", ShiroFilterFactoryBean.class);
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        ......
        return securityManager;
    }

从上面的代码调试跟踪可知createSubject方法的实现在DefaultWebSecurityManager的父类DefaultSecurityManager中.

    public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);

        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);

        Subject subject = doCreateSubject(context);

        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        save(subject);

        return subject;
    }

其中第11行context = resolveSession(context);看注释是通过引用的sessionid来解析关联的会话,进去看看它的实现:

    protected SubjectContext resolveSession(SubjectContext context) {
        if (context.resolveSession() != null) {
            log.debug("Context already contains a session.  Returning.");
            return context;
        }
        try {
            //Context couldn't resolve it directly, let's see if we can since we have direct access to 
            //the session manager:
            Session session = resolveContextSession(context);
            if (session != null) {
                context.setSession(session);
            }
        } catch (InvalidSessionException e) {
            log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                    "(session-less) Subject instance.", e);
        }
        return context;
    }

注意第9行Session session = resolveContextSession(context);跟进去这个方法.

    protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
        SessionKey key = getSessionKey(context);
        if (key != null) {
            return getSession(key);
        }
        return null;
    }
    protected SessionKey getSessionKey(SubjectContext context) {
        Serializable sessionId = context.getSessionId();
        if (sessionId != null) {
            return new DefaultSessionKey(sessionId);
        }
        return null;
    }

其中getSession(key)调用的是抽象类SessionsSecurityManager中的getSession方法.

    public Session getSession(SessionKey key) throws SessionException {
        return this.sessionManager.getSession(key);
    }

继续往下跑则到了抽象类AbstractNativeSessionManager中的getSession方法.

    public Session getSession(SessionKey key) throws SessionException {
        Session session = lookupSession(key);
        return session != null ? createExposedSession(session, key) : null;
    }

    private Session lookupSession(SessionKey key) throws SessionException {
        if (key == null) {
            throw new NullPointerException("SessionKey argument cannot be null.");
        }
        return doGetSession(key);
    }

而第10行的doGetSession方法则调用了抽象类AbstractValidatingSessionManager中的doGetSession方法

    @Override
    protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
        enableSessionValidationIfNecessary();

        log.trace("Attempting to retrieve session with key {}", key);

        Session s = retrieveSession(key);
        if (s != null) {
            validate(s, key);
        }
        return s;
    }

这里重点来看看第7行retrieveSession方法,跟进去发现它的实现在shiro的默认session管理器类DefaultSessionManager中.

    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            log.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a " +
                    "session could not be found.", sessionKey);
            return null;
        }
        Session s = retrieveSessionFromDataSource(sessionId);
        if (s == null) {
            //session ID was provided, meaning one is expected to be found, but we couldn't find one:
            String msg = "Could not find session with ID [" + sessionId + "]";
            throw new UnknownSessionException(msg);
        }
        return s;
    }

由第2行Serializable sessionId = getSessionId(sessionKey);进去getSessionId方法,其实现在类DefaultWebSessionManager中.

    @Override
    public Serializable getSessionId(SessionKey key) {
        Serializable id = super.getSessionId(key);
        if (id == null && WebUtils.isWeb(key)) {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            id = getSessionId(request, response);//此处调用下面的getSessionId方法
        }
        return id;
    }
    
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return getReferencedSessionId(request, response);//调用下面的getReferencedSessionId方法
    }
    
    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        //这句是重点
        String id = getSessionIdCookieValue(request, response);
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        } else {
            //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):

            //try the URI path segment parameters first:
            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);

            if (id == null) {
                //not a URI path segment parameter, try the query parameters:
                String name = getSessionIdName();
                id = request.getParameter(name);
                if (id == null) {
                    //try lowercase:
                    id = request.getParameter(name.toLowerCase());
                }
            }
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            }
        }
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            //automatically mark it valid here.  If it is invalid, the
            //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        }

        // always set rewrite flag - SHIRO-361
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());

        return id;
    }

关键在第18行String id = getSessionIdCookieValue(request, response);这句,继续跟进去.

    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
        if (!isSessionIdCookieEnabled()) {
            log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
            return null;
        }
        if (!(request instanceof HttpServletRequest)) {
            log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
            return null;
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }

可以看到这个方法的返回值是getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));其中getSessionIdCookie()返回的是一个Cookie对象,但是注意这里的Cookie是shiro自定义的一个接口.

public interface Cookie {
    /**
     * The value of deleted cookie (with the maxAge 0).
     */
    public static final String DELETED_COOKIE_VALUE = "deleteMe";
    

    /**
     * The number of seconds in one year (= 60 * 60 * 24 * 365).
     */
    public static final int ONE_YEAR = 60 * 60 * 24 * 365;

    /**
     * Root path to use when the path hasn't been set and request context root is empty or null.
     */
    public static final String ROOT_PATH = "/";

    String getName();

    void setName(String name);

    String getValue();

    void setValue(String value);

    String getComment();

    void setComment(String comment);

    String getDomain();

    void setDomain(String domain);

    int getMaxAge();

    void setMaxAge(int maxAge);

    String getPath();

    void setPath(String path);

    boolean isSecure();

    void setSecure(boolean secure);

    int getVersion();

    void setVersion(int version);

    void setHttpOnly(boolean httpOnly);

    boolean isHttpOnly();

    void saveTo(HttpServletRequest request, HttpServletResponse response);

    void removeFrom(HttpServletRequest request, HttpServletResponse response);

    String readValue(HttpServletRequest request, HttpServletResponse response);
}

我们来看看其子类SimpleCookie中的readValue,这里的SimpleCookie就是我们之前在springboot中所配置的bean,我们给它命了个名字叫"token".

    @Bean
    public SimpleCookie wapsession() {
        SimpleCookie simpleCookie = new SimpleCookie("token");
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }

然后去看它的readValue方法.

    public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
        String name = getName();
        String value = null;
        javax.servlet.http.Cookie cookie = getCookie(request, name);
        if (cookie != null) {
            // Validate that the cookie is used at the correct place.
            String path = StringUtils.clean(getPath());
            if (path != null && !pathMatches(path, request.getRequestURI())) {
                log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path});
            } else {
                value = cookie.getValue();
                log.debug("Found '{}' cookie value [{}]", name, value);
            }
        } else {
            log.trace("No '{}' cookie value", name);
        }

        return value;
    }

上面第2行的getName()返回的是cookie的名称,我们在配置时传入的是字符串"token",再看第4行中的getCookie方法,虽然前面的Cookie对象是shiro自定义的,但这里获取的cookie却是java原生的javax.servlet.http.Cookie类.

    private static javax.servlet.http.Cookie getCookie(HttpServletRequest request, String cookieName) {
        javax.servlet.http.Cookie cookies[] = request.getCookies();
        if (cookies != null) {
            for (javax.servlet.http.Cookie cookie : cookies) {
                if (cookie.getName().equals(cookieName)) {
                    return cookie;
                }
            }
        }
        return null;
    }

跟到这里总算找到shiro是在哪里拦截cookie的了,前面我们说过在app端是以Cookie为键,token=web_session_key-xxx为值的键值对方式传输的所以不会被拦截,而h5或小程序由于无法传输cookie则直接传token,那么服务端自然取不到cookie也就是说sessionid为null,所以当调用doGetSession(key)方法时返回的session对象也是null的,我们再往回看DefaultSecurityManager的createSubject方法,执行完context = resolveSession(context);之后返回的是一个session为null的上下文信息,紧接着执行context = resolvePrincipals(context);获取登录用户的Principal信息,由于在上面返回的context中并没有找到相关已登录的信息,自然取出来的principal和authenticationInfo也是null.

    protected SubjectContext resolvePrincipals(SubjectContext context) {

        PrincipalCollection principals = context.resolvePrincipals();

        if (CollectionUtils.isEmpty(principals)) {
            log.trace("No identity (PrincipalCollection) found in the context.  Looking for a remembered identity.");

            principals = getRememberedIdentity(context);

            if (!CollectionUtils.isEmpty(principals)) {
                log.debug("Found remembered PrincipalCollection.  Adding to the context to be used " +
                        "for subject construction by the SubjectFactory.");

                context.setPrincipals(principals);

                // The following call was removed (commented out) in Shiro 1.2 because it uses the session as an
                // implementation strategy.  Session use for Shiro's own needs should be controlled in a single place
                // to be more manageable for end-users: there are a number of stateless (e.g. REST) applications that
                // use Shiro that need to ensure that sessions are only used when desirable.  If Shiro's internal
                // implementations used Subject sessions (setting attributes) whenever we wanted, it would be much
                // harder for end-users to control when/where that occurs.
                //
                // Because of this, the SubjectDAO was created as the single point of control, and session state logic
                // has been moved to the DefaultSubjectDAO implementation.

                // Removed in Shiro 1.2.  SHIRO-157 is still satisfied by the new DefaultSubjectDAO implementation
                // introduced in 1.2
                // Satisfies SHIRO-157:
                // bindPrincipalsToSession(principals, context);

            } else {
                log.trace("No remembered identity found.  Returning original context.");
            }
        }

        return context;
    }

    public PrincipalCollection resolvePrincipals() {
        PrincipalCollection principals = getPrincipals();

        if (CollectionUtils.isEmpty(principals)) {
            //check to see if they were just authenticated:
            AuthenticationInfo info = getAuthenticationInfo();
            if (info != null) {
                principals = info.getPrincipals();
            }
        }

        if (CollectionUtils.isEmpty(principals)) {
            Subject subject = getSubject();
            if (subject != null) {
                principals = subject.getPrincipals();
            }
        }

        if (CollectionUtils.isEmpty(principals)) {
            //try the session:
            Session session = resolveSession();
            if (session != null) {
                principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
            }
        }

        return principals;
    }

再接着就是Subject subject =doCreateSubject(context); 创建一个subject了.来看下大致是怎么创建subject的.

    public Subject createSubject(SubjectContext context) {
        //这里的context其实就是个map集合即上文调用context = resolveSession(context);返回的
        if (!(context instanceof WebSubjectContext)) {
            return super.createSubject(context);
        }
        WebSubjectContext wsc = (WebSubjectContext) context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        Session session = wsc.resolveSession();//取出的session是null
        //在context中没有取到key的SESSION_CREATION_ENABLED的元素,所以直接返回为ture
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        PrincipalCollection principals = wsc.resolvePrincipals();//取出的principals是null
        //由于前面得到的AuthenticationInfo是null,所以这里的authenticated 为false
        boolean authenticated = wsc.resolveAuthenticated();
        String host = wsc.resolveHost();
        ServletRequest request = wsc.resolveServletRequest();
        ServletResponse response = wsc.resolveServletResponse();

        return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
                request, response, securityManager);
    }

可以看到最终创建的subject对象.


image.png

subject创建完了,接着就是过滤器链的处理了,中间经过ProxiedFilterChain、OncePerRequestFilter、AdviceFilter、PathMatchingFilter这几个过滤器再到AccessControlFilter.onPreHandle方法.

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

isAccessAllowed调的是AuthenticationFilter中的isAccessAllowed方法.

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

这里的subject就是我们之前创建的subject了,显然isAuthenticated方法返回false.接着执行onAccessDenied方法,此方法实现在AuthenticatingFilter的子类FormAuthenticationFilter中.

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                if (log.isTraceEnabled()) {
                    log.trace("Login submission detected.  Attempting to execute login.");
                }
                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;
        }
    }

代码很简单非登录请求则执行saveRequestAndRedirectToLogin方法.

    protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        saveRequest(request);
        redirectToLogin(request, response);
    }
    //以下两个方法皆在WebUtils类中实现.
    protected void saveRequest(ServletRequest request) {
        WebUtils.saveRequest(request);
    }
    public static void saveRequest(ServletRequest request) {
        Subject subject = SecurityUtils.getSubject();
        Session session = subject.getSession();//重点看这行
        HttpServletRequest httpRequest = toHttp(request);
        SavedRequest savedRequest = new SavedRequest(httpRequest);
        session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
    }

上面的Session session = subject.getSession();真正调用的是getSession(true);方法参数为true表示会创建一个新的session对象.这块代码相对简单可以加断点一步步跟进去,大致上最终就是调用我们自定义的RedisSessionDao创建一个新的session对象之后,再执行DefaultWebSessionManager的storeSessionId方法创建一个SimpleCookie对象,最后在response中添加到请求头header里面.

    public void saveTo(HttpServletRequest request, HttpServletResponse response) {

        String name = getName();
        String value = getValue();
        String comment = getComment();
        String domain = getDomain();
        String path = calculatePath(request);
        int maxAge = getMaxAge();
        int version = getVersion();
        boolean secure = isSecure();
        boolean httpOnly = isHttpOnly();

        addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
    }

    private void addCookieHeader(HttpServletResponse response, String name, String value, String comment,
                                 String domain, String path, int maxAge, int version,
                                 boolean secure, boolean httpOnly) {

        String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
        response.addHeader(COOKIE_HEADER_NAME, headerValue);

        if (log.isDebugEnabled()) {
            log.debug("Added HttpServletResponse Cookie [{}]", headerValue);
        }
    }

新的session对象生成之后最终将执行redirectToLogin方法,由于我们自定义了扩展自FormAuthenticationFilter的过滤器,因此调用的是我们自己的方法,并在其中对不同的调用端做不同的响应处理.

    @Override
    protected void redirectToLogin(ServletRequest request,
                                   ServletResponse response) throws IOException {
        String loginUrl = getLoginUrl();
        if (logger.isDebugEnabled()) {
            logger.debug("客户端登录的URL:{}", loginUrl);
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        //System.out.println(httpRequest.getRequestURL());
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("text/html; charset=utf-8");
        httpRequest.setCharacterEncoding("UTF-8");
        // 是否为APP登录请求
        if (StringHelpUtils.isNotBlank(httpRequest.getHeader("device"))
                && (httpRequest.getHeader("device").equals("APP") || httpRequest
                .getHeader("device").equals("H5"))) {
            String token = httpRequest.getHeader(TOKEN);
            if (logger.isDebugEnabled()) {
                logger.debug("客户端设备:{},token:{}",
                        httpRequest.getHeader("device"), token);
            }
            if (StringHelpUtils.isBlank(token)) {
                ResponseEntity result = new ResponseEntity().isOk(HttpStatus.TOKEN_NOT_EXIST,
                        "认证失败!");
                httpResponse.getWriter().append(JSON.toJSONString(result));
                httpResponse.getWriter().flush();
                httpResponse.getWriter().close();
            } else {
                ResponseEntity result = new ResponseEntity().isOk(
                        HttpStatus.APP_UNKNOW_ACCOUNT, "认证失败!");
                httpResponse.getWriter().append(JSON.toJSONString(result));
                httpResponse.getWriter().flush();
                httpResponse.getWriter().close();
            }
        } else {
            // PC跳转 如果是非Ajax请求 按默认的配置跳转到登录页面
            if (!"XMLHttpRequest".equalsIgnoreCase(httpRequest
                    .getHeader("X-Requested-With"))) {// 不是ajax请求
                WebUtils.issueRedirect(request, response, loginUrl);
            } else {
                // 如果是Aajx请求,则返回会话失效的JSON信息
                ResponseEntity result = new ResponseEntity().isOk(
                        HttpStatus.SESSION_UNVAILDATE
                        , "请求失败!");
                httpResponse.getWriter().append(JSON.toJSONString(result));
                httpResponse.getWriter().flush();
                httpResponse.getWriter().close();
            }
        }
    }

好了,经过一步步的抽丝剥茧,到这里我们已经差不多完全了解shiro的拦截校验机制了,既然找到问题的根源在哪里,下一篇文章我们再来看看如何针对性的根据项目中遇到的实际问题来定制相应的解决方案.
参考资料
http://jinnianshilongnian.iteye.com/blog/2025656
https://blog.csdn.net/kingherooo/article/details/39086903
https://blog.csdn.net/abinge317/article/details/52882913
https://blog.csdn.net/wang11234ilike/article/details/60575239
https://blog.csdn.net/king624498030/article/details/68922819
....

推荐阅读更多精彩内容