shiro源码篇 - shiro认证与授权,你值得拥有
前言
开心一刻
我和儿子有个共同的心愿,出国旅游。昨天儿子考试得了全班第一,我跟媳妇合计着带他出国见见世面,吃晚饭的时候,一家人开始了讨论这个。我:“儿子,你的心愿是什么?”,儿子:“吃汉堡包”,我:“往大了说”,儿子:“变形金刚”,我:“今天你爹说了算,想想咱俩共同的心愿”,儿子怯生生的瞅了媳妇一眼说:“换个妈?",我心里咯噔一下:“这虎犊子,坑自己也就算了,怎么还坑爹呢”。
牛:小子,你的杀气太重,我早就看穿一切,吃我一脚!
路漫漫其修远兮,吾将上下而求索!
github:
码云(gitee):
前情回顾与补充
回顾
在中,我们讲到了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的dispatcher),先生成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; }
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); } } }
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链不继续执行了 } }
最终会重定向到/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(), "用户名或密码错误"); } }
登录认证过程委托给了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"; }
将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》
上一篇: laravel使用Schema创建数据表
下一篇: Linux系统巧用NMAP来收集主机信息