Shiro使用

shiro概述

shiro是一个鉴权框架,鉴(Authentication)是指用户登录验证,权(Authorization)是指用户权限控制。
Subject:主体,其实就是用户
Principal:标识,理解为用户名即可
Crendential:凭证,理解为密码
Session:服务器端保存的用户信息,需要配合客户端cookie或者token。建议采用token(jwt),因为token更通用,而cookie只适用于PC网页。通过subject.getSession(create)可以得到session。
RolePermission:角色和权限,一般都是RBAC(role based access control),一个subject拥有多个角色,每个角色拥有多个权限。
ShiroFilter:filter,拦截所有url请求;在web.xml里配置DelegatingFilterProxy,因为web.xml是容器第一步加载的,而spring ContextLoadListener也要配置在web.xml里,但是呢shiro的很多配置都是要通过spring完成,这样就形成了延迟加载和依赖倒置(这里和依赖倒置原则里的依赖倒置不是一个意思),所以就要通过DelegatingFilterProxy了。
SecurityManager:处理登录、登出,Subject.login/logout就是委托给SecurityManager处理的,登录前后、登出前后,登录成功、失败也都在这里处理,你可以继承SecurityManager,然后加上自己的业务逻辑。这里要注意,在登录相关的业务代码中,如果抛异常请抛AuthenticationException及其子类。
SessionManager:保存session及从request中根据sessionId获取session,一般session保存在分布式缓存redis中。在SecurityUtils.getSubject()就会调用SessionManager中获取session。
SessionKey:其实就是一个字符串,看你的sessionId怎么生成的,
FilterChainResolver:根据请求url和shiro.xml中配置的filterChainDefinition来解析出一个url被哪些shiro filter拦截,需要和FilterChainManager结合使用。我们知道servlet中所有请求都被会filter拦截,多个filter之间会组成filterChain,FilterChainResolver会对url解析出一个新的filterChain,这个新的filterChain在原先filterChain前面添加了shiro自己的filter,这样先执行shiro的filter,执行失败就抛出异常。
Realm:这个单词意思是领域,初次可能不太知道是什么意思,其实就是shiro提供验证信息和权限信息的地方。一般系统会提供多种登录方式,如用户名+密码,手机号+验证码和第三方集成登录。
用户名+密码:这种方式在企业内部常用,在互联网领域没落了,除非你是大厂。用户名和密码存储在数据库,密码是经盐(salt)加密。
手机号+验证码:这个在互联网领域很常见,因为现在应用太多了,普通人很难记住这么多密码,甚至会一套密码走天下。验证码看情况存储在数据库或缓存中都可以。
第三方集成:一般会选择一些大厂进行集成,因为用户肯定在这些大厂有账号啊,如QQ、微信、支付宝、微博等。需要和第三方做集成,一般采用oauth2授权方式,分2步走,第一步跳到微信(以微信为例)授权页面,需要用户手动点击同意授权,然后返回一个code,后台需要拿这个code换取openId,后面就拿openId去访问用户信息了(如昵称、头像等基本资料,重要资料你也拿不到的)。一般还要在本系统里创建一个新用户,关联上openId。有些网站还会让你新建用户名关联手机号啥的,其实这和注册一个用户差不多,个人比较反感,因为我已经授权微信登录了。可以先在后台创建一个默认账户,至于手机号在需要交易等需要的时候再绑定,这样可以提高用户转化率、留存率。不要在一开始就吓跑用户。
注意上面不管采用哪种登录方式,最后都要转化为本系统上的一个用户才行。

上面说了这么多,回到Realm上来,Realm就是提供验证信息和权限信息的来源,因此有AuthenticationRealm和AuthorizingRealm 2种,其实AuthorizingRealm已经继承了AuthenticationRealm,因此你的Realm继承AuthorizingRealm,实现getAuthenticationInfo()和getAuthorizationInfo()即可。
AuthenticationStrategy:多个Realm时是全部Realm都通过还是只需要一个,默认是只需要一个通过即可,没遇到所有Realm都需要通过的场景。
Authenticator:系统中根据不同的登录方式会提供不同的Realm,Authenticator就是用来决定采用哪个Realm进行鉴定。
Authorizer:用来判定用户是否拥有权限,也是通过Realm来获取权限信息。
Cache:用户验证信息和权限信息一般放在缓存里
CacheManager:不同类型信息放在不同的缓存里,如验证信息、权限信息、用户密码尝试次数等都可以放在不同的cache中。
CredentialsMatcher:凭证比对器,就是用户给的凭证和系统中的凭证进行比对,通过了就认为是合法用户,通不过就报密码错误之类的信息。是依赖Realm提供的getAuthenticationInfo()。不同的登录方式匹配规则也不同,如用户名+密码方式要匹配密码,手机号+验证码就要匹配验证码。
AuthorizationAttributeSourceAdvisor:看名字就知道这是一个Spring Advisor,用于拦截shiro的几个角色、权限注解,如RequiresRoles,RequiresPermissions。有时间写一篇Spring AOP(Pointcut,Advice,Advisor)文章。
MethodInvokingFactoryBean:在shiro配置中会看到这个东西,其实这个东西就是一个反射调用对象(类)方法,因为shiro有的地方并不符合javabean规范,一些属性不提供setter/getter,这里就可以通过MethodInvokingFactoryBean来设置。当然你也可以自己写个bean,在你的bean里调用反射方法,其实一样的。 MethodInvokingFactoryBean只是一个工具类。
LifecycleBeanPostProcessor:就是一个BPP,在shiro bean生成和销毁的时候做初始化、销毁操作。
AccessControlFilter:这里有个很重要的方法:

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

这个方法返回true表示继续执行后面的filterChain,返回false就不再执行后续filterChain。
isAccessAllowed是判断当前用户是否允许通过,如果当前用户是登录状态肯定返回true,如果不是登录状态就要执行后面的onAccessDenied(),在这个方法里如果没有登录就要跳转登录了,shiro的FormAuthenticationFilter就是实现了onAccessDenied()来实现登录的,一般判断url=loginUrl并且是post请求就认为是登录请求。

注意
有的人会在shiro.xml里面配置DefaultAdvisorAutoProxyCreator,其实这是不必要的,这样会造成二次代理问题,基本在项目中不要手动配置DefaultAdvisorAutoProxyCreator,以免和spring中其他AOP如事务产生二次代理问题。

注意
shiroFilter依赖了securityManager, securityManager依赖了Realm,如果你的Realm因为要获取用户和角色导致Realm依赖了UserService之类的,会造成UserService配置的事务AOP无效,具体原因和解决方法见:https://www.jianshu.com/p/b1209cd3686d

注意
我们在任何地方都能通过SecurityUtils.getSubject()获取当前用户对象,其实用到的就是ThreadLocal,shiro中的ThreadContext就是专门处理Subject和ThreadLocal绑定、解绑的。

注意
Subject.getPrincipal()返回是当前使用的用户名,getPreviousPrincipal()返回以前的用户名,是个栈结构,可以一直取。
当用户通过手机号或者微信登录系统时,principal一开始是手机号和微信的code,登录成功后principal肯定要转成系统里的用户名,因为后面getAuthorizationInfo()和其他地方都用取subject.getPrincipal()来获取当前用户标识。
Subject.runAs()、releaseRunAs()就是处理这种场景,一个人以另一个身份访问资源。

注意
手机号+验证码登录方式,如果开启了authenticationInfo cache,用户获取了验证码,然后authenticationInfo被缓存了,故意验证失败,然后再次请求验证码,再次尝试登录,会因为cache中的authenticationInfo没清空导致永远登录不成功。可以禁用authenticationInfo cache。

注意
还有一种场景是在线程池中获取当前用户信息,如在业务中你往线程池中提交了一个异步任务,在任务代码中要访问当时用户信息,肯定不能把subject对象当做参数传入,我们还要通过SecurityUtils.getSubject()获得当时提交任务的用户,而线程池中的线程是没有Subject的。其实Subject提供了方法:associateWith(Runnable),但是这种有个问题,即用户在提交任务后立马登出,这样ThreadLocal中的Subject里面数据肯定就没有了,应该复制出来一份Subject的,目前还没碰到这样的问题,待验证。

登录过程中Subject变化
在登录成功之前,代码里可能就已经通过SecurityUtils.getSubject()获取到当前线程绑定的Subject了,此时subject.authenticated=false,在登录成功后会返回一个authenticated=true的subject,后返回的subject里包含了前面subject里内容。

 public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                if (log.isInfoEnabled()) {
                    log.info("onFailedLogin method threw an " +
                            "exception.  Logging and propagating original AuthenticationException.", e);
                }
            }
            throw ae; //propagate
        }

        Subject loggedIn = createSubject(token, info, subject);

        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

shiro工作流程

当用户访问一个url时,会被shiroFilter拦截,根据filterChainResolver会匹配对应的拦截器filter(注意只匹配第一个,不会匹配最优的),所以需要登录的路径放在filterChainDefinition(LinkedHashMap)最上面,匿名访问url放最下面。
如果没有登录,就会跳到登录页面,登录成功后跳转successUrl或者SavedRequest页面。
如果登录了就放行。
权限的话可以用注解,这样方便但是零散,每个方法上都要写,重复劳动,但是灵活。
如果url有规则的话,如/user/create,, order/update这种的话可以写个filter处理。
shiro只能处理动作权限,即是否有访问url权限,但是不能处理数据权限,如一条记录是否能看,里面的字段是否能看,这个没啥好方法,要么硬编码要么自己抽规则了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容