Shiro(二):Spring-boot如何集成Shiro(上)

这篇文章主要介绍了spring-boot是如何集成shiro的authentication流程的。

从shiro-spring-boot-web-starter说起

shiro-spring-boot-web-starter是shiro在web环境下快速集成至spring-boot的配置包。其本身引入了shiro的必要模块。并在Configuration中以@Bean的形式声明了Shiro各组件,交由spring容器统一管理。先看META-INF定义了配置类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration =   org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration,  org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration

可以看到主要有两个配置:ShiroWebAutoConfiguration和ShiroWebFilterConfiguration。

  • ShiroWebAutoConfiguration
    ShiroWebAutoConfiguration声明了Shiro需要的各个对象,在需要shiro框架进行处理的地方,可以方便注入,从而到达应用shiro特性的目的。同理,我们也可以根据需要替换上述部分Bean,让Shiro的流程更负责自己的业务需求。
/**
 * @since 1.4.0
 */
@Configuration
@AutoConfigureBefore(ShiroAutoConfiguration.class)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {

    //声明了认证时的策略,默认是AtLeastOneSuccessfulStrategy
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected AuthenticationStrategy authenticationStrategy() {
        return super.authenticationStrategy();
    }

    //声明Authenticator对象,负责认证的过程
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected Authenticator authenticator() {
        return super.authenticator();
    }

    //声明Authorizer的对象,负责授权的过程
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected Authorizer authorizer() {
        return super.authorizer();
    }

    //声明Subject数据访问对象,通过该对象可以对Subject数据进行CRUD操作
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SubjectDAO subjectDAO() {
        return super.subjectDAO();
    }

    //声明SessionStorageEvaluator对象,用来决定Session是否需要持久化
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        return super.sessionStorageEvaluator();
    }

    //声明SubjectFactory,用来创建Subject对象
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SubjectFactory subjectFactory() {
        return super.subjectFactory();
    }

    //声明SessionFactory对象,用来创建Session
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionFactory sessionFactory() {
        return super.sessionFactory();
    }

    //声明Session数据访问对象,提供对Session的CRUD操作
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionDAO sessionDAO() {
        return super.sessionDAO();
    }

    //声明SessionManager对象,负责Session管理
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionManager sessionManager() {
        return super.sessionManager();
    }

    //声明SecurityMananger,Shiro中最重要的组件,对象内封装了各个其他对象,用来处理不同的业务
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected SessionsSecurityManager securityManager(List<Realm> realms) {
        return createSecurityManager();
    }

    //创建会话cookie时的模板,后期应用该模板的属性到创建的cookie对象上
    @Bean
    @ConditionalOnMissingBean(name = "sessionCookieTemplate")
    @Override
    protected Cookie sessionCookieTemplate() {
        return super.sessionCookieTemplate();
    }

    //Remember Me Manager,管理RememerMe
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected RememberMeManager rememberMeManager() {
        return super.rememberMeManager();
    }

    //RememberMe模板
    @Bean
    @ConditionalOnMissingBean(name = "rememberMeCookieTemplate")
    @Override
    protected Cookie rememberMeCookieTemplate() {
        return super.rememberMeCookieTemplate();
    }

    //定义了Shiro的Filter和URL的映射关系
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
        return super.shiroFilterChainDefinition();
    }
}
  • ShiroWebFilterConfiguration
    以往spring项目的权限控制大多也是通过过滤器或是拦截器实现的,即请求在到达控制器之前,先经过过滤器或拦截器的处理,如果校验成功,再将数据发送至控制器。Shiro也是利用同样的道理。

package org.apache.shiro.spring.config.web.autoconfigure;

import javax.servlet.DispatcherType;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.AbstractShiroWebFilterConfiguration;
import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @since 1.4.0
 */
@Configuration
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebFilterConfiguration extends AbstractShiroWebFilterConfiguration {

    //ShiroFilter的FactoryBean,用来创建ShiroFilter
    @Bean
    @ConditionalOnMissingBean
    @Override
    protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        return super.shiroFilterFactoryBean();
    }

    //ShiroFilter的RegistrationBean,spring-boot舍弃了web.xml的配置,FilterRegistrationBean就成了添加filter的入口
    @Bean(name = "filterShiroFilterRegistrationBean")
    @ConditionalOnMissingBean
    protected FilterRegistrationBean filterShiroFilterRegistrationBean() throws Exception {

        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR);
        filterRegistrationBean.setFilter((AbstractShiroFilter) shiroFilterFactoryBean().getObject());
        filterRegistrationBean.setOrder(1);

        return filterRegistrationBean;
    }
}

FilterRegistrationBean主要设置了要添加的Filter
是由ShiroFilterFactoryBean所创建。因此,我们重点关注ShiroFilterFactoryBean。

ShiroFilterFactoryBean

ShiroFilterFactoryBean同时实现了FactoryBeanBeanPostProcessor。说明他同时有创建Bean和Bean的后置处理两种能力。简单看一下源代码:


package org.apache.shiro.spring.web;

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {

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

    private SecurityManager securityManager;

    //定义了拦截器name和Filter的映射关系
    private Map<String, Filter> filters;
    //定义了拦截器URL和name的映射关系
    private Map<String, String> filterChainDefinitionMap; //urlPathExpression_to_comma-delimited-filter-chain-definition

    private String loginUrl;
    private String successUrl;
    private String unauthorizedUrl;

    private AbstractShiroFilter instance;

    public ShiroFilterFactoryBean() {
        this.filters = new LinkedHashMap<String, Filter>();
        this.filterChainDefinitionMap = new LinkedHashMap<String, String>(); //order matters!
    }

    public SecurityManager getSecurityManager() {
        return securityManager;
    }

    public void setSecurityManager(SecurityManager securityManager) {
        this.securityManager = securityManager;
    }

    public String getLoginUrl() {
        return loginUrl;
    }

    public void setLoginUrl(String loginUrl) {
        this.loginUrl = loginUrl;
    }

    public String getSuccessUrl() {
        return successUrl;
    }

    public void setSuccessUrl(String successUrl) {
        this.successUrl = successUrl;
    }

    public String getUnauthorizedUrl() {
        return unauthorizedUrl;
    }

    public void setUnauthorizedUrl(String unauthorizedUrl) {
        this.unauthorizedUrl = unauthorizedUrl;
    }

    public Map<String, Filter> getFilters() {
        return filters;
    }

    public void setFilters(Map<String, Filter> filters) {
        this.filters = filters;
    }

    public Map<String, String> getFilterChainDefinitionMap() {
        return filterChainDefinitionMap;
    }

    public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
        this.filterChainDefinitionMap = filterChainDefinitionMap;
    }

    //提供了通过ini配置文件初始化Filter的能力
    public void setFilterChainDefinitions(String definitions) {
        Ini ini = new Ini();
        ini.load(definitions);
        //did they explicitly state a 'urls' section?  Not necessary, but just in case:
        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
        if (CollectionUtils.isEmpty(section)) {
            //no urls section.  Since this _is_ a urls chain definition property, just assume the
            //default section contains only the definitions:
            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        }
        setFilterChainDefinitionMap(section);
    }

    //FactoryBean获取Bean的方法,这里就是获取对应Filter Bean
    public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();
        }
        return instance;
    }

    //FactoryBean获取生成的Bean的类型
    public Class getObjectType() {
        return SpringShiroFilter.class;
    }

    //要生成的Bean是否需要是单例
    public boolean isSingleton() {
        return true;
    }

    //创建FilterChainManager对象,该对象可以将对应的url和filter组成过滤器链
    protected FilterChainManager createFilterChainManager() {

        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        //Shiro自带的Filter,不需要额外配置,可以开箱即用,详见DefualtFilter
        Map<String, Filter> defaultFilters = manager.getFilters();

        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }

        //获取ShiroFactoryBean中自定义的Filters,添加至manager中
        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                //'init' argument is false, since Spring-configured filters should be initialized
                //in Spring (i.e. 'init-method=blah') or implement InitializingBean:
                manager.addFilter(name, filter, false);
            }
        }

        //根据定义的url和filter映射关系,创建过滤器链
        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                manager.createChain(url, chainDefinition);
            }
        }

        return manager;
    }

   //创建ShiroFilter的实例
    protected AbstractShiroFilter createInstance() throws Exception {

        log.debug("Creating Shiro Filter instance.");

        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }

        FilterChainManager manager = createFilterChainManager();

        //Expose the constructed FilterChainManager by first wrapping it in a
        // FilterChainResolver implementation. The AbstractShiroFilter implementations
        // do not know about FilterChainManagers - only resolvers:
        //创建FilterChainResolver对象,FilterChainResolver包装了Manager,ShiroFilter不需要知道Manager对象
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
        //FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class
        //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
        //injection of the SecurityManager and FilterChainResolver:
        //创建ShiroFilter对象
        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }

    private void applyLoginUrlIfNecessary(Filter filter) {
        String loginUrl = getLoginUrl();
        if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
            AccessControlFilter acFilter = (AccessControlFilter) filter;
            //only apply the login url if they haven't explicitly configured one already:
            String existingLoginUrl = acFilter.getLoginUrl();
            if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
                acFilter.setLoginUrl(loginUrl);
            }
        }
    }

    private void applySuccessUrlIfNecessary(Filter filter) {
        String successUrl = getSuccessUrl();
        if (StringUtils.hasText(successUrl) && (filter instanceof AuthenticationFilter)) {
            AuthenticationFilter authcFilter = (AuthenticationFilter) filter;
            //only apply the successUrl if they haven't explicitly configured one already:
            String existingSuccessUrl = authcFilter.getSuccessUrl();
            if (AuthenticationFilter.DEFAULT_SUCCESS_URL.equals(existingSuccessUrl)) {
                authcFilter.setSuccessUrl(successUrl);
            }
        }
    }

    private void applyUnauthorizedUrlIfNecessary(Filter filter) {
        String unauthorizedUrl = getUnauthorizedUrl();
        if (StringUtils.hasText(unauthorizedUrl) && (filter instanceof AuthorizationFilter)) {
            AuthorizationFilter authzFilter = (AuthorizationFilter) filter;
            //only apply the unauthorizedUrl if they haven't explicitly configured one already:
            String existingUnauthorizedUrl = authzFilter.getUnauthorizedUrl();
            if (existingUnauthorizedUrl == null) {
                authzFilter.setUnauthorizedUrl(unauthorizedUrl);
            }
        }
    }

    //配置全局属性,只要是针对特定类型的Filter配置其所需要的URL属性
    private void applyGlobalPropertiesIfNecessary(Filter filter) {
        applyLoginUrlIfNecessary(filter);
        applySuccessUrlIfNecessary(filter);
        applyUnauthorizedUrlIfNecessary(filter);
    }

    //通过后置处理器的机制,直接Filter类型的bean,无需配置
    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;
            applyGlobalPropertiesIfNecessary(filter);
            getFilters().put(beanName, filter);
        } else {
            log.trace("Ignoring non-Filter bean '{}'", beanName);
        }
        return bean;
    }

    /**
     * Does nothing - only exists to satisfy the BeanPostProcessor interface and immediately returns the
     * {@code bean} argument.
     */
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    //SpringShiroFilter只是简单的集成了AbstractShirlFilter,在构造函数中封装了设置SecurityManager和Resolver的操作
    private static final class SpringShiroFilter extends AbstractShiroFilter {

        protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            super();
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            }
            setSecurityManager(webSecurityManager);
            if (resolver != null) {
                setFilterChainResolver(resolver);
            }
        }
    }
}

最重要的步骤是在createInstance()中,该方法主要用来创建ShiroFilter的实例,大致过程分成三步:

  1. 通过createFilterChainManager()创建FilterChainManager对象:

    • 创建FilterChainManager
    • 加载Shiro默认的Filter
    • 加载用户添加的Filter
    • 根据URL和Filter的映射关系,创建过滤器链
  2. 创建FilterChainResolver对象,封装FilterChainManager
  3. 创建ShiroFilter对象,传入SecurityManagerFilterChainReslover

SpringShiroFilter

SpringShiroFilter就是我们通过容器添加的Filter对象。Shiro通过添加该过滤器实现了往集成Spring框架集成的目的。
其实SpringShiroFilter只是单纯的集成了AbstractShiroFilter,在构造函数中增加了设置SecurityManagerFilterChainResolver的过程。所有的逻辑在AbstractShiroFilter中已经定义好了。先从UML图中了解下整个类的继承关系:

其中从上到下看:

  • Filter
    Filter是Serlvet包提供的。由Servlet规范定义Filter基本的行为。
  • ServletContextSupport
    提供了操作ServletContext的能力
  • AbstractFilter
    初步实现了Filter,定义了init的过程(分成setFilterConfigonFilterConfigSet两部分),提供了设置和获取配置的方法。
  • Nameable
    增加了命名的能力
  • NameableFilter
    为Filter增加命名能力
  • OncePerRequestFilter
    主要定义了doFilter的过程,并在该过程中限制了一次请求该Filter只处理一次。
    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
            //先判断是否已经被处理过
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( //已经处理过,则不再处理 request.getAttribute(alreadyFilteredAttributeName) != null ) {
            log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());
            filterChain.doFilter(request, response);
        } else //noinspection deprecation
        //未处理过,但是不可用,也不处理
            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 {
        //调用Filter处理,并添加已处理属性
            // Do invoke this filter...
            log.trace("Filter '{}' not yet executed.  Executing now.", getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

            try {
            //抽象方法,交给子类实现
                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);
            }
        }
    }
  • AbstractShiroFilter
    AbstractShiroFilter已经在Filter的处理过程中添加了Shiro的验证。
package org.apache.shiro.web.servlet;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.ExecutionException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.subject.WebSubject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.Callable;

public abstract class AbstractShiroFilter extends OncePerRequestFilter {

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

    private static final String STATIC_INIT_PARAM_NAME = "staticSecurityManagerEnabled";

    // Reference to the security manager used by this filter
    private WebSecurityManager securityManager;

    // Used to determine which chain should handle an incoming request/response
    private FilterChainResolver filterChainResolver;

    /**
     * Whether or not to bind the constructed SecurityManager instance to static memory (via
     * SecurityUtils.setSecurityManager).  This was added to support https://issues.apache.org/jira/browse/SHIRO-287
     * @since 1.2
     */
    private boolean staticSecurityManagerEnabled;

    protected AbstractShiroFilter() {
        this.staticSecurityManagerEnabled = false;
    }

    public WebSecurityManager getSecurityManager() {
        return securityManager;
    }

    public void setSecurityManager(WebSecurityManager sm) {
        this.securityManager = sm;
    }

    public FilterChainResolver getFilterChainResolver() {
        return filterChainResolver;
    }

    public void setFilterChainResolver(FilterChainResolver filterChainResolver) {
        this.filterChainResolver = filterChainResolver;
    }

    public boolean isStaticSecurityManagerEnabled() {
        return staticSecurityManagerEnabled;
    }

    public void setStaticSecurityManagerEnabled(boolean staticSecurityManagerEnabled) {
        this.staticSecurityManagerEnabled = staticSecurityManagerEnabled;
    }

    //重写了AbstractFilter中的空实现,主要设置额外的配置
    protected final void onFilterConfigSet() throws Exception {
        //added in 1.2 for SHIRO-287:
        applyStaticSecurityManagerEnabledConfig();
        init();
        ensureSecurityManager();
        //added in 1.2 for SHIRO-287:
        if (isStaticSecurityManagerEnabled()) {
            SecurityUtils.setSecurityManager(getSecurityManager());
        }
    }

    private void applyStaticSecurityManagerEnabledConfig() {
        String value = getInitParam(STATIC_INIT_PARAM_NAME);
        if (value != null) {
            Boolean b = Boolean.valueOf(value);
            if (b != null) {
                setStaticSecurityManagerEnabled(b);
            }
        }
    }

    public void init() throws Exception {
    }

    private void ensureSecurityManager() {
        WebSecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            log.info("No SecurityManager configured.  Creating default.");
            securityManager = createDefaultSecurityManager();
            setSecurityManager(securityManager);
        }
    }

    protected WebSecurityManager createDefaultSecurityManager() {
        return new DefaultWebSecurityManager();
    }

    protected boolean isHttpSessions() {
        return getSecurityManager().isHttpSessionMode();
    }

    protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
        return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
        ServletRequest toUse = request;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest http = (HttpServletRequest) request;
            toUse = wrapServletRequest(http);
        }
        return toUse;
    }

    protected ServletResponse wrapServletResponse(HttpServletResponse orig, ShiroHttpServletRequest request) {
        return new ShiroHttpServletResponse(orig, getServletContext(), request);
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) {
        ServletResponse toUse = response;
        if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) &&
                (response instanceof HttpServletResponse)) {
            //the ShiroHttpServletResponse exists to support URL rewriting for session ids.  This is only needed if
            //using Shiro sessions (i.e. not simple HttpSession based sessions):
            toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request);
        }
        return toUse;
    }

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

    //对Session生命周期未交给Web容器管理的情况,由Shiro自己维护
    @SuppressWarnings({"UnusedDeclaration"})
    protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {
        if (!isHttpSessions()) { //'native' sessions
            Subject subject = SecurityUtils.getSubject();
            //Subject should never _ever_ be null, but just in case:
            if (subject != null) {
                Session session = subject.getSession(false);
                if (session != null) {
                    try {
                        session.touch();
                    } catch (Throwable t) {
                        log.error("session.touch() method invocation has failed.  Unable to update" +
                                "the corresponding session's last access time based on the incoming request.", t);
                    }
                }
            }
        }
    }

    //重载OncePerRequestFilter的空实现,定义了Filter处理逻辑
    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

        Throwable t = null;

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

            //创建Subject对象
            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    //更新Session访问时间
                    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);
        }
    }

    //获取待执行的过滤器链
    protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
        FilterChain chain = origChain;

        //获取过滤器链解析器
        FilterChainResolver resolver = getFilterChainResolver();
        if (resolver == null) {
            log.debug("No FilterChainResolver configured.  Returning original FilterChain.");
            return origChain;
        }

        //使用解析器对请求进行解析,
        FilterChain resolved = resolver.getChain(request, response, origChain);
        if (resolved != null) {
            //如果该请求URI是需要Shiro拦截的,则由shiro创建代理的过滤器链,
            //在执行原始的过滤器前,插入Shiro过滤器的执行过程
            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;
    }

    //执行拦截器链
    protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
            throws IOException, ServletException {
        //获取拦截器链
        FilterChain chain = getExecutionChain(request, response, origChain);
        //拦截器链处理拦截工作
        chain.doFilter(request, response);
    }
}

其中比较重要的有两个:onFilterConfigSet()doFilterInternal()
onFilterConfigset()AbstractFilter中是空实现,这里重载了该方法,主要是添加一些额外配置。
doFilterInternal()主要流程:

  • 先封装了请求和响应
  • 获取请求代表的Subject对象
  • 更新session最后访问的时间(托管给WEB容器的session不需要shiro管理访问时间)
  • 执行拦截器链executeChain:
    • 获取拦截器链: 如果FilterChainResolver解析到请求的URL是shiro拦截器拦截的URL,则产生代理的FilterChain,让shiro的拦截器集成进拦截器;否则使用原始的拦截器链
    • 拦截器链开始工作

FilterChainResolver和FilterChainManager的工作

之前简单介绍过FilterChainResolver封装了FilterChainManger对象,现在再来看下FilterChainResovler的代码:

public class PathMatchingFilterChainResolver implements FilterChainResolver {

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

    //封装了FilterChainManager
    private FilterChainManager filterChainManager;

    //负责比较URL
    private PatternMatcher pathMatcher;

    public PathMatchingFilterChainResolver() {
        this.pathMatcher = new AntPathMatcher();
        this.filterChainManager = new DefaultFilterChainManager();
    }

    public PathMatchingFilterChainResolver(FilterConfig filterConfig) {
        this.pathMatcher = new AntPathMatcher();
        this.filterChainManager = new DefaultFilterChainManager(filterConfig);
    }

    public PatternMatcher getPathMatcher() {
        return pathMatcher;
    }

    public void setPathMatcher(PatternMatcher pathMatcher) {
        this.pathMatcher = pathMatcher;
    }

    public FilterChainManager getFilterChainManager() {
        return filterChainManager;
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public void setFilterChainManager(FilterChainManager filterChainManager) {
        this.filterChainManager = filterChainManager;
    }

    //获取拦截器链
    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }

        //获取URL
        String requestURI = getPathWithinApplication(request);

        //匹配URL
        for (String pathPattern : filterChainManager.getChainNames()) {

            //如果匹配,则由filterChainManager创建代理过的Filter Chain
            if (pathMatches(pathPattern, requestURI)) {
                if (log.isTraceEnabled()) {
                    log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " +
                            "Utilizing corresponding filter chain...");
                }
                return filterChainManager.proxy(originalChain, pathPattern);
            }
        }

        return null;
    }

    protected boolean pathMatches(String pattern, String path) {
        PatternMatcher pathMatcher = getPathMatcher();
        return pathMatcher.matches(pattern, path);
    }

    protected String getPathWithinApplication(ServletRequest request) {
        return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
    }
}

这个类的代码相对简单,大概过程是根据FilterChainManager配置的URI和Filter的映射关系,比较请求是否需要经过Shiro的Filter,如果需要则交给了FilterChainManager对象创建代理过的FilterChain,从而加入了Shiro的处理流程。可以看到主要的内容都是由FilterChainManager完成。因此我们再来看FilterChainManager类的proxy方法:

    public FilterChain proxy(FilterChain original, String chainName) {
        //NamedFilterList对象可以简单理解为一个命名了Filter的list,是之前解析filter时,创建的
        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);
    }

NamedFilterList创建代理的过程其实就是创建ProxiedFilterChain对象。

public class ProxiedFilterChain implements FilterChain {

    //TODO - complete JavaDoc

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

    private FilterChain orig;
    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.");
        }
        //封装了原始filterChain对象和shiro的filter对象
        this.orig = orig;
        this.filters = filters;
        this.index = 0;
    }

    //实现了filterChain的doFilter方法
    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
        //如果不含有shiro的filter,或是已经遍历完了shiro的filter,则调用原始的fiter chain的方法
        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.");
            }
            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);
        }
    }
}

源码读到这里已经大致了解到shiro是如何集成至spring-boot的。接下来再以一个Shiro的Filter为例,具体了解下authentication的流程。

从FormAuthenticatingFilter了解Shiro的authentication过程

一样先看Filter的继承结构:

OncePerRequestFilter父级的结构已经在前文介绍过。现在关注点放在它的子类上。

  • AdviceFilter:
    主要实现了doFilterInternal方法,将filter的过程再切成preHandleexecuteChainpostHandlecleanup四个阶段。有点类似spring中的拦截器。在方法前,方法后做了拦截。并根据返回的结果决定是否继续再filterChain中往下走:
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        Exception exception = null;

        try {
            //前置处理,根据返回结构决定是否继续走之后的filter
            boolean continueChain = preHandle(request, response);
            if (log.isTraceEnabled()) {
                log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");
            }

            //继续调用之后的filter
            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);
        }
    }
  • PathMatchingFilter:根据url是否匹配的逻辑实现preHandle方法。并提供onPreHandle方法让子类可以修改preHandle方法返回的值。
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;
        }

        //匹配请求URL,决定是否需要经过shiro的filter
        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;
    }

    @SuppressWarnings({"JavaDoc"})
    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});
            }
            //添加onPreHandle方法
            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;
    }

    //默认返回true,子类可以重载该方法实现自己的控制逻辑
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return true;
    }
  • AccessControlFilter:主要重载了onPreHandle方法,增加了isAccessAllowedonAccessDenied方法。可以在该方法中实现访问控制的逻辑。
   public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }
  • AuthenticationFilter:实现了isAccessAllowed方法,通过subject.isAuthenticated()决定是否允许访问。
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        return subject.isAuthenticated();
    }
  • AuthenticatingFilter:除了主要的isAccessAllowedcleanup方法之外,还实现了executeLogin方法,主要是从请求中获取登录的用户名密码,通过shiro进行登录验证
  • FormAuthenticationFilter:最后具体看一下FormAuthenticationFilter
public class FormAuthenticationFilter extends AuthenticatingFilter {

    //TODO - complete JavaDoc

    public static final String DEFAULT_ERROR_KEY_ATTRIBUTE_NAME = "shiroLoginFailure";

    public static final String DEFAULT_USERNAME_PARAM = "username";
    public static final String DEFAULT_PASSWORD_PARAM = "password";
    public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe";

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

    private String usernameParam = DEFAULT_USERNAME_PARAM;
    private String passwordParam = DEFAULT_PASSWORD_PARAM;
    private String rememberMeParam = DEFAULT_REMEMBER_ME_PARAM;

    private String failureKeyAttribute = DEFAULT_ERROR_KEY_ATTRIBUTE_NAME;

    public FormAuthenticationFilter() {
        setLoginUrl(DEFAULT_LOGIN_URL);
    }

    @Override
    public void setLoginUrl(String loginUrl) {
        String previous = getLoginUrl();
        if (previous != null) {
            this.appliedPaths.remove(previous);
        }
        super.setLoginUrl(loginUrl);
        if (log.isTraceEnabled()) {
            log.trace("Adding login url to applied paths.");
        }
        this.appliedPaths.put(getLoginUrl(), null);
    }

    public String getUsernameParam() {
        return usernameParam;
    }

    public void setUsernameParam(String usernameParam) {
        this.usernameParam = usernameParam;
    }

    public String getPasswordParam() {
        return passwordParam;
    }

    public void setPasswordParam(String passwordParam) {
        this.passwordParam = passwordParam;
    }

    public String getRememberMeParam() {
        return rememberMeParam;
    }

    public void setRememberMeParam(String rememberMeParam) {
        this.rememberMeParam = rememberMeParam;
    }

    public String getFailureKeyAttribute() {
        return failureKeyAttribute;
    }

    public void setFailureKeyAttribute(String failureKeyAttribute) {
        this.failureKeyAttribute = failureKeyAttribute;
    }

    //
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //如果isAccessAllowed校验失败,则判断是否是登录请求
        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;
        }
    }

    @SuppressWarnings({"UnusedDeclaration"})
    protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) {
        return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD);
    }

    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
        String username = getUsername(request);
        String password = getPassword(request);
        return createToken(username, password, request, response);
    }

    protected boolean isRememberMe(ServletRequest request) {
        return WebUtils.isTrue(request, getRememberMeParam());
    }

    //定义了一些重定向动作
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) throws Exception {
        issueSuccessRedirect(request, response);
        //we handled the success redirect directly, prevent the chain from continuing:
        return false;
    }

    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        if (log.isDebugEnabled()) {
            log.debug( "Authentication exception", e );
        }
        setFailureAttribute(request, e);
        //login failed, let request continue back to the login page:
        return true;
    }

    protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
        String className = ae.getClass().getName();
        request.setAttribute(getFailureKeyAttribute(), className);
    }

    protected String getUsername(ServletRequest request) {
        return WebUtils.getCleanParam(request, getUsernameParam());
    }

    protected String getPassword(ServletRequest request) {
        return WebUtils.getCleanParam(request, getPasswordParam());
    }

}

总结

shiro通过FilterRegistrationBean添加了ShiroFilter。ShiroFilter在针对需要登录验证的请求,将原始的fiterChain进行代理,从而集成了shiro权限验证的filter。

原文地址:https://www.cnblogs.com/insaneXs/p/11041877.html

时间: 2024-08-05 08:15:26

Shiro(二):Spring-boot如何集成Shiro(上)的相关文章

细说shiro之五:在spring框架中集成shiro

官网:https://shiro.apache.org/ 1. 下载在Maven项目中的依赖配置如下: <!-- shiro配置 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${version.shiro}</version> </dependency&g

Spring Boot Admin 集成自定义监控告警

Spring Boot Admin 集成自定义监控告警 前言 Spring Boot Admin 是一个社区项目,可以用来监控和管理 Spring Boot 应用并且提供 UI,详细可以参考 官方文档. Spring Boot Admin 本身提供监控告警功能,但是默认只提供了 Hipchat.Slack 等国外流行的通讯软件的集成,虽然也有邮件通知,不过考虑到使用体检决定二次开发增加 钉钉 通知. 本文基于 Spring Boot Admin 目前最新版 1.5.7. 准备工作 Spring

spring boot admin 集成的简单配置随笔

和我并肩作战的同事也要相继离职了,心里还是有很多不舍得,现在业务也陆陆续续落在了肩头,上午项目经理让我把spring boot admin集成到现在的项目中,已遍后续的监控. 哇!我哪里搞过这个!心里好慌,好在我面向对象虽然不是很精通,但是面向百度我倒是很拿手,于是开启了,面向百度编程,现在已经成功过了~写个博客继续一下,方便以后使用以及分享. 注:此写法适用于 2.0以下版本 高于2.0请直接官方文档走起:http://codecentric.github.io/spring-boot-adm

spring boot 2 集成JWT实现api接口认证

JSON Web Token(JWT)是目前流行的跨域身份验证解决方案.官网:https://jwt.io/本文spring boot 2 集成JWT实现api接口验证. 一.JWT的数据结构 JWT由header(头信息).payload(有效载荷)和signature(签名)三部分组成的,用“.”连接起来的字符串.JWT的计算逻辑如下:(1)signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(

Spring Boot:整合Shiro权限框架

综合概述 Shiro是Apache旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证.授权.加密.会话管理等功能,与Spring Security一样属基于权限的安全框架,但是与Spring Security 相比,Shiro使用了比较简单易懂易于使用的授权方式.Shiro属于轻量级框架,相对于Spring Security简单很多,并没有security那么复杂. 优势特点 它是一个功能强大.灵活的.优秀的.开源的安全框架. 它可以胜任身份验证.授权.企业会话管理和加密等工作. 它

Spring Boot 快速入门 史上最简单

1.Spring Boot 概述 Spring Boot 是所有基于 Spring 开发的项目的起点.Spring Boot 的设计是为了让你尽可能快的跑起来 Spring 应用程序并且尽可能减少你的配置文件. 2.什么是 Spring Boot Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程.该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置.用我的话来理解,就是spring boot其实不是什么新

Spring Boot 嵌入式 Tomcat 文件上传、url 映射虚拟路径

1.Java web 应用开发完成后如果是导入外置的 Tomcat 的 webapps 目录的话,那么上传的文件可以直接的放在应用的 web 目录下去就好了,浏览器可以很方便的进行访问. 2.Spring Boot 默认使用嵌入式 Tomcat ,将来打包成可执行 Jar 文件进行部署,显然打成 jar 包后,总不可能再将上传的文件放在 resources 目录下去了. 3.Spring Boot 于是提供了 url 地址匹配本地虚拟路径的功能: 1)上传文件到服务器,服务器将文件保存到了本地,

docker部署spring boot项目在服务器上

IDE:idea 工具:docker spring boot:2.0.1 ======================================== 简单记录一下流程,以供参考: 第一步:首先得有一个spring boot的项目[集成了jpa+mybatis的spring boot 例子:https://github.com/AngelSXD/swapping] 第二步:项目打包为jar包 install成功以后 找到项目根目录下的target目录,这里面的jar就是打包好可以部署的jar

spring boot Rabbitmq集成,延时消息队列实现

本篇主要记录Spring boot 集成Rabbitmq,分为两部分, 第一部分为创建普通消息队列, 第二部分为延时消息队列实现: spring boot提供对mq消息队列支持amqp相关包,引入即可: [html] view plain copy <!-- rabbit mq --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-

(二)Spring Boot配置

1.配置文件 Spring Boot使用一个全局的配置文件,额皮质文件名是固定的; application.properties application.yml 配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好; YAML(YAML Ain't Markup Language) YAML A Markup Language:是一个标记语言 YAML isn't Markup Language:不是一个标记语言; 标记语言: 以前的配置文件;大