前言
本文主要講解的知識(shí)點(diǎn)有以下:
- Shiro授權(quán)過(guò)濾器使用
- Shiro緩存
- 與Ehcache整合
- Shiro應(yīng)用->實(shí)現(xiàn)驗(yàn)證碼功能
- 記住我功能
一、授權(quán)過(guò)濾器測(cè)試
我們的授權(quán)過(guò)濾器使用的是permissionsAuthorizationFilter來(lái)進(jìn)行攔截。我們可以在application-shiro中配置filter規(guī)則
<!--商品查詢需要商品查詢權(quán)限 -->
/items/queryItems.action = perms[item:query]
/items/editItems.action = perms[item:edit]
測(cè)試流程:
1、在applicationContext-shiro.xml中配置filter規(guī)則
- ``
- /items/queryItems.action = perms[item:query]
2、用戶在認(rèn)證通過(guò)后,請(qǐng)求/items/queryItems.action
3、被PermissionsAuthorizationFilter攔截,發(fā)現(xiàn)需要“item:query”權(quán)限
4、PermissionsAuthorizationFilter 調(diào)用realm中的doGetAuthorizationInfo獲取數(shù)據(jù)庫(kù)中正確的權(quán)限
5、PermissionsAuthorizationFilter對(duì)item:query 和從realm中獲取權(quán)限進(jìn)行對(duì)比,如果“item:query”在realm返回的權(quán)限列表中,授權(quán)通過(guò)。
realm中獲取認(rèn)證的信息,查詢出該用戶對(duì)應(yīng)的權(quán)限,封裝到simpleAuthorizationInfo中,PermissionsAuthorizationFilter會(huì)根據(jù)對(duì)應(yīng)的權(quán)限來(lái)比對(duì)。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//從 principals獲取主身份信息
//將getPrimaryPrincipal方法返回值轉(zhuǎn)為真實(shí)身份類(lèi)型(在上邊的doGetAuthenticationInfo認(rèn)證通過(guò)填充到SimpleAuthenticationInfo中身份類(lèi)型),
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
//根據(jù)身份信息獲取權(quán)限信息
//從數(shù)據(jù)庫(kù)獲取到權(quán)限數(shù)據(jù)
List<SysPermission> permissionList = null;
try {
permissionList = sysService.findPermissionListByUserId(activeUser.getUserid());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//單獨(dú)定一個(gè)集合對(duì)象
List<String> permissions = new ArrayList<String>();
if(permissionList!=null){
for(SysPermission sysPermission:permissionList){
//將數(shù)據(jù)庫(kù)中的權(quán)限標(biāo)簽 符放入集合
permissions.add(sysPermission.getPercode());
}
}
/* List<String> permissions = new ArrayList<String>();
permissions.add("user:create");//用戶的創(chuàng)建
permissions.add("item:query");//商品查詢權(quán)限
permissions.add("item:add");//商品添加權(quán)限
permissions.add("item:edit");//商品修改權(quán)限
*/ //....
//查到權(quán)限數(shù)據(jù),返回授權(quán)信息(要包括 上邊的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//將上邊查詢到授權(quán)信息填充到simpleAuthorizationInfo對(duì)象中
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
在bean中我們已經(jīng)配置了:如果沒(méi)有權(quán)限,那么跳轉(zhuǎn)到哪個(gè)JSP頁(yè)面了
<!-- 通過(guò)unauthorizedUrl指定沒(méi)有權(quán)限操作時(shí)跳轉(zhuǎn)頁(yè)面-->
<property name="unauthorizedUrl" value="/refuse.jsp" />
到目前為止,現(xiàn)在問(wèn)題又來(lái)了:
1、在applicationContext-shiro.xml中配置過(guò)慮器鏈接,需要將全部的url和權(quán)限對(duì)應(yīng)起來(lái)進(jìn)行配置,比較發(fā)麻不方便使用。
2、每次授權(quán)都需要調(diào)用realm查詢數(shù)據(jù)庫(kù),對(duì)于系統(tǒng)性能有很大影響,可以通過(guò)shiro緩存來(lái)解決。
二、使用注解式和標(biāo)簽式配置授權(quán)
上面的那種方法,還是需要我們將全部的url和權(quán)限對(duì)應(yīng)起來(lái)進(jìn)行配置,是比較不方便的。我們可以使用授權(quán)的另外兩種方式
- 注解式
- 標(biāo)簽式
2.1注解式
如果要使用注解式,那么就必須在Spring中開(kāi)啟controller類(lèi)aop支持
<!-- 開(kāi)啟aop,對(duì)類(lèi)代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 開(kāi)啟shiro注解支持 -->
<bean
class="
org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
在Controller中使用注解來(lái)進(jìn)行配置就行了,就不用在我們的application-shiro中全部集中配置了
//商品信息方法
@RequestMapping("/queryItems")
@RequiresPermissions("item:query")//執(zhí)行queryItems需要"item:query"權(quán)限
public ModelAndView queryItems(HttpServletRequest request) throws Exception {
System.out.println(request.getParameter("id"));
//調(diào)用service查詢商品列表
List<ItemsCustom> itemsList = itemsService.findItemsList(null);
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("itemsList", itemsList);
// 指定邏輯視圖名
modelAndView.setViewName("itemsList");
return modelAndView;
}
2.2jsp標(biāo)簽 授權(quán)
當(dāng)調(diào)用controller的一個(gè)方法,由于該 方法加了@RequiresPermissions("item:query") ,shiro調(diào)用realm獲取數(shù)據(jù)庫(kù)中的權(quán)限信息,看"item:query"是否在權(quán)限數(shù)據(jù)中存在,如果不存在就拒絕訪問(wèn),如果存在就授權(quán)通過(guò)。
當(dāng)展示一個(gè)jsp頁(yè)面時(shí),頁(yè)面中如果遇到
三、Shiro緩存
針對(duì)上邊授權(quán)頻繁查詢數(shù)據(jù)庫(kù),需要使用shiro緩存
3.1緩存流程
shiro中提供了對(duì)認(rèn)證信息和授權(quán)信息的緩存。shiro默認(rèn)是關(guān)閉認(rèn)證信息緩存的,對(duì)于授權(quán)信息的緩存shiro默認(rèn)開(kāi)啟的。主要研究授權(quán)信息緩存,因?yàn)?strong>授權(quán)的數(shù)據(jù)量大。
用戶認(rèn)證通過(guò)。
該用戶第一次授權(quán):調(diào)用realm查詢數(shù)據(jù)庫(kù)
該用戶第二次授權(quán):不調(diào)用realm查詢數(shù)據(jù)庫(kù),直接從緩存中取出授權(quán)信息(權(quán)限標(biāo)識(shí)符)。
3.2使用ehcache和Shiro整合
導(dǎo)入jar包
配置緩存管理器,注入到安全管理器中
<!-- 緩存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
<!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
<!-- 注入緩存管理器 -->
<property name="cacheManager" ref="cacheManager"/>
</bean>
ehcache的配置文件shiro-ehcache.xml:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--diskStore:緩存數(shù)據(jù)持久化的目錄 地址 -->
<diskStore path="F:\develop\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
3.3緩存清空
如果用戶正常退出,緩存自動(dòng)清空。
如果用戶非正常退出,緩存自動(dòng)清空。
還有一種情況:
- 當(dāng)管理員修改了用戶的權(quán)限,但是該用戶還沒(méi)有退出,在默認(rèn)情況下,修改的權(quán)限無(wú)法立即生效。需要手動(dòng)進(jìn)行編程實(shí)現(xiàn):在權(quán)限修改后調(diào)用realm的clearCache方法清除緩存。
清除緩存:
//清除緩存
public void clearCached() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
3.4sessionManager
和shiro整合后,使用shiro的session管理,shiro提供sessionDao操作 會(huì)話數(shù)據(jù)。
配置sessionManager
<!-- 會(huì)話管理器 -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session的失效時(shí)長(zhǎng),單位毫秒 -->
<property name="globalSessionTimeout" value="600000"/>
<!-- 刪除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
</bean>
注入到安全管理器中
<!-- securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
<!-- 注入緩存管理器 -->
<property name="cacheManager" ref="cacheManager"/>
<!-- 注入session管理器 -->
<property name="sessionManager" ref="sessionManager" />
</bean>
四、驗(yàn)證碼
在登陸的時(shí)候,我們一般都設(shè)置有驗(yàn)證碼,但是我們?nèi)绻褂肧hiro的話,那么Shiro默認(rèn)的是使用FormAuthenticationFilter進(jìn)行表單認(rèn)證。
而我們的驗(yàn)證校驗(yàn)的功能應(yīng)該加在FormAuthenticationFilter中,在認(rèn)證之前進(jìn)行驗(yàn)證碼校驗(yàn)。
FormAuthenticationFilter是Shiro默認(rèn)的功能,我們想要在FormAuthenticationFilter之前進(jìn)行驗(yàn)證碼校驗(yàn),就需要繼承FormAuthenticationFilter類(lèi),改寫(xiě)它的認(rèn)證方法!
4.1自定義Form認(rèn)證類(lèi)
public class CustomFormAuthenticationFilter extends FormAuthenticationFilter {
//原FormAuthenticationFilter的認(rèn)證方法
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
//在這里進(jìn)行驗(yàn)證碼的校驗(yàn)
//從session獲取正確驗(yàn)證碼
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpSession session =httpServletRequest.getSession();
//取出session的驗(yàn)證碼(正確的驗(yàn)證碼)
String validateCode = (String) session.getAttribute("validateCode");
//取出頁(yè)面的驗(yàn)證碼
//輸入的驗(yàn)證和session中的驗(yàn)證進(jìn)行對(duì)比
String randomcode = httpServletRequest.getParameter("randomcode");
if(randomcode!=null && validateCode!=null && !randomcode.equals(validateCode)){
//如果校驗(yàn)失敗,將驗(yàn)證碼錯(cuò)誤失敗信息,通過(guò)shiroLoginFailure設(shè)置到request中
httpServletRequest.setAttribute("shiroLoginFailure", "randomCodeError");
//拒絕訪問(wèn),不再校驗(yàn)賬號(hào)和密碼
return true;
}
return super.onAccessDenied(request, response);
}
}
4.2配置自定義類(lèi)
我們編寫(xiě)完自定義類(lèi)以后,是需要在Shiro配置文件中配置我們這個(gè)自定義類(lèi)的。
由于這是我們自定義的,因此我們并不需要用戶名就使用username,密碼就使用password,這個(gè)也是我們可以自定義的。
<!-- 自定義form認(rèn)證過(guò)慮器 -->
<!-- 基于Form表單的身份驗(yàn)證過(guò)濾器,不配置將也會(huì)注冊(cè)此過(guò)慮器,表單中的用戶賬號(hào)、密碼及l(fā)oginurl將采用默認(rèn)值,建議配置 -->
<bean id="formAuthenticationFilter"
class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter ">
<!-- 表單中賬號(hào)的input名稱 -->
<property name="usernameParam" value="username" />
<!-- 表單中密碼的input名稱 -->
<property name="passwordParam" value="password" />
</bean>
在Shiro的bean中注入自定義的過(guò)濾器
<!-- 自定義filter配置 -->
<property name="filters">
<map>
<!-- 將自定義 的FormAuthenticationFilter注入shiroFilter中-->
<entry key="authc" value-ref="formAuthenticationFilter" />
</map>
</property>
在我們的Controller添加驗(yàn)證碼錯(cuò)誤的異常判斷,從我們的Controller就可以發(fā)現(xiàn),為什么我們要把錯(cuò)誤信息存放在request域?qū)ο髎hiroLoginFailure,因?yàn)槲覀兊迷贑ontroller中獲取獲取信息,從而給用戶對(duì)應(yīng)的提示
@RequestMapping("login")
public String login(HttpServletRequest request)throws Exception{
//如果登陸失敗從request中獲取認(rèn)證異常信息,shiroLoginFailure就是shiro異常類(lèi)的全限定名
String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
//根據(jù)shiro返回的異常類(lèi)路徑判斷,拋出指定異常信息
if(exceptionClassName!=null){
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
//最終會(huì)拋給異常處理器
throw new CustomException("賬號(hào)不存在");
} else if (IncorrectCredentialsException.class.getName().equals(
exceptionClassName)) {
throw new CustomException("用戶名/密碼錯(cuò)誤");
} else if("randomCodeError".equals(exceptionClassName)){
throw new CustomException("驗(yàn)證碼錯(cuò)誤 ");
}else {
throw new Exception();//最終在異常處理器生成未知錯(cuò)誤
}
}
//此方法不處理登陸成功(認(rèn)證成功),shiro認(rèn)證成功會(huì)自動(dòng)跳轉(zhuǎn)到上一個(gè)請(qǐng)求路徑
//登陸失敗還到login頁(yè)面
return "login";
}
<TR>
<TD>驗(yàn)證碼:</TD>
<TD><input id="randomcode" name="randomcode" size="8" /> <img
id="randomcode_img" src="${baseurl}validatecode.jsp" alt=""
width="56" height="20" align='absMiddle' /> <a
href=javascript:randomcode_refresh()>刷新</a></TD>
</TR>
五、記住我
Shiro還提供了記住用戶名和密碼的功能!
用戶登陸選擇“自動(dòng)登陸”本次登陸成功會(huì)向cookie寫(xiě)身份信息,下次登陸從cookie中取出身份信息實(shí)現(xiàn)自動(dòng)登陸。
想要實(shí)現(xiàn)這個(gè)功能,我們的認(rèn)證信息需要實(shí)現(xiàn)Serializable接口。
public class ActiveUser implements java.io.Serializable {
private String userid;//用戶id(主鍵)
private String usercode;// 用戶賬號(hào)
private String username;// 用戶名稱
private List<SysPermission> menus;// 菜單
private List<SysPermission> permissions;// 權(quán)限
}
5.1配置rememeber管理器
<!-- rememberMeManager管理器,寫(xiě)cookie,取出cookie生成用戶信息 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie" />
</bean>
<!-- 記住我cookie -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- rememberMe是cookie的名字 -->
<constructor-arg value="rememberMe" />
<!-- 記住我cookie生效時(shí)間30天 -->
<property name="maxAge" value="2592000" />
</bean>
注入到安全管理器類(lèi)上
<!-- securityManager安全管理器 -->
<bean id="securityManager"~~~····
<property name="cacheManager" ref="cacheManager"/>
<!-- 注入session管理器 -->
<property name="sessionManager" ref="sessionManager" />
<!-- 記住我 -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
配置頁(yè)面的input名稱:
<tr>
<TD></TD>
<td><input type="checkbox" name="rememberMe" />自動(dòng)登陸</td>
</tr>
如果設(shè)置了“記住我”,那么訪問(wèn)某些URL的時(shí)候,我們就不需要登陸了。將記住我即可訪問(wèn)的地址配置讓UserFilter攔截。
<!-- 配置記住我或認(rèn)證通過(guò)可以訪問(wèn)的地址 -->
/index.jsp = user
/first.action = user
/welcome.jsp = user
六、總結(jié)
- Shiro的授權(quán)過(guò)程和認(rèn)證過(guò)程是類(lèi)似的,在配置文件上配置需要授權(quán)的路徑,當(dāng)訪問(wèn)路徑的時(shí)候,Shiro過(guò)濾器去找到reaml,reaml返回?cái)?shù)據(jù)以后進(jìn)行比對(duì)。
- Shiro支持注解式授權(quán),直接在Controller方法上使用注解聲明訪問(wèn)該方法需要授權(quán)
- Shiro還支持標(biāo)簽授權(quán),但一般很少用
- 由于每次都要對(duì)reaml查詢數(shù)據(jù)庫(kù),性能會(huì)低。Shiro默認(rèn)是支持授權(quán)緩存的。為了達(dá)到很好的效果,我們使用Ehcache來(lái)對(duì)Shiro的緩存進(jìn)行管理
- 配置會(huì)話管理器,對(duì)會(huì)話時(shí)間進(jìn)行控制
- 手動(dòng)清空緩存
- 由于驗(yàn)證用戶名和密碼之前,一般需要驗(yàn)證驗(yàn)證碼的。所以,我們要改寫(xiě)表單驗(yàn)證的功能,先讓它去看看驗(yàn)證碼是否有錯(cuò),如果驗(yàn)證碼有錯(cuò)的話,那么用戶名和密碼就不用驗(yàn)證了。
- 將自定義的表單驗(yàn)證類(lèi)配置起來(lái)。
- 使用Shiro提供的記住我功能,如果用戶已經(jīng)認(rèn)證了,那就不用再次登陸了。可以直接訪問(wèn)某些頁(yè)面。
如果文章有錯(cuò)的地方歡迎指正,大家互相交流。習(xí)慣在微信看技術(shù)文章,想要獲取更多的Java資源的同學(xué),可以關(guān)注微信公眾號(hào):Java3y