shiro和web項目整合,實現類似真實項目的應用
- web項目中認證
- web項目中授權
- shiro緩存
- sessionManager使用
- 驗證碼功能實現
- 記住我功能實現
web項目中認證
實現方式
修改CustomRealm
的 doGetAuthenticationInfo
方法,從數據庫中獲取用戶信息,CustomRealm
返回查詢到的用戶信息,包括(加密後的密碼字符串和salt以及上文中的菜單)。
修改 doGetAuthenticationInfo
方法
@Autowired
private SysService sysService;
/**
* 用於認證
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//第一步:通過token獲取身份信息
String userCode = (String) token.getPrincipal();
//從數據庫中查詢賬號信息是否存在
SysUser sysUser = null;
try {
sysUser = sysService.findSysUserByUserCode(userCode);
} catch (Exception e1) {
e1.printStackTrace();
}
//如果查詢不到返回null
if(sysUser==null){
return null;
}
//第二步:通過獲取的身份信息進行數據庫查詢
String password = sysUser.getPassword();
//組裝ActiveUser類
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(sysUser.getId());
activeUser.setUsercode(sysUser.getUsercode());
activeUser.setUsername(sysUser.getUsername());
//查詢菜單信息
List<SysPermission> menus = null;
try {
menus = sysService.findMenuListByUserId(sysUser.getUsercode());
} catch (Exception e) {
e.printStackTrace();
}
activeUser.setMenus(menus);
//得到鹽
String salt = sysUser.getSalt();
//如果查詢到結果返回AuthenticationInfo
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(activeUser, password,ByteSource.Util.bytes(salt), "");
return authenticationInfo;
}
設置憑證匹配器
在我們的數據庫存儲的是MD5散列值,在自定義的realm中需要自定義設置散列算法以及散列次數。這裏和前面介紹的散列認證的配置方式類似。
<!-- 自定義的realm -->
<bean id="customRealm" class="cn.itcast.ssm.shiro.CustomRealm">
<!--將匹配器設置到realm中 -->
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
<!--定義憑證匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 設置hash散列算法 -->
<property name="hashAlgorithmName" value="md5" />
<!-- 設置hash散列次數 -->
<property name="hashIterations" value="1" />
</bean>
驗證認證功能
數據庫存在兩條用戶數據,具體如下:
其中:張三 的密碼是 111111。當然也可以自己修改密碼:
SELECT MD5('密碼'+'鹽')
如果可能正常登錄則沒有問題。
授權
實現方式
修改 CustomRealm
中的 doGetAuthorizationInfo
方法從數據庫中查詢授權信息。
這裏講解註解式授權和jsp標籤授權方法。
修改 doGetAuthorizationInfo
方法
/**
* 用於授權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//獲取身份信息,這個字段是在認證通過後返回的,也就是通過執行認證方法返回的AuthenticationInfo類的第一個屬性
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
//通過userId查詢數據庫獲取該身份信息的所有權限。
List<SysPermission> permissionList = null;
try {
permissionList = sysService.findPermissionListByUserId(activeUser.getUserid());
} catch (Exception e) {
e.printStackTrace();
}
List<String> permissions = new ArrayList<>();
if(permissionList!=null){
for(SysPermission p:permissionList){
permissions.add(p.getPercode());
}
}
//查詢到權限信息,然後返回權限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//將查詢到的授權信息填充到SimpleAuthorizationInfo中
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
controller類的AOP支持
<!--開啓aop,對類代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 開啓shiro註解支持 -->
<bean
class="
org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
在ItemsController類方法上添加註解
//商品信息方法
@RequestMapping("/queryItems")
@RequiresPermissions("item:query")//通過註解的方式進行授權
public ModelAndView queryItems(HttpServletRequest request) throws Exception {
System.out.println(request.getParameter("id"));
//調用service查詢商品列表
List<ItemsCustom> itemsList = itemsService.findItemsList(null);
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("itemsList", itemsList);
// 指定邏輯視圖名
modelAndView.setViewName("itemsList");
return modelAndView;
}
jsp標籤授權
在jsp頁面添加shiro taglib
<%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro" %>
shiro包括的jsp標籤
標籤名稱 | 標籤條件(均顯示標籤內容) |
---|---|
<shiro:authenticated> |
登錄之後 |
<shiro:notAuthenticated> |
不在登錄狀態時 |
<shiro:guest> |
用戶在沒有RememberMe時 |
<shiro:user> |
用戶在RememberMe時 |
<shiro:hasAnyRoles name="abc,123" > |
在有abc或者123角色時 |
<shiro:hasRole name="abc"> |
擁有角色abc |
<shiro:lacksRole name="abc"> |
沒有角色abc |
<shiro:hasPermission name="abc"> |
擁有權限資源abc |
<shiro:lacksPermission name="abc"> |
沒有abc權限資源 |
<shiro:principal> |
顯示用戶身份名稱 |
<shiro:principal property="username"/> |
顯示用戶身份中的屬性值 |
修改itemsList.jsp文件
<!-- 具有item:update權限才顯示修改鏈接,沒有的話不顯示。相當於if(hasPermission(item:update)) -->
<shiro:hasPermission name="item:update">
<a href="${pageContext.request.contextPath }/items/editItems.action?id=${item.id}">修改</a>
</shiro:hasPermission>
授權測試
當調用controller的一個方法時,由於該方法加了@RequiresPermissions("item:query")
註解,shiro會調用realm獲取數據庫中的權限信息,看 item:query
是否在權限數據中存在,如果不存在就拒絕訪問,如果存在就授權通過
當展現一個jsp頁面時,頁面中如果遇到 <shiro:hasPermission name="item:update">
標籤,shiro調用realm獲取數據庫中的權限信息,看 item:update
是否在權限數據中存在,如果不存在就不顯示標籤包含內容,如果存在則顯示。
在這裏只要遇到註解或shiro jsp標籤授權,就會調用realm查詢數據庫,在這裏需要引入緩存解決。
shiro緩存
針對授權時頻繁查詢數據庫的問題,引入shiro緩存。
緩存流程
用戶認證通過。
用戶第一次授權:調用realm查詢數據庫。
用戶第二次授權:不調用realm查詢數據庫,直接從緩存中讀取授權信息。
使用 ehcache
添加Ehcache的jar包
配置Ehcache配置文件:
新建shiro-ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!--diskStore:緩存數據持久化的目錄 地址 -->
<diskStore path="E:\develop\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
配置cacheManager
<!--securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 定義shiro緩存管理器 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
</bean>
清空緩存
當用戶權限修改後,用戶再次登錄shiro會自動調用realm從數據庫獲取權限數據,如果在修改權限後想立即清除緩存則可以調用realm的clearCache方法清除。
在CustomRealm
中定義clearCached方法:
/**
* 清除緩存方法
*/
public void clearCache(){
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
驗證碼功能實現
實現方式
shiro使用FormAuthenticationFilter進行表單認證,驗證碼校驗的功能應該加在FormAuthenticationFilter中,在認證之前進行驗證碼校驗。
自定義FormAuthenticationFilter
public class CustomFormAuthenticationFilter extends FormAuthenticationFilter{
/**
* 原AuthenticationFilter驗證方法
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//獲取正確的驗證碼和用戶輸入的驗證碼進行比對
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpSession session = httpServletRequest.getSession();
//從session獲取正確驗證碼
String validateCode = (String) session.getAttribute("validateCode");
//取出頁面的驗證碼
String randomcode = (String) httpServletRequest.getParameter("randomcode");
if(validateCode!=null && randomcode!=null && !validateCode.equals(randomcode)){
//驗證碼不相同,給shiroLoginFailure屬性設置值
request.setAttribute("shiroLoginFailure","randomcodeError");
//拒絕訪問,不再校驗賬號和密碼
return true;
}
return super.onAccessDenied(request, response);
}
}
配置自定義的FormAuthenticationFilter
在applicationContext-shiro.xml文件中配置
<!-- 自定義form認證過濾器 -->
<bean id="formAuthenticationFilter" class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter">
<!--表單中賬號的name屬性的值-->
<property name="usernameParam" value="account"/>
<!--表單中賬號的password屬性的值-->
<property name="passwordParam" value="accountPassword"/>
</bean>
修改 shiroFilter
配置
<!-- web.xml中shiro的filter對應的bean -->
<!-- shiro的web過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- logiUrl認證提交地址,如果沒有認證通過將會請求此地址進行認證,請求此地址將由formAuthenticationFilter進行表單認證 -->
<property name="loginUrl" value="/login.action" />
<!-- 認證成功後統一跳轉到first.action,建議不配置,shiro認證成功自動到上一個鏈接 -->
<property name="successUrl" value="/first.action" />
<!-- 通過unauthorizedUrl指定沒有權限時跳轉頁面 -->
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!-- 自定義filter配置 -->
<property name="filters">
<map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
修改 LoginController
的 login
方法
@RequestMapping("/login")
public String login(HttpServletRequest request)throws Exception{
//如果登錄失敗從request中獲取認證異常信息,shiroLoginFailure就是shiro異常類的全限定名
String exceptionClassName = (String) request.getAttribute("shiroLoginFailure");
if(exceptionClassName!=null){
if(UnknownAccountException.class.getName().equals(exceptionClassName)){
throw new CustomException("賬號不存在");
}else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)){
throw new CustomException("用戶名或密碼錯誤");
}else if("randomcodeError".equals(exceptionClassName)){
throw new CustomException("驗證碼錯誤");
}else{
throw new Exception();//最終在異常處理器生成未知錯誤
}
}
//此方法不處理登錄成功(認證成功),shiro認證成功會自動跳轉到上一個請求路徑。
//登錄失敗還到login頁面
return "login";
}
在登錄頁面添加驗證碼
<TR>
<TD>用戶名:</TD>
<TD colSpan="2"><input type="text" id="usercode" name="account" style="WIDTH: 130px" /> </TD>
</TR>
<TR>
<TD>密 碼:</TD>
<TD><input type="password" id="pwd" name="accountPassword" style="WIDTH: 130px" /></TD>
</TR>
<TR>
<TD>驗證碼:</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>
實現記住我功能
用戶登錄選擇”記住我”選項,本次登錄成功會向cookie寫身份信息,下次登錄從cookie中取出身份信息實現自動登錄。
用戶身份信息相關類實現 java.io.Serializable
接口
向cookie記錄身份信息的對象需要實現序列號接口,如下:
public class ActiveUser implements java.io.Serializable {
public class SysPermission implements java.io.Serializable{
配置 rememberMeManager
<!--配置記住我cookie-->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- rememerMe是cookie名稱 -->
<constructor-arg value="rememberMe"/>
<property name="maxAge" value="2592000"/>
</bean>
<!-- rememberMeManager管理器,寫cookie,取出cookie生成用戶信息 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="rememberMeCookie"/>
</bean>
添加到securityManager中
<!--securityManager安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
<property name="cacheManager" ref="cacheManager"/>
<!-- 記住我 -->
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
修改登錄頁面
<TR>
<TD></TD>
<TD><input type="checkbox" name="rememberMe">記住我</TD>
</TR>
修改rememberMe的input名稱
在前面的配置中修改了賬號和密碼的input的name屬性,”記住我”的name屬性值也可以修改
<!-- 自定義form認證過濾器 -->
<bean id="formAuthenticationFilter" class="cn.itcast.ssm.shiro.CustomFormAuthenticationFilter">
<!--表單中賬號的name屬性的值-->
<property name="usernameParam" value="account"/>
<!--表單中賬號的password屬性的值-->
<property name="passwordParam" value="accountPassword"/>
<!-- 修改記住我的name屬性的值 -->
<property name="rememberMeParam" value="rememberMe"/>
</bean>
測試記住我功能
選擇自動登錄後,需要查看cookie是否有rememberMe
使用UserFilter
在前一篇博客中有說明UserFilter的功能如下:
user:例如/admins/user/**=user沒有參數表示必須存在用戶, 身份認證通過或通過記住我認證通過的可以訪問,當登入操作時不做檢查
我們修改applicationContext-shiro.xml配置文件
<!-- shiro的web過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<!-- logiUrl認證提交地址,如果沒有認證通過將會請求此地址進行認證,請求此地址將由formAuthenticationFilter進行表單認證 -->
<property name="loginUrl" value="/login.action" />
<!-- 認證成功後統一跳轉到first.action,建議不配置,shiro認證成功自動到上一個鏈接 -->
<property name="successUrl" value="/first.action" />
<!-- 通過unauthorizedUrl指定沒有權限時跳轉頁面 -->
<property name="unauthorizedUrl" value="/refuse.jsp" />
<!-- 自定義filter配置 -->
<property name="filters">
<map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<!-- 過濾器鏈定義,從上向下順序執行,一般將/**放在最後面 -->
<property name="filterChainDefinitions">
<value>
<!--靜態資源可以匿名訪問 -->
/images/** = anon
/js/** = anon
/styles/** = anon
/validatecode.jsp = anon
<!-- 請求logout.action地址,shiro去清除session -->
/logout.action = logout
<!-- 配置需要授權的url,查詢商品需要有商品查詢權限 -->
<!-- /items/queryItems.action = perms[item:query] /items/editItems.action
= perms[item:update] -->
<!-- 配置記住我或認證通過可以訪問地址 -->
/index.jsp = user
/first.action = user
/welcome.jsp = user
<!-- /**=authc 表示所有的url都需要認證才能訪問 -->
/** = authc
</value>
</property>
</bean>
blog項目的下載地址
點擊進入下載頁面