zoukankan      html  css  js  c++  java
  • shiro源码篇

    前言

      开心一刻 

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

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

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

      github:https://github.com/youzhibing

      码云(gitee):https://gitee.com/youzhibing

    前情回顾与补充

      回顾

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

        如果请求经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。

      补充

        shiro的Filter关系图

    shiro filter关系图

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

        ShiroFilterFactoryBean中的createFilterChainManager()

    protected FilterChainManager createFilterChainManager() {
    
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        Map<String, Filter> defaultFilters = manager.getFilters();
        //apply global settings if necessary:    应用全局设置
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }
    
        //Apply the acquired and/or configured filters: 应用和配置filter,一般没有
        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);
            }
        }
    
        //build up the chains:  构建shiro 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;
    }
    View Code

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

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

    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链

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

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

    认证

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

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

      未登录

        url请求:http://localhost:8881/

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

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

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

    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 没搞明白,可能应对一些特殊的filter
    
    
    protected 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变成了:http://localhost:8881/login。此时的ProxiedFilterChain如下所示

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

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

        将登录页返回回去

      登录

        url请求:http://localhost:8881/login,请求方式是post

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

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

    @PostMapping("/login")
    @ResponseBody
    public OwnResult dologin(String username, String password) {
        username = username.trim();
        // 判断当前用户是否可用
        User user = userService.findUserByUsername(username);
        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(token);        // 登录认证交给shiro
            return OwnResult.ok();
        } catch (AuthenticationException e) {
            return OwnResult.build(RespCode.ERROR_USERNAME_PASSWORD.getCode(), "用户名或密码错误");
        }
    }
    View Code

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

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

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

      登录成功

        登录成功后,我们往往会请求主页,url请求:http://localhost:8881/index

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

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

    @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返回回去

    授权

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

      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(动态代理),在目标对象的前后可以织入增强处理,具体我们往下看。

      注解权限控制

        authorizationInfo获取

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

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

        权限检查

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

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

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

    总结

      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")中的xxx来完成权限的验证。个人建议开启权限缓存,权限的验证还是挺多的,如果不开启缓存,那么会给数据库造成一定的压力;

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

    参考

      《跟我学shiro》

  • 相关阅读:
    一个老程序员的从零开始的从新出发
    Django静态文件配置
    Data truncated for column 'id' at row 1
    MyBatis之java.lang.UnsupportedOperationException异常解决方案
    mybatis批量查询-插入-删除
    Mybatis中进行批量更新(updateBatch)
    mybatis插入insert操作,返回自增id
    redis五大数据类型
    redis中key键操作
    Redis主从复制失败(master_link_status down)
  • 原文地址:https://www.cnblogs.com/youzhibing/p/10122606.html
Copyright © 2011-2022 走看看