分享 Shiro 学习过程中遇到的一些问题
最近在学习 shiro 安全框架后,自己手写了一个小的管理系统 web 项目,并使用 shiro 作为安全管理框架。接下来分享一下在这过程中,遇到的一些问题以及自己的解决思路和方法。
一、log out 之后再次登录,出现 403 forbidden
这个问题不一定所有朋友都会碰到,出现的原因是我的 webapp 根目录下没有 index 页面(我的 index 页面放在 /web-inf/view/ 下),先看 shirofilterfactorybean的配置代码。
1 <!--配置 shiro 框架的过滤器--> 2 <bean id="shirofilter" class="org.apache.shiro.spring.web.shirofilterfactorybean"> 3 <!--注入安全管理器--> 4 <property name="securitymanager" ref="securitymanager"/> 5 <!--默认的认证成功后跳转的页面--> 6 <property name="successurl" value="/index"/> 7 <!--认证失败、登录访问的页面--> 8 <property name="loginurl" value="/login"/> 9 <!--没有权限访问时跳转的页面--> 10 <property name="unauthorizedurl" value="/unauthorized"/> 11 12 <!--注入自定义 filter--> 13 <property name="filters"> 14 <map> 15 <entry key="authc" value-ref="myformauthenticationfilter"/> 16 </map> 17 </property> 18 19 <!--配置过滤器链--> 20 <property name="filterchaindefinitions"> 21 <value> 22 <!--静态资源不需要验证,放行--> 23 /lib/** = anon 24 /static/** = anon 25 /verifycode.jsp = anon 26 /checkverifycode = anon 27 28 <!--退出登录--> 29 /logout = logout 30 31 <!--其它所有页面都需要验证访问--> 32 /** = authc 33 </value> 34 </property> 35 </bean>
以上的配置是没有问题的,但是当你的 webapp 下没有 index 页面(或者没有配置 web.xml 的 <welcome-file-list>),就会出现标题所描述的问题。
我们先来分析一下各项配置的具体含义,只针对几个容易出现错误的配置项。
successurl
这是一个容易令人引起误解的配置,让人以为登录成功后就一定会跳转到这个页面。实际上在 shiro 的底层,这是一个验证成功后默认的跳转页面,但是 shiro 底层会记录你的上次访问页面,当你登陆成功后会跳转到上次访问被拒绝的页面。
举个例子:当你打开浏览器,访问一个需要授权的页面(/** = authc),例如“user/list”页面,此时会因为没有授权,而跳转到配置中的“loginurl”也就是登录界面,在你登录成功后,则会跳转到“user/list”页面,而不是“successurl”。
那 successurl 什么时候生效呢?当我们直接访问的就是“/login”页面时,登录成功后就会跳转到这个默认的验证成功的 “successurl”页面。
loginurl
这个配置的值为当用户访问需要授权的页面时,shiro 判断没有授权时跳转的页面。需要注意的是,在我们设计登录页面时,登录的表单提交的地址,也要和这个地址一样。
例如当我们访问“/login”控制器进入登陆页面,点击登录后,表单提交到的地址也应该是“/login”,否则登录不成功,继续跳转到登陆页面。我的猜测这种情况是因为,只有当表单提交的地址和 loginurl 的地址相同时,请求才会走 formauthenticationfilter 过滤器进行登录验证。
当我们登陆失败时,会继续跳转到 loginurl 这个页面。基于这种情况,我们可以在“/login”的控制器上同时获得登陆失败的异常,这个异常被 shiro 封装在 request 的属性中,key 为 “shirologinfailure”。接着可以根据异常信息,返回错误提示给前端显示。
@requestmapping("/login") public string login(httpservletrequest request, model m) { //获取认证失败的错误信息,在shiro框架的 formauthenticationfilter 过滤器*享 // 共享的属性名称 shirologinfailure , 通过 request 获取 // 共享的 shiro 异常的字节码 string shirologininfailure = string.valueof(request.getattribute("shirologinfailure")); if (unknownaccountexception.class.getname().equals(shirologininfailure)) { m.addattribute("errormsg", "账户不存在"); } else if (incorrectcredentialsexception.class.getname().equals(shirologininfailure)) { m.addattribute("errormsg", "密码错误"); } system.out.println("异常类型:" + shirologininfailure); return "login"; }
unauthorizedurl
当我们给 filterchaindefinitions 添加了权限管理时,没有权限访问这个页面,就会跳入 unauthorizedurl。需要注意的是,如果我们用注解的方式添加权限管理,不会走这个页面,这个页面只对 filterchaindefinitions 内的配置有效
/logout = logout
当我们访问这个地址时,就会经过 logoutfilter ,这个过滤器会将我们当前用户退出,源码如下图。
同时,这个过滤器会重定向到“/”这个路径,这就是我们题目所述问题的根源。
接下来的过程就是:
- 我们登出之后重定向到“/”,“/”符合 “/** = authc”这个配置,需要验证才能访问;
- 然后我们进入了 loginurl ,进行登录;
- 登录验证成功后,会跳转到上次访问失败的页面,也就是“/”地址;
- 当我们访问根路径时,tomcat 会默认调用 index.html 等类似的静态资源,或者根据 web.xml 中配置的 <welcome-file-list> 进行访问,若这些都没有资源进行访问,就会报上述的 403 forbidden 错误。
解决方案
我们有两种解决方案
第一种,将页面路径加入 <welcome-file-list>,如
1 <welcome-file-list> 2 <welcome-file>/web-inf/view/index.jsp</welcome-file> 3 </welcome-file-list>
第二种,我们修改负责进入 index 页面的控制器的 requestmapping,如
@requestmapping(value = {"/index", ""}) public string index() { return "index"; }
二、登录之后,不 logout,手动进入登陆界面再次登录
按照标题的方式操作时,出现的现象是,当前 subject 的 principal 没有变更,同时我们继续跳转至登录界面,可以说很不符合客户体验的需求。
出现这个现象的原因是:首先,当我们访问“/login”时,表单提交的地址也是“/login”,所以很正常我们继续停留在了此页面;另外,每次我们访问满足“/** = authc”的页面时,authenticationfilter 会先进行 isallowedaccess 方法的判断,我们登录过后,这个方法会返回 true,我们就可以直接进入页面,不走验证流程。
于是我们可以新建一个类继承 formauthenticationfilter,并重写其 isallowedaccess 方法,在判断请求时指向登陆页面,并有表单提交时,如果当前有用户通过验证了,将当前用户 log out,再继续进行父类的验证。
subject.logout 会同时清空 session,所以我们登录成功后进入的是 successurl 页面。完美的用户体验
public class myformauthenticationfilter extends formauthenticationfilter { /** * 重写 isaccessallowed 方法,解决重复登录的问题 */ @override protected boolean isaccessallowed(servletrequest request, servletresponse response, object mappedvalue) { if (isloginrequest(request, response)) { if (isloginsubmission(request, response)) { subject subject = this.getsubject(request, response); //判断当前是否已经登录 if (subject.getprincipal() != null) { system.out.println("log out:" + subject.getprincipal()); //登出用户 subject.logout(); } } } return super.isaccessallowed(request, response, mappedvalue); } }
这还没有完,我们需要在 shirofilterfactorybean 的配置中,将我们的 filter 加入进去,替代 authc 进行验证
<bean id="shirofilter" class="org.apache.shiro.spring.web.shirofilterfactorybean"> <!-- 其他配置略 --> <property name="filters"> <map> <entry key="authc" value-ref="myformauthenticationfilter"/> </map> </property> </bean>
三、注解模式下的权限配置,无法进入 unauthorizedurl
注解模式下,无权访问的异常类型和在配置文件下的权限配置的不同,所以需要我们用 spring 手动捕捉,并跳转到需要显示的异常页面。需要注意的是,跳转地址受视图解析器影响。
<!-- 开启 spring 的异常拦截捕获权限异常 --> <bean class="org.springframework.web.servlet.handler.simplemappingexceptionresolver"> <property name="exceptionmappings"> <props> <prop key="org.apache.shiro.authz.authorizationexception">unauthorized</prop> </props> </property> </bean>
key 就是异常全限定名,该配置跳转的地址是:/web-inf/view/unauthorized.jsp
四、开启 rememberme 后,没有效果
rememberme 要求 principal 对象是能够序列化的,也就是 实现 serializable 接口。按照要求我把作为 principal 的 user 类实现了 serializable 接口,但是依然失败,在测试记住我功能的时候,浏览器一直没有获得 cookie。
出现这个问题的原因是,我的 user 类下,属性中还有一个 其他类的对象,该对象没有实现 serializable 接口,所以导致了序列化失败。解决办法也很简单,就是让它也实现序列化接口。
不一定出现该问题都是这个原因,只是提醒大家这个点不要忽视了