shiro概述
shiro是一个鉴权框架,鉴(Authentication)是指用户登录验证,权(Authorization)是指用户权限控制。
Subject
:主体,其实就是用户
Principal
:标识,理解为用户名即可
Crendential
:凭证,理解为密码
Session
:服务器端保存的用户信息,需要配合客户端cookie或者token。建议采用token(jwt),因为token更通用,而cookie只适用于PC网页。通过subject.getSession(create)可以得到session。
Role
、Permission
:角色和权限,一般都是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权限,但是不能处理数据权限,如一条记录是否能看,里面的字段是否能看,这个没啥好方法,要么硬编码要么自己抽规则了。