前言

  开心一刻

    小明的朋友骨折了,小明去他家里看他。他老婆很细心的为他换药,敷药,然后出去买菜。小明满脸羡慕地说:你特么真幸福啊,你老婆对你那么好!朋友哭得稀里哗啦的说:兄弟你别说了,我幸福个锤子,就是她把我打骨折的。

图片 1

揣摩下此刻男人的内心

  路漫漫其修远兮,吾将上下而求索!

  github:

  码云:

前言

  开心一刻

    已经报废了一年多的电脑,今天特么突然开机了,吓老子一跳,只见电脑管家缓缓地出来了,本次开机一共用时一年零六个月,打败了全国0%的电脑,电脑管家已经对您的电脑失去信心,然后它把自己卸载了,只剩我在一旁发呆。

图片 2

  路漫漫其修远兮,吾将上下而求索!

  github:

  码云:

前言

  开心一刻 

    我和儿子有个共同的心愿,出国旅游。昨天儿子考试得了全班第一,我跟媳妇合计着带他出国见见世面,吃晚饭的时候,一家人开始了讨论这个。我:“儿子,你的心愿是什么?”,儿子:“吃汉堡包”,我:“往大了说”,儿子:“变形金刚”,我:“今天你爹说了算,想想咱俩共同的心愿”,儿子怯生生的瞅了媳妇一眼说:“换个妈?”,我心里咯噔一下:“这虎犊子,坑自己也就算了,怎么还坑爹呢”。

图片 3牛:小子,你的杀气太重,我早就看穿一切,吃我一脚!

  路漫漫其修远兮,吾将上下而求索!

  github:

  码云:

前言

  开心一刻

    老师对小明说:”乳就是小的意思,比如乳猪就是小猪,乳名就是小名,请你用乳字造个句”
    小明:”我家很穷,只能住在40平米的乳房”
    老师:”…, 这个不行,换一个”
    小明:”我每天上学都要跳过我家门口的一条乳沟”
    老师:”……, 这个也不行,再换一个”
    小明:”老师,我想不出来了,把我的乳头都想破了!”

  路漫漫其修远兮,吾将上下而求索!

  github:

  码云:

前言

  开心一刻

    开学了,表弟和同学因为打架,老师让他回去叫家长。表弟硬气的说:不用,我打得过他。老师板着脸对他说:和你打架的那位同学已经回去叫家长了。表弟犹豫了一会依然硬气的说:可以,两个我也打得过。老师:……

  路漫漫其修远兮,吾将上下而求索!

  github:

  码云:

前情回顾

  上篇中主要讲了两点认证与授权,认证主要FormAuthenticationFilter和AnonymousFilter两个filter来控制,shiro对所有请求都会先生成ProxiedFilterChain,请求会经过ProxiedFilterChain,先执行shiro的filter链,再执行剩下的servlet
Filter链,最后来到我们的Controller。

  认证过程是通过filter控制实现的,我们所有的请求由shiro中3个Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分摊了,LogoutFilter负责/logout,AnonymousFilter负责/login和静态资源,FormAuthenticationFilter则负责剩下的,三个filter只会有一个生效(注意filter的配置顺序);当FormAuthenticationFilter生效的时候会进行登录认证,认证过程:先从缓存获取authenticationInfo,没有则通过realm从数据库获取并放入缓存,然后将页面输入的用户信息(UsernamePasswordToken)与authenticationInfo进行匹配验证,认证通过会将subject中的authenticated设置成true,表示当前subject已经被认证过了。关于认证缓存,个人不建议开启,因为当修改用户信息后,不好处理缓存中的authenticationInfo,另外认证频率本来就不高,缓存的意义不大。

  一般情况下授权是通过注解方式实现的,注解配合aop会在我们的业务方法前织入前置权限检查处理,检查过程与认证过程类似:从缓存中获取authorizationInfo,没有则通过realm从数据库获取,然后放入缓存,然后将authorizationInfo与@RequiresPermissions中的xxx来进行匹配,完成权限检查,检查通过则进入我们的目标方法,不通过则抛出异常。关于权限缓存,个人建议开启,因为权限的验证还是挺频繁的,如果不开启缓存,那么会给数据库造成一定的压力。

shiroFilter的注册

  此篇博文讲到了springboot的filter注册,但只是filter注册的一种方式:通过FilterRegistrationBean实现。而Shiro
Filter的注册是采用另外的方式实现的,我们接着往下看

  ShiroFilterFactoryBean实现了FactoryBean,我们来看下ShiroFilterFactoryBean对FactoryBean的getObject方法的实现

图片 4

git图一

    可以看到createInstance()返回的是SpringShiroFilter的实例;SpringShiroFilter的类图如下

图片 5

  getObject方法只是创建了一个SpringShiroFilter实例,并注册到了spring容器中,那是如何注册到servlet容器的呢?我们来跟下源码

    这篇博文其实涉及到了,只是那时候没细讲,我们还是从那里开始

图片 6

gif图二

    ServletContextInitializerBeans.java的构造方法,里面有addServletContextInitializerBeans(beanFactory)方法和addAdaptableBeans(beanFactory),我们来好好瞅一瞅

前情回顾与补充

前情回顾

  shiro的session创建与session的查询、更新、过期、删除中,shiro对session的操作基本都讲到了,但还缺一个session共享没有讲解;session共享的原理其实在自定义session管理一文已经讲过了,本文不讲原理,只看看shiro的session共享的实现。

前情回顾

  大家还记得上篇博文讲了什么吗,我们来一起简单回顾下:

    HttpServletRequestWrapper是HttpServletRequest的装饰类,我们通过继承HttpServletRequestWrapper来实现我们自定义的HttpServletRequest:CustomizeSessionHttpServletRequest,重写CustomizeSessionHttpServletRequest的getSession,将其指向我们自定义的session。然后通过Filter将CustomizeSessionHttpServletRequest添加到Filter
chain中,使得到达Servlet的ServletRequest是我们的CustomizeSessionHttpServletRequest。

  今天不讲session共享,我们先来看看shiro的session创建

遗留问题解答

  上篇遗留问题:session过期后,我们再请求,shiro是如何处理并跳转到登录页的?回答这个问题之前,我们先看看另外一个问题:

上篇博文中讲到了登录认证成功后会将subject的authenticated设置成true,表示当前subject已经被认证过了,但是只是当前subject;我们可以将subject看成是request,每次请求来的时候都会将request/response对封装成subject,AbstractShiroFilter的方法doFilterInternal中有这样一个调用final Subject subject = createSubject(request, response);我们来看看createSubject的方法描述:    Creates a  WebSubject instance to associate with the incoming request/response pair which will be used throughout the request/response execution.    创建一个关联request/response对的WebSubject实例,用于后续request/response的执行

  也就是目前我们还只是看到了当前请求有认证状态,当前会话还没有看到认证状态;撇开shiro,如果是我们自己实现,我们会怎么实现,肯定会在subject的authenticated设置成true的时候也将认证状态也设置在session中,至于是存储在自定义session的某个标志字段(类似subject的authenticated)中,还是存储在session的attributes中(setAttribute(Object
key, Object value)进行设置),看我们的需求和个人喜好。

  归纳下这个问题:shiro是如何保存当前会话认证状态的,是上述中的某种实现方式,还是shiro有另外的实现方式

    addServletContextInitializerBeans(beanFactory)

图片 7图片 8

private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {    for (Entry<String, ServletContextInitializer> initializerBean : getOrderedBeansOfType(            beanFactory, ServletContextInitializer.class)) {        addServletContextInitializerBean(initializerBean.getKey(),                initializerBean.getValue(), beanFactory);    }}private void addServletContextInitializerBean(String beanName,        ServletContextInitializer initializer, ListableBeanFactory beanFactory) {    if (initializer instanceof ServletRegistrationBean) {        Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();        addServletContextInitializerBean(Servlet.class, beanName, initializer,                beanFactory, source);    }    else if (initializer instanceof FilterRegistrationBean) {        Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();        addServletContextInitializerBean(Filter.class, beanName, initializer,                beanFactory, source);    }    else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {        String source = ((DelegatingFilterProxyRegistrationBean) initializer)                .getTargetBeanName();        addServletContextInitializerBean(Filter.class, beanName, initializer,                beanFactory, source);    }    else if (initializer instanceof ServletListenerRegistrationBean) {        EventListener source = ((ServletListenerRegistrationBean<?>) initializer)                .getListener();        addServletContextInitializerBean(EventListener.class, beanName, initializer,                beanFactory, source);    }    else {        addServletContextInitializerBean(ServletContextInitializer.class, beanName,                initializer, beanFactory, initializer);    }}

View Code

      会将spring
bean工厂(beanFactory)中类型是ServletContextInitalizer类型的实例(包括ServletRegistrationBean、FilterRegistrationBean、DelegatingFilterProxyRegistrationBean、ServletListenerRegistrationBean)添加进ServletContextInitializerBeans的initializers属性中。

private final MultiValueMap<Class<?>, ServletContextInitializer> initializers;

      ServletContextInitalizer的子类图如下所示

图片 9

      这也就是上篇博文注册Filter的方式,以RegistrationBean方式实现的,但是SpringShiroFilter不是在这添加进ServletContextInitializerBeans的initializers中的哦

  回顾

    在上篇博文中,我们讲到了SpringShiroFilter是如何注册到servlet容器的:SpringShiroFilter首先注册到spring容器,然后被包装成FilterRegistrationBean,最后通过FilterRegistrationBean注册到servlet容器,至此shiro的Filter加入到了servlet容器的FilterChain中。另外还讲到了shiro的代理FilterChain:ProxiedFilterChain,请求来到shiro的Filter后,会先经过shiro的Filter链,再接着走servlet容器的Filter链,如下图所示

图片 10

    如果请求经PathMatchingFilterChainResolver匹配成功,那么请求会先经过shiro
Filter链(ProxiedFilterChain),之后再走剩下的servlet
Filter链,如果匹配不成功,则直接走剩下的servlet
Filter链。每一次请求都会经过shiro Filter,shiro
Filter来控制filter链的走向(有点类似springmvc的DispatcherServlet),先生成ProxiedFilterChain,请求先走ProxiedFilterChain,然后再走接着走servlet
filter链。

    上图中,在单独的shiro工程中,shiro
Filter是ShiroFilter,而在与spring的集成工程中则是SpringShiroFilter。

  为何需要session共享

    如果是单机应用,那么谈不上session共享,session放哪都无所谓,不在乎放到默认的servlet容器中,还是抽出来放到单独的地方;

    也就是说session共享是针对集群(或分布式、或分布式集群)的;如果不做session共享,仍然采用默认的方式(session存放到默认的servlet容器),当我们的应用是以集群的方式发布的时候,同个用户的请求会被分发到不同的集群节点(分发依赖具体的负载均衡规则),那么每个处理同个用户请求的节点都会重新生成该用户的session,这些session之间是毫无关联的。那么同个用户的请求会被当成多个不同用户的请求,这肯定是不行的。

SecurityManager

  SecurityManager,安全管理器;即所有与安全相关的操作都会与SecurityManager交互;它管理着所有Subject,所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;SecurityManager是shiro的核心,它负责与shiro的其他组件进行交互,类似SpringMVC中的DispatcherServlet或Struts2中的FilterDispatcher。

  我们在使用shiro的时候,首先都会先初始化SecurityManager,然后往SecurityManager中注入shiro的其他组件,像sessionManager、realm等。我们的spring-boot-shiro中初始化的是DefaultWebSecurityManager,如下

图片 11图片 12

@Beanpublic SecurityManager securityManager(AuthorizingRealm myShiroRealm, CacheManager shiroRedisCacheManager) {    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();    securityManager.setCacheManager(shiroRedisCacheManager);    securityManager.setRememberMeManager(cookieRememberMeManager;    securityManager.setRealm(myShiroRealm);    return securityManager;}

View Code

  shiro是如何保存会话认证状态的

    每次请求都会生成新的subject,如果我们把认证状态只放到subject中,那么每次请求都需要进行认证,这显然是不合理的,我们需要将认证状态保存到会话中,那么整个会话期间只需要认证一次即可。那么shiro是怎么实现的了,我们来看看源码,从我们Controller的doLogin方法开始

图片 13

    如果我们继续跟进s.setAttribute(attributeKey,
value),会发现认证状态最终存放到了SimpleSession的attributes属性中,

private transient Map<Object, Object> attributes;

    也就是说认证状态会存在session的attributes中,正是我们上面说的方式之一,shiro没有用它特有的方式,最终在session中的存在形式如下图

图片 14

    看过上篇博客的朋友应该会有印象:FormAuthenticationFilter的isAccessAllowed方法(从AuthenticatingFilter继承)中第一个认证的是subject的authenticated

图片 15

    为什么获取subject的authenticated,而不是直接获取session的认证状态,我还没弄清楚为什么,难道是为了组件的分工明确?
既然shiro这么做了肯定有它的道理,我们先不纠结这个(知道的朋友可以评论区提示下)。我们知道登录成功后,subject的authenticated会被赋值成true,但是登录成功后的其他请求,比如:

图片 16

    可以看到,在创建subject的时候,会将session中的认证状态赋值给subject的authenticated。

    小结下:登录时,登录成功会将认证状态存储到session的attributes中,之后的每一次请求,在创建subject的时候,都会将session中的认证状态赋值给subject的authenticated,那么FormAuthenticationFilter在认证的时候会直接返回true,继续走servlet
filter链,最终来到我们的Controller。

    至此,该问题就明了了,会话认证状态还是保存在session中,只是中间处理的时候会将session中的认证状态赋值给subject,由subject传递给FormAuthenticationFilter认证状态。

    addAdaptableBeans(beanFactory)

图片 17图片 18

private void addAdaptableBeans(ListableBeanFactory beanFactory) {    MultipartConfigElement multipartConfig = getMultipartConfig(beanFactory);    addAsRegistrationBean(beanFactory, Servlet.class,            new ServletRegistrationBeanAdapter(multipartConfig));    addAsRegistrationBean(beanFactory, Filter.class,            new FilterRegistrationBeanAdapter;    for (Class<?> listenerType : ServletListenerRegistrationBean            .getSupportedTypes {        addAsRegistrationBean(beanFactory, EventListener.class,                (Class<EventListener>) listenerType,                new ServletListenerRegistrationBeanAdapter;    }}

View Code

      会将beanFactory中的Servlet、Filter、Listener实例封装成对应的RegistrationBean,然后添加到ServletContextInitializerBeans的initializers;部分细节没去跟,有兴趣的可以自行去跟下源代码。

    ServletContextInitializerBeans的sortedList的内容最终如下

图片 19

    然后遍历这个sortedList,逐个注入到servlet容器

图片 20

  补充

    shiro的Filter关系图

图片 21

shiro filter关系图

      此关系图中涉及到了shiro的入口:ShiroFilter或SpringShiroFilter,认证拦截器:FormAuthenticationFilter,没有涉及授权Filter(PermissionsAuthorizationFilter、RolesAuthorizationFilter),因为shiro的授权我们一般用的是注解的方式,而不是Filter方式。

    ShiroFilterFactoryBean中的createFilterChainManager()

图片 22图片 23

protected FilterChainManager createFilterChainManager() {    DefaultFilterChainManager manager = new DefaultFilterChainManager();    Map<String, Filter> defaultFilters = manager.getFilters();    //apply global settings if necessary:    应用全局设置    for (Filter filter : defaultFilters.values {        applyGlobalPropertiesIfNecessary;    }    //Apply the acquired and/or configured filters: 应用和配置filter,一般没有    Map<String, Filter> filters = getFilters();    if (!CollectionUtils.isEmpty {        for (Map.Entry<String, Filter> entry : filters.entrySet {            String name = entry.getKey();            Filter filter = entry.getValue();            applyGlobalPropertiesIfNecessary;            if (filter instanceof Nameable) {                ( filter).setName;            }            //'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);        }    }    //build up the chains:  构建shiro filter链    Map<String, String> chains = getFilterChainDefinitionMap();    if (!CollectionUtils.isEmpty {        for (Map.Entry<String, String> entry : chains.entrySet {            String url = entry.getKey();            String chainDefinition = entry.getValue();            manager.createChain(url, chainDefinition);        }    }    return manager;}

View Code

      1、给shiro默认的filter应用全局配置

图片 24图片 25

//apply global settings if necessary:for (Filter filter : defaultFilters.values {    applyGlobalPropertiesIfNecessary;}private void applyGlobalPropertiesIfNecessary(Filter filter) {    applyLoginUrlIfNecessary;            // 设置filter的loginUrl    applySuccessUrlIfNecessary;            // 设置filter的successUrl    applyUnauthorizedUrlIfNecessary;    // 这个我们一般没有配置}private void applyLoginUrlIfNecessary(Filter filter) {    String loginUrl = getLoginUrl();    // shiroFilterFactoryBean.setLoginUrl; 设置的loginUrl    if (StringUtils.hasText && (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;        }    }}private void applySuccessUrlIfNecessary(Filter filter) {    String successUrl = getSuccessUrl();    // shiroFilterFactoryBean.setSuccessUrl; 设置的successUrl    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);        }    }}

View Code

图片 26

shiro 默认11个filter

        标红的的filter的loginUrl和successUrl会被设置成我们在ShiroFilterFactoryBean配置的,loginUrl会被设置成”/login”,successUrl被设置成”index”;这里我们需要关注下AnonymousFilter、LogoutFilter和FormAuthenticationFilter,我们目前只用到了这三个filter。

      2、应用和配置我们在ShiroFilterFactoryBean设置的Filters

        ShiroFilterFactoryBean类有个setFilters(Map<String,Filter>
filters>方法,可以通过此方法向shiro注册filter,不过我们一般没有用到。

      3、构建filter链

图片 27        会将ShiroFilterFactoryBean中private
Map<String, String>
filterChainDefinitionMap的元素逐个放到DefaultFilterChainManager的private
Map<String, NamedFilterList>
filterChains中,最终filterChains的内容如下

图片 28

        我们配置的filterChainDefinitionMap中涉及到3个Filter,LogoutFilter负责/logout,AnonymousFilter负责(/login,/favicon.ico,/js/**,/css/**,/img/**,/fonts/**),FormAuthenticationFilter负责/**。至此,filter链准备工作完成。

  如何实现session共享

    实现方式其实有很多,甚至可以不做session共享,具体有哪些,大家自行去查资料。本文提供一种方式:redis实现session共享,就是将session从servlet容器抽出来,放到redis中存储,所有集群节点都从redis中对session进行操作。

  SecurityManager类图

    结构如下,认真看看,注意看下属性

图片 29

    顶层组件SecurityManager直接继承了SessionManager且提供了SessionsSecurityManager实现,SessionsSecurityManager直接把会话管理委托给相应的SessionManager;SecurityManager的默认实现:DefaultSecurityManager及DefaultWebSecurityManager都继承了SessionsSecurityManager,也就是说:默认情况下,session的管理由DefaultSecurityManager或DefaultWebSecurityManager中的SessionManager来负责。

  session过期后,我们再请求,shiro是如何处理并跳转到登录页的

    如果我们明白了上个问题,那么这个问题就很好理解了。如果session过期,那么通过sessionDAO获取的session为null,subject的authenticated就会被赋值成false,那么在FormAuthenticationFilter中认证不通过,则会重定向到/login,让用户重新进行登录认证。事实是这样吗,我们来跟下源码(假设此时请求是:

图片 30

    可以看到,请求进过SpringShiroFilter时,subject中authenticated被设置成false,然后生成ProxiedFilterChain

图片 31

    请求会来到FormAuthenticationFilter,认证不通过,重定向到/login,并返回false,表示filter链不用继续往下走了(具体可查看上篇博文)。

    强调下:很多对session的操作,都会同步到缓存,包括session刷新、设置session属性(setAttribute()等等,具体可以看AbstractNativeSessionManager,很多session操作中都会调用onChange方法

protected void onChange(Session session) {    sessionDAO.update;}

  小结

    springboot下有3种方式注册Filter(Servlet、Listener类似),FilterRegistrationBean、@WebFilter
和@Bean,@WebFilter我还没试过,另外这3种方式注册的Filter的优先级是:FilterRegistrationBean
>@WebFilter
>@Bean(网上查阅的资料,我没试哦,使用过程中需要注意!)。

    不管是FilterRegistrationBean方式、@WebFilter方式,还是@Bean方式,只要是受spring容器管理,最终都会添加到ServletContextInitializerBeans的initializers中,都会成功注册到servlet容器。@WebFilter方式和@Bean方式注册的Filter都会被封装成FilterRegistrationBean,然后添加进ServletContextInitializerBeans的initializers;3种方式最终殊途同归,都以FilterRegistrationBean的形式存在ServletContextInitializerBeans的initializers中。SpringShiroFilter的注册算是@Bean方式注册的,至此SpringShiroFilter就注册到了servlet容器中了。

    ServletContextInitializerBeans的sortedList如下:

private List<ServletContextInitializer> sortedList;

    是一个有序的ServletContextInitializer
List,这个有序针对的同类型,比如所有的FilterRegistrationBean有序,所有的ServletRegistrationBean有序,FilterRegistrationBean与ServletRegistrationBean之间有没有序是无意义的。

认证

  身份认证,即在应用中谁能证明他就是他本人。认证方式有很多,用的最多的就是用户名/密码来证明。shiro中,用户需要提供pricipals和credentials给shiro,从而应用能够验证用户身份。一个主体可以有多个principals,但只有一个Primary
principals,一般是用户名/手机号,credentials是一个只有主体知道的安全值,一般是用户名/数字证书。最常见的principals和credentials组合就是用户名
/ 密码了。

  接下来我们来看看一次完整的请求 :未登录 – 登录 – 登录成功
。还记得是哪个filter注册到了servlet
filter链吗?,就是SpringShiroFilter,每次请求都会经过SpringShiroFilter;从shiro
filter关系图中可知,请求肯定会经过OncePerRequestFilter的doFilter方法,我们就从此方法开始

SessionDAO

  SessionDAO其实是用于session持久化的,但里面有缓存部分,具体细节我们往下看

  shiro已有SessionDAO的实现如下

图片 32

  SessionDAO接口提供的方法如下

图片 33图片 34

package org.apache.shiro.session.mgt.eis;import org.apache.shiro.session.Session;import org.apache.shiro.session.UnknownSessionException;import java.io.Serializable;import java.util.Collection;/** * 从EIS操作session的规范(EIS:例如关系型数据库, 文件系统, 持久化缓存等等, 具体依赖DAO实现) * 提供了典型的CRUD的方法:create, readSession, update, delete */public interface SessionDAO {    /**     * 插入一个新的sesion记录到EIS      */    Serializable create(Session session);    /**     * 根据会话ID获取会话     */    Session readSession(Serializable sessionId) throws UnknownSessionException;    /**     * 更新session; 如更新session最后访问时间/停止会话/设置超时时间/设置移除属性等会调用     */    void update(Session session) throws UnknownSessionException;    /**     * 删除session; 当会话过期/会话停止会调用     */    void delete(Session session);    /**     * 获取当前所有活跃session, 所有状态不是stopped/expired的session     * 如果用户量多此方法影响性能     */    Collection<Session> getActiveSessions();}

View Code

    SessionDAO给出了从持久层(一般而言是关系型数据库)操作session的标准。

  AbstractSessionDAO提供了SessionDAO的基本实现,如下

图片 35图片 36

package org.apache.shiro.session.mgt.eis;import org.apache.shiro.session.Session;import org.apache.shiro.session.UnknownSessionException;import org.apache.shiro.session.mgt.SimpleSession;import java.io.Serializable;/** * SessionDAO的抽象实现, 在会话创建和读取时做一些健全性检查,并在需要时允许可插入的会话ID生成策略. * SessionDAO的update和delete则留给子类来实现 * EIS需要子类自己实现 */public abstract class AbstractSessionDAO implements SessionDAO {    /**     * sessionId生成器     */    private SessionIdGenerator sessionIdGenerator;    public AbstractSessionDAO() {        this.sessionIdGenerator = new JavaUuidSessionIdGenerator();    // 指定JavaUuidSessionIdGenerator为默认sessionId生成器    }    /**     * 获取sessionId生成器     */    public SessionIdGenerator getSessionIdGenerator() {        return sessionIdGenerator;    }    /**     * 设置sessionId生成器     */    public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {        this.sessionIdGenerator = sessionIdGenerator;    }    /**     * 生成一个新的sessionId, 并将它应用到session实例     */    protected Serializable generateSessionId(Session session) {        if (this.sessionIdGenerator == null) {            String msg = "sessionIdGenerator attribute has not been configured.";            throw new IllegalStateException;        }        return this.sessionIdGenerator.generateId;    }    /**     * SessionDAO中create实现; 将创建的sesion保存到EIS.     * 子类doCreate方法的代理,具体的细节委托给了子类的doCreate方法     */    public Serializable create(Session session) {        Serializable sessionId = doCreate;        verifySessionId(sessionId);        return sessionId;    }    /**     * 保证从doCreate返回的sessionId不是null,并且不是已经存在的.     * 目前只实现了null校验,是否已存在是没有校验的,可能shiro的开发者会在后续补上吧.     */    private void verifySessionId(Serializable sessionId) {        if (sessionId == null) {            String msg = "sessionId returned from doCreate implementation is null.  Please verify the implementation.";            throw new IllegalStateException;        }    }    /**     * 分配sessionId给session实例     */    protected void assignSessionId(Session session, Serializable sessionId) {        ((SimpleSession) session).setId(sessionId);    }    /**     * 子类通过实现此方法来持久化Session实例到EIS.     */    protected abstract Serializable doCreate(Session session);    /**     * SessionDAO中readSession实现; 通过sessionId从EIS获取session对象.     * 子类doReadSession方法的代理,具体的获取细节委托给了子类的doReadSession方法.     */    public Session readSession(Serializable sessionId) throws UnknownSessionException {        Session s = doReadSession(sessionId);        if (s == null) {            throw new UnknownSessionException("There is no session with id [" + sessionId + "]");        }        return s;    }    /**     * 子类通过实现此方法从EIS获取session实例     */    protected abstract Session doReadSession(Serializable sessionId);}

View Code

    SessionDao的基本实现,实现了SessionDao的create、readSession(具体还是依赖AbstractSessionDAO子类的doCreate、doReadSession实现);同时加入了自己的sessionId生成器,负责sessionId的操作。

  CachingSessionDAO提供了session缓存的功能,如下

图片 37图片 38

package org.apache.shiro.session.mgt.eis;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheManager;import org.apache.shiro.cache.CacheManagerAware;import org.apache.shiro.session.Session;import org.apache.shiro.session.UnknownSessionException;import org.apache.shiro.session.mgt.ValidatingSession;import java.io.Serializable;import java.util.Collection;import java.util.Collections;/** * 应用层与持久层(EIS,如关系型数据库、文件系统、NOSQL)之间的缓存层实现 * 缓存着所有激活状态的session * 实现了CacheManagerAware,会在shiro加载的过程中调用此对象的setCacheManager方法 */public abstract class CachingSessionDAO extends AbstractSessionDAO implements CacheManagerAware {    /**     * 激活状态的sesion的默认缓存名     */    public static final String ACTIVE_SESSION_CACHE_NAME = "shiro-activeSessionCache";    /**     * 缓存管理器,用来获取session缓存     */    private CacheManager cacheManager;    /**     * 用来缓存session的缓存实例     */    private Cache<Serializable, Session> activeSessions;    /**     * session缓存名, 默认是ACTIVE_SESSION_CACHE_NAME.     */    private String activeSessionsCacheName = ACTIVE_SESSION_CACHE_NAME;    public CachingSessionDAO() {    }    /**     * 设置缓存管理器     */    public void setCacheManager(CacheManager cacheManager) {        this.cacheManager = cacheManager;    }    /**     * 获取缓存管理器     */    public CacheManager getCacheManager() {        return cacheManager;    }    /**     * 获取缓存实例的名称,也就是获取activeSessionsCacheName的值     */    public String getActiveSessionsCacheName() {        return activeSessionsCacheName;    }    /**     * 设置缓存实例的名称,也就是设置activeSessionsCacheName的值     */    public void setActiveSessionsCacheName(String activeSessionsCacheName) {        this.activeSessionsCacheName = activeSessionsCacheName;    }    /**     * 获取缓存实例     */    public Cache<Serializable, Session> getActiveSessionsCache() {        return this.activeSessions;    }    /**     * 设置缓存实例     */    public void setActiveSessionsCache(Cache<Serializable, Session> cache) {        this.activeSessions = cache;    }    /**     * 获取缓存实例     * 注意:不会返回non-null值     *     * @return the active sessions cache instance.     */    private Cache<Serializable, Session> getActiveSessionsCacheLazy() {        if (this.activeSessions == null) {            this.activeSessions = createActiveSessionsCache();        }        return activeSessions;    }    /**     * 创建缓存实例     */    protected Cache<Serializable, Session> createActiveSessionsCache() {        Cache<Serializable, Session> cache = null;        CacheManager mgr = getCacheManager();        if (mgr != null) {            String name = getActiveSessionsCacheName();            cache = mgr.getCache;        }        return cache;    }    /**     * AbstractSessionDAO中create的重写     * 调用父类(AbstractSessionDAO)的create方法, 然后将session缓存起来     * 返回sessionId     */    public Serializable create(Session session) {        Serializable sessionId = super.create;    // 调用父类的create方法        cache(session, sessionId);                        // 以sessionId作为key缓存session        return sessionId;    }    /**     * 从缓存中获取session; 若sessionId为null,则返回null     */    protected Session getCachedSession(Serializable sessionId) {        Session cached = null;        if (sessionId != null) {            Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();            if (cache != null) {                cached = getCachedSession(sessionId, cache);            }        }        return cached;    }    /**     * 从缓存中获取session     */    protected Session getCachedSession(Serializable sessionId, Cache<Serializable, Session> cache) {        return cache.get(sessionId);    }    /**     * 缓存session,以sessionId作为key     */    protected void cache(Session session, Serializable sessionId) {        if (session == null || sessionId == null) {            return;        }        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();        if (cache == null) {            return;        }        cache(session, sessionId, cache);    }    protected void cache(Session session, Serializable sessionId, Cache<Serializable, Session> cache) {        cache.put(sessionId, session);    }    /**     * AbstractSessionDAO中readSession的重写     * 先从缓存中获取,若没有则调用父类的readSession方法获取session     */    public Session readSession(Serializable sessionId) throws UnknownSessionException {        Session s = getCachedSession(sessionId);        // 从缓存中获取        if (s == null) {            s = super.readSession(sessionId);           // 调用父类readSession方法获取        }        return s;    }    /**     * SessionDAO中update的实现     * 更新session的状态     */    public void update(Session session) throws UnknownSessionException {        doUpdate;                               // 更新EIS中的session        if (session instanceof ValidatingSession) {            if (((ValidatingSession) session).isValid {                cache(session, session.getId;         // 更新缓存中的session            } else {                uncache;                        // 移除缓存中的sesson            }        } else {            cache(session, session.getId;        }    }    /**     * 由子类去实现,持久化session到EIS     */    protected abstract void doUpdate(Session session);    /**     * SessionDAO中delete的实现     * 删除session     */    public void delete(Session session) {        uncache;                                // 从缓存中移除        doDelete;                               // 从EIS中删除    }    /**     * 由子类去实现,从EIS中删除session     */    protected abstract void doDelete(Session session);    /**     * 从缓存中移除指定的session     */    protected void uncache(Session session) {        if (session == null) {            return;        }        Serializable id = session.getId();        if (id == null) {            return;        }        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();        if (cache != null) {            cache.remove;        }    }    /**     * SessionDAO中getActiveSessions的实现     * 获取所有的存活的session     */    public Collection<Session> getActiveSessions() {        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();        if (cache != null) {            return cache.values();        } else {            return Collections.emptySet();        }    }}

View Code

    是应用层与持久化层之间的缓存层,不用频繁请求持久化层以提升效率。重写了AbstractSessionDAO中的create、readSession方法,实现了SessionDAO中的update、delete、getActiveSessions方法,预留doUpdate和doDelele给子类去实现(doXXX方法操作的是持久层)

  MemorySessionDAO,SessionDAO的简单内存实现,如下

图片 39图片 40

package org.apache.shiro.session.mgt.eis;import org.apache.shiro.session.Session;import org.apache.shiro.session.UnknownSessionException;import org.apache.shiro.util.CollectionUtils;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.io.Serializable;import java.util.Collection;import java.util.Collections;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;/** * 基于内存的SessionDao的简单实现,所有的session存在ConcurrentMap中 * DefaultSessionManager默认用的MemorySessionDAO */public class MemorySessionDAO extends AbstractSessionDAO {    private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class);    private ConcurrentMap<Serializable, Session> sessions;                // 存放session的容器    public MemorySessionDAO() {        this.sessions = new ConcurrentHashMap<Serializable, Session>();    }    // AbstractSessionDAO 中doCreate的重写; 将session存入sessions    protected Serializable doCreate(Session session) {        Serializable sessionId = generateSessionId;        // 生成sessionId        assignSessionId(session, sessionId);                        // 将sessionId赋值到session        storeSession(sessionId, session);                            // 存储session到sessions        return sessionId;    }    // 存储session到sessions    protected Session storeSession(Serializable id, Session session) {        if (id == null) {            throw new NullPointerException("id argument cannot be null.");        }        return sessions.putIfAbsent(id, session);    }    // AbstractSessionDAO 中doReadSession的重写; 从sessions中获取session    protected Session doReadSession(Serializable sessionId) {        return sessions.get(sessionId);    }    // SessionDAO中update的实现; 更新sessions中指定的session    public void update(Session session) throws UnknownSessionException {        storeSession(session.getId(), session);    }    // SessionDAO中delete的实现; 从sessions中移除指定的session    public void delete(Session session) {        if (session == null) {            throw new NullPointerException("session argument cannot be null.");        }        Serializable id = session.getId();        if (id != null) {            sessions.remove;        }    }    // SessionDAO中SessionDAO中delete的实现的实现; 获取sessions中全部session    public Collection<Session> SessionDAO中delete的实现() {        Collection<Session> values = sessions.values();        if (CollectionUtils.isEmpty {            return Collections.emptySet();        } else {            return Collections.unmodifiableCollection;        }    }}

View Code

    将session保存在内存中,存储结构是ConcurrentHashMap;项目中基本不用,即使我们不实现自己的SessionDAO,一般用的也是EnterpriseCacheSessionDAO。

  EnterpriseCacheSessionDAO,提供了缓存功能的session维护,如下

图片 41图片 42

package org.apache.shiro.session.mgt.eis;import org.apache.shiro.cache.AbstractCacheManager;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheException;import org.apache.shiro.cache.MapCache;import org.apache.shiro.session.Session;import java.io.Serializable;import java.util.concurrent.ConcurrentHashMap;public class EnterpriseCacheSessionDAO extends CachingSessionDAO {    public EnterpriseCacheSessionDAO() {                // 设置默认缓存器,并实例化MapCache作为cache实例        setCacheManager(new AbstractCacheManager() {            @Override            protected Cache<Serializable, Session> createCache(String name) throws CacheException {                return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>;            }        });    }    // AbstractSessionDAO中doCreate的重写;     protected Serializable doCreate(Session session) {        Serializable sessionId = generateSessionId;        assignSessionId(session, sessionId);        return sessionId;    }    // AbstractSessionDAO中doReadSession的重写    protected Session doReadSession(Serializable sessionId) {        return null; //should never execute because this implementation relies on parent class to access cache, which        //is where all sessions reside - it is the cache implementation that determines if the        //cache is memory only or disk-persistent, etc.    }    // CachingSessionDAO中doUpdate的重写    protected void doUpdate(Session session) {        //does nothing - parent class persists to cache.    }    // CachingSessionDAO中doDelete的重写    protected void doDelete(Session session) {        //does nothing - parent class removes from cache.    }}

View Code

    设置了默认的缓存管理器(AbstractCacheManager)和默认的缓存实例,实现了缓存效果。从父类继承的持久化操作方法都是空实现,也就说EnterpriseCacheSessionDAO是没有实现持久化操作的,仅仅只是简单的提供了缓存实现。当然我们可以继承EnterpriseCacheSessionDAO,重写doXXX方法来实现持久化操作。

  总结下:SessionDAO定义了从持久层操作session的标准;AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等;CachingSessionDAO提供了对开发者透明的session缓存的功能,只需要设置相应的
CacheManager
即可;MemorySessionDAO直接在内存中进行session维护;而EnterpriseCacheSessionDAO提供了缓存功能的session维护,默认情况下使用
MapCache
实现,内部使用ConcurrentHashMap保存缓存的会话。因为shiro不知道我们需要将session持久化到哪里(关系型数据库,还是文件系统),所以只提供了MemorySessionDAO持久化到内存(听起来怪怪的,内存中能说成持久层吗)

    DefaultSecurityManager

      默认安全管理器,用于我们的javaSE安全管理,一般而言用到的少,但我们需要记住,万一哪次有这个需求呢。

      我们来看下他的构造方法

图片 43

      默认的sessionManager是DefaultSessionManager,DefaultSessionManager具体详情请看下文。

shiro源码系列

  系列地址

shiro中的Filter链

  未登录

    url请求:

    那么此时的url与我们配置的哪个filterChainDefinition匹配呢?很显然是filterChainDefinitionMap.put(“/**”,
“authc”)。authc是shiro中默认11个filter中FormAuthenticationFilter的名字,那么也就是说生成的ProxiedFilterChain如下所示

图片 44

    也就是请求会先经过FormAuthenticationFilter,之后再回到servlet
filter链:orig。那我们接着看请求到FormAuthenticationFilter中后做了些什么处理(注意看shiro
filter关系图)

图片 45

    executeChain(request, response,
chain)继续执行filter链之前有个preHandle(request,
response)处理,来判断时候需要继续执行filter链。跟进去会来到onPreHandle方法

图片 46图片 47

public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);    }protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {    return super.isAccessAllowed(request, response, mappedValue) ||            (!isLoginRequest(request, response) && isPermissive(mappedValue));}// super.isAccessAllowed判断时候已经认证过,有个标志字段:authenticated// isLoginRequest判断是否是登录请求,很显然不是,登录请求是/login,目前是/// isPermissive 没搞明白,可能应对一些特殊的filterprotected boolean onAccessDenied(ServletRequest request,     ServletResponse response, Object mappedValue) throws Exception {    return onAccessDenied(request, response);}protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {    if (isLoginRequest(request, response)) {    // 是否是登录请求        if (isLoginSubmission(request, response)) {    // 是否是post请求            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;    // get方式的登录请求则继续执行filter链,最终会来到我们的controller的登录get请求        }    } else {        if (log.isTraceEnabled {            log.trace("Attempting to access a path which requires authentication.  Forwarding to the " +                    "Authentication url [" + getLoginUrl() + "]");        }        saveRequestAndRedirectToLogin(request, response); // 重定向到/login        return false;    // 返回false,表示filter链不继续执行了    }}

View Code

    最终会重定向到/login,这又是一次新的get请求,会重新将上面的流程走一遍,只是url变成了:

图片 48

    请求来到AnonymousFilter之后,onPreHandler直接返回true,接着走剩下的servlet
Filter链,最终来到我们的controller

@GetMapping("/login")public String loginPage() {    return "login";}

    将登录页返回回去

shiro session共享

    DefaultWebSecurityManager

      默认web安全管理器,用于我们的web安全管理;一般而言,我们的应用中初始化此安全管理器。

      我们来看看其构造方法

图片 49

图片 50图片 51

public DefaultWebSecurityManager() {    super();                                                    // 会调用SessionsSecurityManager的构造方法,实例化DefaultSessionManager    ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator;    this.sessionMode = HTTP_SESSION_MODE;    setSubjectFactory(new DefaultWebSubjectFactory;    setRememberMeManager(new CookieRememberMeManager;    setSessionManager(new ServletContainerSessionManager;    // 设置sessionManager,替换掉上面的DefaultSessionManager}

View Code

      可以看出此时的sessionManager是ServletContainerSessionManager,ServletContainerSessionManager具体详情请看下文。

    由此可知默认情况下,DefaultSecurityManager会将session管理委托给DefaultSessionManager,而DefaultWebSecurityManager则将session管理委托给ServletContainerSessionManager。

    我们可以通过继承DefaultSecurityManager或DefaultWebSecurityManager来实现自定义SecurityManager,但一般而言没必要,DefaultSecurityManager和DefaultWebSecurityManager基本能满足我们的需要了,我们根据需求二选其一即可。无论DefaultSecurityManager还是DefaultWebSecurityManager,我们都可以通过setSessionManager方法来指定sessionManager,如果不指定sessionManager的话就用的SecurityManager默认的sessionManager。

  shiro源码篇 – shiro的session创建,你值得拥有

    SessionManager负责session的操作,包括创建、维护、删除、失效、验证等;

    AbstractNativeSessionManager的start是创建session的入口;

    SimpleSession是shiro完完全全的自己实现,是shiro对session的一种拓展。但SimpleSession不对外暴露,我们一般操作的是SimpleSession的代理:DelegatingSession,或者是DelegatingSession的代理:StoppingAwareProxiedSession;对session的操作,会通过一层层代理,来到DelegatingSession,DelegatingSession将session的操作转交给sessionMananger,sessionManager通过一些校验后,最后转交给SimpleSession处理。

  shiro的默认filter列表

    除了SpringShiroFilter之外,shiro还有默认的11个Filter;细心的朋友应该在git图一中已经发现了,在创建DefaultFilterChainManager,就把默认的11个Filter添加到它的filters中

private Map<String, Filter> filters; //pool of filters available for creating chainsprivate Map<String, NamedFilterList> filterChains; //key: chain name, value: chainpublic DefaultFilterChainManager() {    this.filters = new LinkedHashMap<String, Filter>();    this.filterChains = new LinkedHashMap<String, NamedFilterList>();    addDefaultFilters(false);    // 将默认的11个Filter添加到filters}

    这11个Filter具体如下

anon(AnonymousFilter.class),authc(FormAuthenticationFilter.class),authcBasic(BasicHttpAuthenticationFilter.class),logout(LogoutFilter.class),noSessionCreation(NoSessionCreationFilter.class),perms(PermissionsAuthorizationFilter.class),port(PortFilter.class),rest(HttpMethodPermissionFilter.class),roles(RolesAuthorizationFilter.class),ssl(SslFilter.class),user(UserFilter.class);

  登录

    url请求:

    流程与上面未登录差不多,此时的ProxiedFilterChain如下所示

图片 52

    AnonymousFilter的onPreHandler方法直接返回的true,请求会接着走剩下的servlet
Filter链,最终来到我们的controller

图片 53图片 54

@PostMapping("/login")@ResponseBodypublic OwnResult dologin(String username, String password) {    username = username.trim();    // 判断当前用户是否可用    User user = userService.findUserByUsername;    if(user == null) {        return OwnResult.build(RespCode.ERROR_USER_NOT_EXIST.getCode(), username + " 用户不存在");    }    if (user.getStatus() == Constants.USER_DISABLED) {        return OwnResult.build(RespCode.ERROR_USER_DISABLED.getCode(), "账号已被禁用, 请联系管理员");    }    UsernamePasswordToken token = new UsernamePasswordToken(username, password);    Subject subject = SecurityUtils.getSubject();    try {        subject.login;        // 登录认证交给shiro        return OwnResult.ok();    } catch (AuthenticationException e) {        return OwnResult.build(RespCode.ERROR_USERNAME_PASSWORD.getCode(), "用户名或密码错误");    }}

View Code

    登录认证过程委托给了shiro,我们来看看具体的认证过程

图片 55

    如果开启了认证缓存(authenticationCachingEnabled=true),则会先从缓存中获取authenticationInfo,若没有则调用我们自定义Realm的doGetAuthenticationInfo方法获取数据库中用户的信息,并缓存起来;然后将authenticationInfo与登录页面输入的用户信息(封装成UsernamePasswordToken)进行匹配验证。登录认证失败会抛出AuthenticationException;登录成功则会将subject的authenticated设置成true,表示已经认证过了。

    注意:登录认证没有完全交给shiro,而是在我们的controller中委托给shiro了,这与完全交由shiro还是有区别的(具体可以看下FormAuthenticationFilter的onAccessDenied方法)。

  共享实现

    shiro的session共享其实是比较简单的,重写CacheManager,将其操作指向我们的redis,然后实现我们自己的CachingSessionDAO定制缓存操作和缓存持久化。

    自定义CacheManager

      ShiroRedisCacheManager

图片 56图片 57

package com.lee.shiro.config;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheException;import org.apache.shiro.cache.CacheManager;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class ShiroRedisCacheManager implements CacheManager {    @Autowired    private Cache shiroRedisCache;    @Override    public <K, V> Cache<K, V> getCache throws CacheException {        return shiroRedisCache;    }}

View Code

      ShiroRedisCache

图片 58图片 59

package com.lee.shiro.config;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheException;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Component;import java.util.Collection;import java.util.Set;import java.util.concurrent.TimeUnit;@Componentpublic class ShiroRedisCache<K,V> implements Cache<K,V>{    @Autowired    private RedisTemplate<K,V> redisTemplate;    @Value("${spring.redis.expireTime}")    private long expireTime;    @Override    public V get throws CacheException {        return redisTemplate.opsForValue;    }    @Override    public V put throws CacheException {        redisTemplate.opsForValue().set(k,v,expireTime, TimeUnit.SECONDS);        return null;    }    @Override    public V remove throws CacheException {        V v = redisTemplate.opsForValue;        redisTemplate.opsForValue().getOperations().delete;        return v;    }    @Override    public void clear() throws CacheException {    }    @Override    public int size() {        return 0;    }    @Override    public Set<K> keys() {        return null;    }    @Override    public Collection<V> values() {        return null;    }}

View Code

    自定义CachingSessionDAO

      继承EnterpriseCacheSessionDAO,然后重新设置其CacheManager(替换掉默认的内存缓存器),这样也可以实现我们的自定义CachingSessionDAO,但是这是优选吗;如若我们实现持久化,继承EnterpriseCacheSessionDAO是优选,但如果只是实现session缓存,那么CachingSessionDAO是优选,自定义更灵活。那么我们还是继承CachingSessionDAO来实现我们的自定义CachingSessionDAO

      ShiroSessionDAO

图片 60图片 61

package com.lee.shiro.config;import org.apache.shiro.session.Session;import org.apache.shiro.session.mgt.eis.CachingSessionDAO;import org.springframework.stereotype.Component;import java.io.Serializable;@Componentpublic class ShiroSessionDAO extends CachingSessionDAO {    @Override    protected void doUpdate(Session session) {    }    @Override    protected void doDelete(Session session) {    }    @Override    protected Serializable doCreate(Session session) {        // 这里绑定sessionId到session,必须要有        Serializable sessionId = generateSessionId;        assignSessionId(session, sessionId);        return sessionId;    }    @Override    protected Session doReadSession(Serializable sessionId) {        return null;    }}

View Code

    最后将ShiroSessionDAO实例赋值给SessionManager实例,再讲SessionManager实例赋值给SecurityManager实例即可

    具体代码请参考spring-boot-shiro

SessionManager

  shiro提供了完整的会话管理功能,不依赖底层容器,JavaSE应用和JavaEE应用都可以使用。会话管理器管理着应用中所有Subject的会话,包括会话的创建、维护、删除、失效、验证等工作。

  shiro源码篇 – shiro的session的查询、刷新、过期与删除,你值得拥有

    一般操作的session是session的代理,代理将session操作委托给sessionManager,sesionManager校验之后再转交给SimpleSession;

    session过期定时任务默认60分钟执行一次,所session已过期或不合法,则抛出对应的异常,上层通过捕获异常从sessionDAO中删除session;

    不只定时任务做session的校验,session的基本操作都在sessionManager中有做session的校验,例如touch、setAttribute等,具体可以查看AbstractNativeSessionManager,对session的操作都是通过AbstractNativeSessionManager处理后转交给SimpleSession。

  Filter链

    SpringShiroFilter注册到servlet容器中,请求肯定会经过SpringShiroFilter的doFilter方法,我们就从此开始跟一跟源代码

图片 62

gif图三

    上图中可能展示的不够细,主要就是两点:1、路径匹配:pathMatches(pathPattern,
requestURI),默认的Fliter逐个与请求URI进行匹配;2、代理FilterChain:ProxiedFilterChain。如果匹配不上,那么直接走servlet的FilterChain,否则先走shiro的代理FilterChain(ProxiedFilterChain),之后再走servlet的FilterChain。

    ProxiedFilterChain源代码如下

图片 63图片 64

/* * Licensed to the Apache Software Foundation  under one * or more contributor license agreements.  See the NOTICE file * distributed with this work for additional information * regarding copyright ownership.  The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License.  You may obtain a copy of the License at * *     http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied.  See the License for the * specific language governing permissions and limitations * under the License. */package org.apache.shiro.web.servlet;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.servlet.*;import java.io.IOException;import java.util.List;/** * A proxied filter chain is a {@link FilterChain} instance that proxies an original {@link FilterChain} as well * as a {@link List List} of other {@link Filter Filter}s that might need to execute prior to the final wrapped * original chain.  It allows a list of filters to execute before continuing the original  * {@code FilterChain} instance. * * @since 0.9 */public class ProxiedFilterChain implements FilterChain {    //TODO - complete JavaDoc    private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);    private FilterChain orig;                    // 原FilterChain,也就是servlet容器的FilterChain    private List<Filter> filters;                // shiro默认的11个Filter    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.");            }            this.orig.doFilter(request, response);                        // 当shiro的11个Filter走完之后,继续走servlet容器的FilterChain        } else {            if (log.isTraceEnabled {                log.trace("Invoking wrapped filter at index [" + this.index + "]");            }            this.filters.get(this.index++).doFilter(request, response, this);    // 递归逐个走shiro的11个Filter        }    }}

View Code

    Shiro对Servlet容器的FilterChain进行了代理,即ShiroFilter在继续Servlet容器的Filter链的执行之前,通过ProxiedFilterChain对Servlet容器的FilterChain进行了代理;即先走Shiro自己的Filter体系,然后才会委托给Servlet容器的FilterChain进行Servlet容器级别的Filter链执行;Shiro的ProxiedFilterChain执行流程:1、先执行Shiro自己的Filter链;2、再执行Servlet容器的Filter链(即原始的
Filter)。

  登录成功

    登录成功后,我们往往会请求主页,url请求:

    流程与上面两个差不多,此时的ProxiedFilterChain如下所示

图片 65

    此时authenticated已经为true,会接着走余下的servlet
Filler链,最终请求会来到我们的controller

图片 66图片 67

@RequestMapping({"/","/index"})public String index(Model model){    List<Menu> menus = menuService.listMenu();    model.addAttribute("menus", menus);    model.addAttribute("username", getUsername;    return "index_v1";}

View Code

    将index_v1.html返回回去

  源码解析

    底层还是利用Filter
+HttpServletRequestWrapper将对session的操作接入到自己的实现中来,而不走默认的servlet容器,这样对session的操作完全由我们自己掌握。

    shiro的session创建中其实讲到了shiro中对session操作的基本流程,这里不再赘述,没看的朋友可以先去看看再回过头来看这篇。本文只讲shiro中,如何将一个请求的session接入到自己的实现中来的;shiro中有很多默认的filter,我会单独开一篇来讲shiro的filter,这篇我们先不纠结这些filter。

    OncePerRequestFilter中doFilter方法如下

图片 68图片 69

public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)        throws ServletException, IOException {    String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();    if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {    // 当前filter已经执行过了,进行下一个filter        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 ) {    // 当前filter未被启用或忽略此filter,则进行下一个filter;shouldNotFilter已经被废弃了        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 {            // 执行当前filter            doFilterInternal(request, response, filterChain);        } finally {            // 一旦请求完成,我们清除当前filter的"已经过滤"的状态            request.removeAttribute(alreadyFilteredAttributeName);        }    }}

View Code

图片 70

    上图中,我可以看到AbstractShiroFilter的doFilterInternal放中将request封装成了shiro自定义的ShiroHttpServletRequest,将response也封装成了shiro自定义的ShiroHttpServletResponse。既然Filter中将request封装了ShiroHttpServletRequest,那么到我们应用的request就是ShiroHttpServletRequest类型,也就是说我们对session的操作最终都是由shiro完成的,而不是默认的servlet容器。

    另外补充一点,shiro的session创建不是懒创建的。servlet容器中的session创建是第一次请求session(第一调用request.getSession时才创建。shiro的session创建如下图

图片 71

    此时,还没登录,但是subject、session已经创建了,只是subject的认证状态为false,说明还没进行登录认证的。至于session创建过程已经保存到redis的流程需要大家自行去跟,或者阅读我之前的博文

  SessionManager类图

图片 72

  shiro源码篇 – shiro的session共享,你值得拥有

    session共享实现的原理其实都是一样的,都是filter +
HttpServletRequestWrapper,只是实现细节会有所区别;

    shiro的session共享其实是比较简单的,重写CacheManager,将其操作指向我们的redis,然后定制CachingSessionDAO实现session缓存操作和session持久化;

    如果session需要持久化,推荐自定义sessionDAO继承EnterpriseCacheSessionDAO,如果只是缓存,则推荐自定义sessionDAO继承CachingSessionDAO。

总结

  1、SpringShiroFilter注册到spring容器,会被包装成FilterRegistrationBean,通过FilterRegistrationBean注册到servlet容器;

  2、一般而言,shiro的PathMatchingFilterChainResolver会匹配所有的请求,Shiro对Servlet容器的FilterChain进行了代理,生成代理FilterChain:ProxiedFilterChain,请求先走Shiro自己的Filter链,再走Servelt容器的Filter链;

  3、题外话,springboot注册Filter、Servlet、Listener方式类似,都有3种,具体是哪三种,大家去上文看;关于Shiro的Filter,本文没做更详细的讲解,需要了解的可以去看《跟我学shiro》

授权

  授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。授权中有几个需要了解的关键对象:主体、资源、权限(Permission)、角色。主体:即访问应用的用户,shiro中使用Subject代表该用户;资源:应用中用户可以访问的任何东西,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法等;权限:表示在应用中用户有没有操作某个资源的权力,能不能访问某个资源;角色:可以理解成权限的集合,一般情况下我们会赋予用户角色而不是权限,这样用户可以拥有一组权限,赋予权限时比较方便。

总结

  1、当以集群方式对外提供服务的时候,不做session共享也是可以的

    可以通过ip_hash的机制将同个ip的请求定向到同一台后端,这样保证用户的请求始终是同一台服务处理,与单机应用基本一致了;但这有很多方面的缺陷,不推荐使用。

  2、servlet容器之间做session同步也是可以实现session共享的

    一个servlet容器生成session,其他节点的servlet容器从此servlet容器进行session同步,以达到session信息一致。这个也不推荐,某个时间点会有session不一致的问题,毕竟同步过程受到各方面的影响,不能保证session实时一致。

  3、session共享实现的原理其实都是一样的,都是filter +
HttpServletRequestWrapper,只是实现细节会有所区别;有兴趣的可以看下spring-session的实现细节。

  4、如果我们采用的spring集成shiro,其实可以将缓存管理器交由spring管理,相当于由spring统一管理缓存。

  5、shiro的CacheManager不只是管理session缓存,还管理着身份认证缓存、授权缓存,shiro的缓存都是CacheManager管理。但是身份认证缓存默认是关闭的,个人也不推荐开启。

  6、shiro的session创建时机是在登录认证之前,而不是第一次调用getSession()时。

      DefaultSessionManager

      DefaultSecurityManager默认使用的SessionManager,用于JavaSE环境的session管理。

图片 73

      通过上图可知(结合SecurityManager类图),session创建的关键入口是SessionsSecurityManager的start方法,此方法中会将session的创建任务委托给具体的SessionManager实现。

      DefaultSessionManager继承自AbstractNativeSessionManager,没用重写start方法,所以此时AbstractNativeSessionManager的start方法会被调用,start方法如下

图片 74图片 75

public Session start(SessionContext context) {    Session session = createSession;            // 创建session,类型是SimpleSession    applyGlobalSessionTimeout;                    // 设置session的timeout,也就是有效时间,默认30分钟    onStart(session, context);                                notifyStart;                                // 通知session监听器    //Don't expose the EIS-tier Session object to the client-tier:    return createExposedSession(session, context);        // 创建对外暴露的session,SimpleSession的代理;类型是DelegatingSession,持有sessionManager的引用}

View Code

      跟进createSession(),代码入下

图片 76图片 77

protected Session createSession(SessionContext context) throws AuthorizationException {    enableSessionValidationIfNecessary();        // 初次被调用时启动定时任务来验证session,具体请看下篇博客    return doCreateSession;            // 真正创建session}

View Code

      其中doCreateSession方法完成session的创建,doCreateSession方法大家可以自行去跟下,我在这总结一下:       

        创建session,并生成sessionId,session是shiro的SimpleSession类型,sessionId采用的是随机的UUID字符串;
        sessionDAO类型是MemorySessionDAO,session存放在sessionDAO的private
ConcurrentMap<Serializable, Session>
sessions;属性中,key是sessionId,value是session对象;
        除了MemorySessionDAO,shiro还提供了EnterpriseCacheSessionDAO,具体两者有啥区别请看我的另一篇博客讲解。

  shiro源码篇 – shiro的filter,你值得拥有

    SpringShiroFilter注册到spring容器,会被包装成FilterRegistrationBean,通过FilterRegistrationBean注册到servlet容器;SpringShiroFilter相当于是整个shiro的入口;

    SpringShiroFilter会创建ProxiedFilterChain,代理servlet
FilterChain,让请求先走shiro的filter链,让后再走servlet FilterChain。

参考

  《跟我学shiro》

  shiro源码

  shiro支持三种方式的授权

    1、编程式,通过写if/else授权代码块

Subject subject = SecurityUtils.getSubject();if(subject.hasRole("admin")) {    // 有权限,执行相关业务} else {    // 无权限,给相关提示}

    2、注解式,通过在执行的Java方法上放置相应的注解完成

@RequiresPermissions("sys:user:user")public List<User> listUser() {    // 有权限,获取数据  }

    3、JSP/GSP标签,在JSP/GSP页面通过相应的标签完成

<shiro:hasRole name="admin">    <!-- 有权限 --></shiro:hashRole>

    一般而言,编程式基本不用,注解方式比较普遍,标签方式用的不多;那么我们就来看看注解方式,它是如何实现权限控制的。一看到注解,我们就要想到aop,在目标对象的前后可以织入增强处理,具体我们往下看。

参考

  《跟我学shiro》

    ServletContainerSessionManager

      DefaultWebSecurityManager默认使用的SessionManager,用于Web环境,直接使用的Servlet容器的会话,具体实现我们往下看。

      ServletContainerSessionManager实现了SessionManager,并重写了SessionManager的start方法,那么我们从ServletContainerSessionManager的start方法开始来看看session的创建过程,如下图

图片 78

      shiro有自己的HttpServletSession,HttpServletSession持有servlet的HttpSession的引用,最终对HttpServletSession的操作都会委托给HttpSession(装饰模式)。那么此时的session是标准servlet容器支持的HttpSession实例,它不与Shiro的任何与会话相关的组件(如SessionManager,SecurityManager等)交互,完全由servlet容器管理。

  shiro源码篇 – shiro认证与授权,你值得拥有

    认证通过Filter实现,anon表示匿名访问,不需要认证,一般就是针对游客可以访问的资源,而authc则表示需要登录认证;

    我们所有的请求一般由shiro中3个Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分摊了,LogoutFilter负责/logout,AnonymousFilter负责/login和静态资源,FormAuthenticationFilter则负责剩下的;

    认证由FormAuthenticationFilter实现,未登录的请求会由它重定向到/login;认证过程是将界面输入的信息(UsernamePasswordToken)与缓存中的authenticationInfo进行匹对验证;认证信息不建议缓存;

    授权由注解方式,配合aop实现目标方法前的增强织入;认证过程是将缓存中的authorizationInfo与@RequiresPermissions中的xxx进行匹配校验;认证信息建议缓存起来。

  注解权限控制

    DefaultWebSessionManager

      用于Web环境,可以替换ServletContainerSessionManager,废弃了Servlet容器的会话管理;通过此可以实现我们自己的session管理;

      从SessionManager类图可知,DefaultWebSessionManager继承自DefaultSessionManager,也没有重写start方法,那么创建过程还是沿用的AbstractNativeSessionManager的start方法;如果我们没有指定自己的sessionDao,那么session还是存在MemorySessionDAO的ConcurrentMap<Serializable,
Session> sessions中,具体可以看上述中的DefaultSessionManager。

      通过DefaultWebSessionManager实现session共享,请点此处!

参考

  《跟我学shiro》

    authorizationInfo获取

图片 79

      执行目标方法前(也就是@RequiresPermissions修饰的方法),会先调用assertAuthorized(methodInvocation)进行权限的验证,分两步:先获取authorizationInfo,再进行权限的检查。上图展示了authorizationInfo,权限的检查请往下看。

      先从缓存中获取authorizationInfo,若没有则调用我们自定义Realm的doGetAuthorizationInfo方法来获取authorizationInfo(设置了roles与stringPermissions),并将其放入缓存中,然后返回authorizationInfo;若从缓存中获取到了authorizationInfo,则直接返回,而不需要通过Realm从数据库中获取了。一般情况下,权限缓存是开启的:myShiroRealm.setAuthorizationCachingEnabled;

总结

  SecurityManager和SessionManager的类图需要认真看看;

  Subject的所有交互都会委托给SecurityManager;SecurityManager是shiro的核心,它负责与shiro的其他组件进行交互,类似SpringMVC中的DispatcherServlet或Struts2中的FilterDispatcher;

  SecurityManager会将session管理委托给SessionManager;SessionsSecurityManager的start方法中将session的创建委托给了具体的sessionManager,是创建session的关键入口。
  shiro的SimpleSession与HttpServletSession
    HttpServletSession只是servlet容器的session的装饰,最终还是依赖servlet容器,是shiro对servlet容器的session的一种支持;
    而SimpleSession是shiro完完全全的自己实现,是shiro对session的一种拓展。但SimpleSession不对外暴露,我们一般操作的是SimpleSession的代理:DelegatingSession,或者是DelegatingSession的代理:StoppingAwareProxiedSession;对session的操作,会通过一层层代理,来到DelegatingSession,DelegatingSession将session的操作转交给sessionMananger,sessionManager通过一些校验后,最后转交给SimpleSession处理。

    权限检查

      当authorizationInfo获取到之后,进行来就是需要检查authorizationInfo中是否含有@RequiresPermissions中的xxx了,我们往下看

图片 80

      可以看到,检查过程过程就是将authorizationInfo中的Permission集合组个与xxx进行匹对,一旦匹对成功,则权限检查通过,流程往下走即执行目标方法(也就是我们的业务方法),如果一个都没匹对成功,则会抛出UnauthorizedException异常

    上述讲了Permission的方式进行权限的控制,通过Role控制的方式大同小异,有兴趣的朋友可以自己去跟一跟。当然还有其他的方式,但用的最多的是Permission和Role。

参考

  《跟我学shiro》

总结

  1、SpringShiroFilter作用就是生成shiro的代理filter链:ProxiedFilterChain,并将请求交给ProxiedFilterChain;

  2、anon:匿名访问,不需要认证,一般就是针对游客可以访问的资源;authc:登录认证;

  3、我们所有的请求一般由shiro中3个Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分摊了,LogoutFilter负责/logout,AnonymousFilter负责/login和静态资源,FormAuthenticationFilter则负责剩下的;

  4、未登录的请求会由FormAuthenticationFilter重定向/login,登录成功后会将authenticated设置成true,那么之后的请求会正常走剩下的servlet
filter链,最终来到我们的controller;登录认证过程会先从缓存获取authenticationInfo,没有则通过realm从数据库获取并放入缓存,然后将页面输入的用户信息UsernamePasswordToken与authenticationInfo进行匹配验证。个人不建议开启认证缓存,当修改用户信息后刷新缓存中的认证信息,不好处理,另外认证频率本来就不高,缓存的意义不大;

  5、授权一般采用注解方式,注解往往配合aop来实现目标方法前后的增强织入,shiro的权限注解就是在目标方法前的增强处理。校验过程与认证过程类似,先从缓存中获取authorizationInfo,没有则通过realm从数据库获取,然后放入缓存,看authorizationInfo中是否有@RequiresPermissions中的xxx来完成权限的验证。个人建议开启权限缓存,权限的验证还是挺多的,如果不开启缓存,那么会给数据库造成一定的压力;

  留个疑问,有兴趣的朋友可以去查看下源码:假如session过期后,我们再请求,shiro是如何处理并跳转到登录页的

参考

  《跟我学shiro》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图