Shiro提供身份驗證、受權、企業會話管理和加密等功能。css
一、添加依賴:html
<!-- shiro spring. --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.2</version> </dependency> <!-- shiro ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency> <!-- shiro-thymeleaf 2.0.0--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
二、在src/main/resources添加config文件夾,建立ehcache-shiro.xml文件,用於權限緩存:前端
<?xml version="1.0" encoding="UTF-8"?> <!--<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"--> <!--xsi:noNamespaceSchemaLocation="ehcache.xsd">--> <ehcache name="es"> <diskStore path="java.io.tmpdir"/> <!-- name:緩存名稱。 maxElementsInMemory:緩存最大數目 maxElementsOnDisk:硬盤最大緩存個數。 eternal:對象是否永久有效,一但設置了,timeout將不起做用。 overflowToDisk:是否保存到磁盤,當系統當機時 timeToIdleSeconds:設置對象在失效前的容許閒置時間(單位:秒)。僅當eternal=false對象不是永久有效時使用,可選屬性,默認值是0,也就是可閒置時間無窮大。 timeToLiveSeconds:設置對象在失效前容許存活時間(單位:秒)。最大時間介於建立時間和失效時間之間。僅當eternal=false對象不是永久有效時使用,默認是0.,也就是對象存活時間無窮大。 diskPersistent:是否緩存虛擬機重啓期數據 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. diskSpoolBufferSizeMB:這個參數設置DiskStore(磁盤緩存)的緩存區大小。默認是30MB。每一個Cache都應該有本身的一個緩衝區。 diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認是120秒。 memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。默認策略是LRU(最近最少使用)。你能夠設置爲FIFO(先進先出)或是LFU(較少使用)。 clearOnFlush:內存數量最大時是否清除。 memoryStoreEvictionPolicy: Ehcache的三種清空策略; FIFO,first in first out,這個是你們最熟的,先進先出。 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一點就是講一直以來最少被使用的。如上面所講,緩存的元素有一個hit屬性,hit值最小的將會被清出緩存。 LRU,Least Recently Used,最近最少使用的,緩存的元素有一個時間戳,當緩存容量滿了,而又須要騰出地方來緩存新的元素的時候,那麼現有緩存元素中時間戳離當前時間最遠的元素將被清出緩存。 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> <cache name="user" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"/> <!-- 登陸記錄緩存鎖定1小時 --> <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true" /> </ehcache>
三、實現自定義的ShiroRealm.java類:java
package com.example.demo.realm; import com.example.demo.entity.Menu; import com.example.demo.entity.Role; import com.example.demo.entity.User; import com.example.demo.service.MenuService; import com.example.demo.service.RoleService; import com.example.demo.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.util.HashSet; import java.util.List; import java.util.Set; /** * 在認證、受權內部實現機制中都有提到,最終處理都將交給Real進行處理。由於在Shiro中,最終是經過Realm來獲取應用程序中的用戶、角色及權限信息的。一般狀況下,在Realm中會直接從咱們的數據源中獲取Shiro須要的驗證信息。能夠說,Realm是專用於安全框架的DAO. * Shiro的認證過程最終會交由Realm執行,這時會調用Realm的getAuthenticationInfo(token)方法。 * 該方法主要執行如下操做: * * 檢查提交的進行認證的令牌信息 * 根據令牌信息從數據源(一般爲數據庫)中獲取用戶信息 * 對用戶信息進行匹配驗證。 * 驗證經過將返回一個封裝了用戶信息的AuthenticationInfo實例。 * 驗證失敗則拋出AuthenticationException異常信息。而在咱們的應用程序中要作的就是自定義一個Realm類,繼承AuthorizingRealm抽象類,重載doGetAuthenticationInfo(),重寫獲取用戶信息的方法。 */ public class ShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private RoleService roleService; @Autowired private MenuService menuService; /** * 驗證用戶身份 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String userName = (String) token.getPrincipal(); String password = new String((char[]) token.getCredentials()); //實際項目中,這裏能夠根據實際狀況作緩存,若是不作,Shiro本身也是有時間間隔機制,2分鐘內不會重複執行該方法 User user = this.userService.findByName(userName); //這裏校驗了,CredentialsMatcher就不須要了,若是這裏不校驗,調用CredentialsMatcher校驗 if (user == null) { throw new UnknownAccountException("用戶名或密碼錯誤!"); } if (!password.equals(user.getPassword())) { throw new IncorrectCredentialsException("用戶名或密碼錯誤!"); } if ("0".equals(user.getEnabled())) { throw new LockedAccountException("帳號已被鎖定,請聯繫管理員!"); } //也能夠在此處更新最後登陸時間(或在登陸方法實現) SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName()); // SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()),getName()); ////salt=username+salt return info; } /** * 受權用戶權限 * 受權的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>標籤的時候調用的,它會去檢測shiro框架中的權限(這裏的permissions)是否包含有該標籤的name值,若是有,裏面的內容顯示,若是沒有,裏面的內容不予顯示(這就完成了對於權限的認證.) */ /** * shiro的權限受權是經過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo(); * 當訪問到頁面的時候,連接配置了相應的權限或者shiro標籤纔會執行此方法不然不會執行,因此若是隻是簡單的身份認證沒有權限的控制的話,那麼這個方法能夠不進行實現,直接返回null便可。 * 在這個方法中主要是使用類:SimpleAuthorizationInfo * 進行角色的添加和權限的添加。 * authorizationInfo.addRole(role.getRole()); * authorizationInfo.addStringPermission(p.getPermission()); * 固然也能夠添加set集合:roles是從數據庫查詢的當前用戶的角色,stringPermissions是從數據庫查詢的當前用戶對應的權限 * authorizationInfo.setRoles(roles); * authorizationInfo.setStringPermissions(stringPermissions); * 就是說若是在shiro配置文件中添加了filterChainDefinitionMap.put(「/add」, 「perms[權限添加]」); * 就說明訪問/add這個連接必需要有「權限添加」這個權限才能夠訪問, * 若是在shiro配置文件中添加了filterChainDefinitionMap.put(「/add」, 「roles[100002],perms[權限添加]」); * 就說明訪問/add這個連接必需要有「權限添加」這個權限和具備「100002」這個角色才能夠訪問。 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //獲取用戶 User user = (User) SecurityUtils.getSubject().getPrincipal(); // User user = (User) principalCollection.getPrimaryPrincipal(); // User user=(User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();//獲取session中的用戶 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); List<Role> roles=this.roleService.findRolesByUserId(user.getId()); //獲取用戶角色 Set<String> roleSet = new HashSet<String>(); for (Role role:roles) { roleSet.add(role.getName()); } info.setRoles(roleSet); List<Menu> menus=this.menuService.findMenusByUserId(user.getId()); //獲取用戶權限 Set<String> permissionSet = new HashSet<String>(); for (Menu menu:menus) { if(!StringUtils.isEmpty(menu.getPermission())) { //權限爲空會異常,Caused by: java.lang.IllegalArgumentException: Wildcard string cannot be null CollectionUtils.mergeArrayIntoCollection(menu.getPermission().split(","), permissionSet); } } info.setStringPermissions(permissionSet); return info; } /** * 待補充: * shiro+redis集成,避免每次訪問有權限的連接都會去執行MyShiroRealm.doGetAuthenticationInfo()方法來查詢當前用戶的權限,由於實際狀況中權限是不會常常變得,這樣就可使用redis進行權限的緩存。 * 實現shiro連接權限的動態加載,以前要添加一個連接的權限,要在shiro的配置文件中添加filterChainDefinitionMap.put(「/add」, 「roles[100002],perms[權限添加]」),這樣很不方便管理,一種方法是將權限使用數據庫進行加載,另外一種是經過init配置文件的方式讀取。 * Shiro 自定義權限校驗Filter定義,及功能實現。 * Shiro Ajax請求權限不知足,攔截後解決方案。這裏有一個前提,咱們知道Ajax不能作頁面redirect和forward跳轉,因此Ajax請求假如沒登陸,那麼這個請求給用戶的感受就是沒有任何反應,而用戶又不知道用戶已經退出了。 * 在線顯示,在線用戶管理(踢出登陸)。 * 登陸註冊密碼加密傳輸。 * 記住個人功能。關閉瀏覽器後仍是登陸狀態。 */ }
四、若有須要實現自定義的密碼校驗CredentialsMatcher.java:git
package com.example.demo.realm; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; /** * shiro中惟一須要程序員編寫的兩個類:類ShiroRealm完成根據用戶名去數據庫的查詢,而且將用戶信息放入shiro中,供第二個類調用.CredentialsMatcher,完成對於密碼的校驗.其中用戶的信息來自ShiroRealm類 */ public class CredentialsMatcher extends SimpleCredentialsMatcher { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken utoken=(UsernamePasswordToken) token; //得到用戶輸入的密碼:(能夠採用加鹽(salt)的方式去檢驗) String inPassword = new String(utoken.getPassword()); //得到數據庫中的密碼 String dbPassword=(String) info.getCredentials(); //進行密碼的比對 return this.equals(inPassword, dbPassword); } }
五、根據須要,選擇建立ShiroSessionListener.java:程序員
package com.example.demo.listener; import java.util.concurrent.atomic.AtomicInteger; import org.apache.shiro.session.Session; import org.apache.shiro.session.SessionListener; public class ShiroSessionListener implements SessionListener{ private final AtomicInteger sessionCount = new AtomicInteger(0); @Override public void onStart(Session session) { sessionCount.incrementAndGet(); } @Override public void onStop(Session session) { sessionCount.decrementAndGet(); } @Override public void onExpiration(Session session) { sessionCount.decrementAndGet(); } }
六、根據須要建立自定義Filter:github
package com.example.demo.filter; import java.io.IOException; import java.util.Set; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.StringUtils; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import org.apache.shiro.web.util.WebUtils; import org.springframework.beans.factory.annotation.Autowired; /** * @Type MyFilter.java * @Desc 用於自定義過濾器,過濾用戶請求是否被受權 ,MyFilter是用於過濾須要權限校驗的請求 */ public class MyFilter extends AuthorizationFilter { @SuppressWarnings("unchecked") @Override protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object arg2) throws Exception { HttpServletRequest request = (HttpServletRequest) req; //獲取請求路徑 String path = request.getServletPath(); Subject subject = getSubject(req, resp); if (null != subject.getPrincipals()) { //根據session中存放的用戶權限,比對路徑,若是擁有該權限則放行 Set<String> userPrivileges = (Set<String>) request.getSession() .getAttribute("USER_PRIVILEGES"); if (null != userPrivileges && userPrivileges.contains(path)) { return true; } } return false; } /** * 會話超時或權限校驗未經過的,統一返回401,由前端頁面彈窗提示 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { if (isAjax((HttpServletRequest) request)) { WebUtils.toHttp(response).sendError(401); } else { String unauthorizedUrl = getUnauthorizedUrl(); if (StringUtils.hasText(unauthorizedUrl)) { WebUtils.issueRedirect(request, response, unauthorizedUrl); } else { WebUtils.toHttp(response).sendError(401); } } return false; } private boolean isAjax(HttpServletRequest request) { String header = request.getHeader("x-requested-with"); if (null != header && "XMLHttpRequest".endsWith(header)) { return true; } return false; } }
package com.example.demo.filter; import java.io.IOException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.StringUtils; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import org.apache.shiro.web.util.WebUtils; /** * @Type LoginFilter.java * @Desc 用於自定義過濾器,過濾用戶請求時是不是登陸狀態 loginFilter主要是覆蓋了自帶的authc過濾器,讓未登陸的請求統一返回401 */ public class LoginFilter extends AuthorizationFilter { @Override protected boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object arg2) throws Exception { Subject subject = getSubject(req, resp); if (null != subject.getPrincipals()) { return true; } return false; } /** * 會話超時或權限校驗未經過的,統一返回401,由前端頁面彈窗提示 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { if (isAjax((HttpServletRequest) request)) { WebUtils.toHttp(response).sendError(401); } else { String unauthorizedUrl = getUnauthorizedUrl(); if (StringUtils.hasText(unauthorizedUrl)) { WebUtils.issueRedirect(request, response, unauthorizedUrl); } else { WebUtils.toHttp(response).sendError(401); } } return false; } private boolean isAjax(HttpServletRequest request) { String header = request.getHeader("x-requested-with"); if (null != header && "XMLHttpRequest".endsWith(header)) { return true; } return false; } }
六、建立Shiro配置類,可用shiro-conf.xml代替:web
package com.example.demo.config; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import com.example.demo.listener.ShiroSessionListener; import com.example.demo.realm.CredentialsMatcher; import org.apache.shiro.cache.MemoryConstrainedCacheManager; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.ehcache.EhCacheManager; import com.example.demo.realm.ShiroRealm; import org.apache.shiro.codec.Base64; import org.apache.shiro.session.SessionListener; import org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.ValidatingSessionManager; import org.apache.shiro.session.mgt.eis.*; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; //import org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler /** * shiro的配置類,須要注意一點filterChainDefinitionMap必須是LinkedHashMap由於它必須保證有序 * @author Administrator * */ @Configuration public class ShiroConfig { //受權緩存管理器 @Bean public EhCacheManager getEhCacheManager() { EhCacheManager em = new EhCacheManager(); em.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml"); return em; } /** * 使用上面的Ehcache或下面的shiro自帶的內存緩存實現 */ // @Bean // public MemoryConstrainedCacheManager getMemoryConstrainedCacheManager() { // return new MemoryConstrainedCacheManager(); // } /** * ShiroFilterFactoryBean 處理攔截資源文件問題。 * 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,由於在 * 初始化ShiroFilterFactoryBean的時候須要注入:SecurityManager * * Filter Chain定義說明 一、一個URL能夠配置多個Filter,使用逗號分隔 二、當設置多個過濾器時,所有驗證經過,才視爲經過 * 三、部分過濾器可指定參數,如perms,roles * * <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> * <property name="securityManager" ref="securityManager" /> * <!-- 配置登陸頁 --> * <property name="loginUrl" value="/login.jsp" /> * <!-- 配置登陸成功後的頁面 --> * <property name="successUrl" value="/list.jsp" /> * <property name="unauthorizedUrl" value="/unauthorized.jsp" /> * <property name="filterChainDefinitions"> * <value> * <!-- 靜態資源容許訪問 --> * <!-- 登陸頁容許訪問 --> * /login.jsp = anon * /test/login = anon * /user/delete = perms["delete"] * /logout = logout * <!-- 其餘資源都須要認證 --> * /** = authc * </value> * </property> * </bean> */ /** * Shiro主過濾器自己功能十分強大,其強大之處就在於它支持任何基於URL路徑表達式的、自定義的過濾器的執行 * Web應用中,Shiro可控制的Web請求必須通過Shiro主過濾器的攔截,Shiro對基於Spring的Web應用提供了完美的支持 * @param securityManager * @return */ @Bean(name="shiroFilter") public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必須設置 SecurityManager,Shiro的核心安全接口 shiroFilterFactoryBean.setSecurityManager(securityManager); // 配置登陸的url,若是不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面(源碼) shiroFilterFactoryBean.setLoginUrl("/login"); //這是後臺的/控制器 // 登陸成功後要跳轉的連接,本例中此屬性用不到,由於登陸成功後的處理邏輯在LoginController裏硬編碼了 shiroFilterFactoryBean.setSuccessUrl("/index"); //這是Index.html頁面 // 未受權界面;配置不會被攔截的連接 順序判斷 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized"); ////這裏設置403並不會起做用 // 有自定義攔截器就放開 wangzs(源碼) // //自定義攔截器 // LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>(); // //限制同一賬號同時在線的個數。 // //filtersMap.put("kickout", kickoutSessionControlFilter()); // shiroFilterFactoryBean.setFilters(filtersMap); // 配置訪問權限,權限控制map.Shiro鏈接約束配置,即過濾鏈的定義, LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 配置不會被攔截的連接 順序判斷 // value值的'/'表明的路徑是相對於HttpServletRequest.getContextPath()的值來的 // anon:它對應的過濾器裏面是空的,什麼都沒作,這裏.do和.jsp後面的*表示參數,比方說login.jsp?main這種 // authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter // 配置退出過濾器,其中的具體的退出代碼Shiro已經替咱們實現了 // filterChainDefinitionMap.put("/logout", "logout"); // 從數據庫獲取動態的權限 // filterChainDefinitionMap.put("/add", "perms[權限添加]"); /userList=roles[admin],須要有admin這個角色,若是沒有此角色訪問此URL會返回無受權頁面,或authc,perms[user:list] // <!-- 須要權限爲add的用戶才能訪問此請求--> // /user=perms[user:add] // <!-- 須要管理員角色才能訪問此頁面 --> // /user/add=roles[admin]或roles[admin],perms[user:add] // <!-- 過濾鏈定義,從上向下順序執行,通常將 /**放在最爲下邊 -->:這是一個坑呢,一不當心代碼就很差使了; // <!-- authc:全部url都必須認證經過才能夠訪問; anon:全部url都均可以匿名訪問--> //logout這個攔截器是shiro已經實現好了的。 // 從數據庫獲取 /*List<SysPermissionInit> list = sysPermissionInitService.selectAll(); for (SysPermissionInit sysPermissionInit : list) { filterChainDefinitionMap.put(sysPermissionInit.getUrl(), sysPermissionInit.getPermissionInit()); }*/ /** * 可自定義過濾器,好比myFilter替代authc * <bean id="myFilter" class="com.cmcc.hygcc.comm.shiro.MyFilter"></bean> * <property name="filters"> * <map> * <entry key="myFilter" value-ref="myFilter" /> * <!-- 覆蓋authc過濾器,使得未登陸的ajax請求返回401狀態 --> * <entry key="authc" value-ref="loginFilter" /> * </map> * </property> * * /**=myFilter */ //靜態資源容許訪問//登陸頁容許訪問,一個URL能夠配置多個Filter,使用逗號分隔,當設置多個過濾器時,所有驗證經過,才視爲經過,部分過濾器可指定參數,如perms,roles // filterChainDefinitionMap.put("/login.html*", "anon"); //表示能夠匿名訪問,*表示參數如?error等 filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/unauthorized", "anon"); filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/druid/**", "anon"); filterChainDefinitionMap.put("/user/regist", "anon"); filterChainDefinitionMap.put("/gifCode", "anon"); filterChainDefinitionMap.put("/logout", "logout"); //logout是shiro提供的過濾器 filterChainDefinitionMap.put("/user/delete", "perms[\"user:delete\"]"); //此時訪問/user/delete須要delete權限,在自定義Realm中爲用戶受權。 //其餘資源都須要認證 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; //Shiro攔截器工廠類注入成功 } //配置核心安全事務管理器 @Bean(name="securityManager") public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 設置realm. securityManager.setRealm(shiroRealm()); //若是方法加上參數@Qualifier("shiroRealm") ShiroRealm shiroRealm,能夠直接securityManager.setRealm(shiroRealm); //注入記住我管理器; securityManager.setRememberMeManager(rememberMeManager()); // 自定義緩存實現 可以使用redis // securityManager.setCacheManager(cacheManager()); securityManager.setCacheManager(getEhCacheManager()); //緩存管理器 // 自定義session管理 可以使用redis securityManager.setSessionManager(sessionManager()); return securityManager; } //Shiro生命週期處理器 @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } //配置自定義的權限登陸器,本段不須要配置自定義的密碼比較器,能夠換成下面的~~裏面的寫法,須要建立自定義的密碼比較器CredentialsMatcher.java @Bean //必須,身份認證realm; (這個須要本身寫,帳號密碼校驗;權限等) public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); return shiroRealm; } //須要這種方式就放開 //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // //配置自定義的權限登陸器 // @Bean(name="shiroRealm") // public ShiroRealm shiroRealm(@Qualifier("credentialsMatcher") CredentialsMatcher matcher) { // ShiroRealm shiroRealm=new ShiroRealm(); // shiroRealm.setCredentialsMatcher(matcher); // return shiroRealm; // } // //配置自定義的密碼比較器 // @Bean(name="credentialsMatcher") // public CredentialsMatcher credentialsMatcher() { // return new CredentialsMatcher(); // } // //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * shiro的密碼比較器 * @return */ // @Bean // public HashedCredentialsMatcher hashedCredentialsMatcher() { // HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法; // hashedCredentialsMatcher.setHashIterations(2);//散列的次數,好比散列兩次,至關於 md5(md5("")); // return hashedCredentialsMatcher; // } /** //必須 * cookie對象;會話Cookie模板 ,默認爲: JSESSIONID 問題: 與SERVLET容器名衝突,從新定義爲sid或rememberMe,自定義 * @return */ public SimpleCookie rememberMeCookie() { //這個參數是cookie的名稱,對應前端的checkbox的name = rememberMe SimpleCookie cookie = new SimpleCookie("rememberMe"); //<!-- 記住我cookie生效時間30天 ,單位秒;--> cookie.setHttpOnly(true); cookie.setMaxAge(86400); return cookie; } /** //必須 * cookie管理對象;記住我功能,rememberMe管理器 * @return */ public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); //rememberMe cookie加密的密鑰 建議每一個項目都不同 默認AES算法 密鑰長度(128 256 512 位) //3AvVhmFLUs0KTA3Kprsdag== cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; } /** * 使用shiro註解爲用戶受權 1. 在shiro-config.xml開啓shiro註解(硬編碼,修改權限碼很麻煩) * 在方法上配置註解@RequiresPermissions("xxx:yyy") * <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> * <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> * <property name="securityManager" ref="securityManager"/> * </bean> * 編程方式實現用戶權限控制 * Subject subject = SecurityUtils.getSubject(); * if(subject.hasRole("admin")){ * //有權限 * }else{ * //無權限 * } * @return AOP式方法級權限檢查,DefaultAdvisorAutoProxyCreator用來掃描上下文,尋找全部的Advistor(通知器),將這些Advisor應用到全部符合切入點的Bean中。 * LifecycleBeanPostProcessor將Initializable和Destroyable的實現類統一在其內部自動分別調用了Initializable.init()和Destroyable.destroy()方法,從而達到管理shiro bean * 生命週期的目的。 */ @Bean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } // AOP式方法級權限檢查 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { //@Qualifier("securityManager") SecurityManager manager AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean //必須(thymeleaf頁面使用shiro標籤控制按鈕是否顯示) //未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect public ShiroDialect shiroDialect() { return new ShiroDialect(); } //SessionManager和SessionDAO能夠不配置,會話DAO @Bean public SessionDAO sessionDAO() { MemorySessionDAO sessionDAO = new MemorySessionDAO(); return sessionDAO; } /** * sessionDao的方法2 * @return */ // @Bean // // public SessionIdGenerator sessionIdGenerator() { // return new JavaUuidSessionIdGenerator(); // } // @Bean // public SessionDAO sessionDAO() { // EnterpriseCacheSessionDAO cacheSessionDAO=new EnterpriseCacheSessionDAO(); // cacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache"); // cacheSessionDAO.setSessionIdGenerator(sessionIdGenerator()); // return cacheSessionDAO; // } //// 會話管理器,設定會話超時及保存 @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); Collection<SessionListener> listeners = new ArrayList<SessionListener>(); listeners.add(new ShiroSessionListener()); sessionManager.setSessionListeners(listeners); sessionManager.setGlobalSessionTimeout(1800000); //全局會話超時時間(單位毫秒),默認30分鐘 sessionManager.setSessionDAO(sessionDAO()); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionValidationSchedulerEnabled(true); //定時清理失效會話, 清理用戶直接關閉瀏覽器形成的孤立會話 sessionManager.setSessionValidationInterval(1800000); // sessionManager.setSessionValidationScheduler(executorServiceSessionValidationScheduler()); sessionManager.setSessionIdCookieEnabled(true); sessionManager.setSessionIdCookie(rememberMeCookie()); return sessionManager; } //會話驗證調度器,每30分鐘執行一次驗證 @Bean(name="sessionValidationScheduler") public ExecutorServiceSessionValidationScheduler executorServiceSessionValidationScheduler() { ExecutorServiceSessionValidationScheduler sessionValidationScheduler=new ExecutorServiceSessionValidationScheduler(); sessionValidationScheduler.setInterval(1800000); sessionValidationScheduler.setSessionManager((ValidatingSessionManager)sessionManager()); return sessionValidationScheduler; } }
七、未受權的設置不生效,須要添加未受權異常處理:ajax
package com.example.demo.resolver; import org.apache.shiro.mgt.SecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import java.util.Properties; /** * 定製的異常處理類 */ // 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默認過濾器(10個) // anon -- org.apache.shiro.web.filter.authc.AnonymousFilter // authc -- org.apache.shiro.web.filter.authc.FormAuthenticationFilter // authcBasic -- org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter // perms -- org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter // port -- org.apache.shiro.web.filter.authz.PortFilter // rest -- org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter // roles -- org.apache.shiro.web.filter.authz.RolesAuthorizationFilter // ssl -- org.apache.shiro.web.filter.authz.SslFilter // user -- org.apache.shiro.web.filter.authc.UserFilter // logout -- org.apache.shiro.web.filter.authc.LogoutFilter @Configuration public class DzExceptionResolver { /** * shiro中unauthorizedUrl不起做用,這是由於shiro源代碼private void applyUnauthorizedUrlIfNecessary(Filter filter)中判斷了filter是否爲AuthorizationFilter,只有perms,roles,ssl,rest,port纔是屬於AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,因此unauthorizedUrl設置後不起做用。 * 解決方法:在shiro配置文件中添加(異常全路徑作key,錯誤頁面作value) * <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> * <property name="exceptionMappings"> * <props> * <prop key="org.apache.shiro.authz.UnauthorizedException">/403</prop> * </props> * </property> * </bean> */ @Bean public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver(); Properties properties=new Properties(); properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized"); simpleMappingExceptionResolver.setExceptionMappings(properties); return simpleMappingExceptionResolver; } /** * 至關於調用SecurityUtils.setSecurityManager(securityManager) * @param securityManager * @return */ @Bean public MethodInvokingFactoryBean getMethodInvokingFactoryBean(@Qualifier("securityManager")SecurityManager securityManager) { MethodInvokingFactoryBean methodInvokingFactoryBean=new MethodInvokingFactoryBean(); methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager"); methodInvokingFactoryBean.setArguments(securityManager); return methodInvokingFactoryBean; } }
八、登陸和退出Controller:redis
package com.example.demo.controller; import com.example.demo.entity.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Properties; @Controller public class LoginController { @RequestMapping(value = "/login",method = RequestMethod.GET) public String login() { return "login"; } /** * 和shiro框架的交互徹底經過Subject這個類去交互,用它完成登陸,註銷,獲取當前的用戶對象等操做 * login請求調用subject.login以後,shiro會將token傳遞給自定義realm,此時realm會先調用doGetAuthenticationInfo(AuthenticationToken authcToken )登陸驗證的方法,驗證經過後會接着調用 doGetAuthorizationInfo(PrincipalCollection principals)獲取角色和權限的方法(受權),最後返回視圖。 * 當其餘請求進入shiro時,shiro會調用doGetAuthorizationInfo(PrincipalCollection principals)去獲取受權信息,如果沒有權限或角色,會跳轉到未受權頁面,如有權限或角色,shiro會放行,此時進入真正的請求方法…… * @param username * @param password * @param model * @param session * @return */ @RequestMapping(value = "/login",method = RequestMethod.POST) // public String loginUser(String username,String password,boolean remeberMe,HttpSession session) { public String loginUser(HttpServletRequest request, String username, String password, Model model, HttpSession session) { // password=new SimpleHash("md5", password, ByteSource.Util.bytes(username.toLowerCase() + "shiro"),2).toHex(); // UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password,remeberMe); UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password); Subject subject = SecurityUtils.getSubject(); try { subject.login(usernamePasswordToken); //完成登陸 User user=(User) subject.getPrincipal(); //更新用戶登陸時間,也能夠在ShiroRealm裏面作 session.setAttribute("user", user); model.addAttribute("user",user); return "index"; } catch(Exception e) { String exception = (String) request.getAttribute("shiroLoginFailure"); //logger.info("登陸失敗從request中獲取shiro處理的異常信息,shiroLoginFailure:就是shiro異常類的全類名"); model.addAttribute("msg",e.getMessage()); return "login";//返回登陸頁面 } } @RequestMapping("/logout") public String logout(HttpSession session,Model model) { Subject subject = SecurityUtils.getSubject(); subject.logout(); // session.removeAttribute("user"); model.addAttribute("msg","安全退出!"); return "login"; } }
九、權限測試Controller:
package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class IndexController { @Autowired private UserService userService; @RequestMapping("/index") public User index() { return userService.getUserById(1); } @RequiresPermissions("test") @RequestMapping("/test") public String test() { return "ok"; } }
十、項目用的Thymeleaf,要在頁面使用shiro標籤,首先添加依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- shiro-thymeleaf 2.0.0--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
十一、application.properties配置thymeleaf:
#thymeleaf的配置是去掉頁面緩存(開發環境)和html的校驗 spring.thymeleaf.cache=false spring.thymeleaf.mode=LEGACYHTML5
十二、在shiro的configuration中配置:
@Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); }
1三、在html中加入xmlns:
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
1四、使用標籤。例如:
<span shiro:authenticated="true" > <span>歡迎您:<span th:text="${userInfo.realName}"></span></span> </span>
1五、標籤說明:
一、用戶沒有身份驗證時顯示相應信息,即遊客訪問信息: <shiro:guest>內容</shiro:guest> 二、用戶已經身份驗證/記住我登陸後顯示相應的信息: <shiro:user>內容</shiro:user> 三、用戶已經身份驗證經過,即Subject.login登陸成功,不是記住我登陸的: <shiro:authenticated>內容</shiro:authenticated> 四、顯示用戶身份信息,一般爲登陸賬號信息,默認調用Subject.getPrincipal()獲取,即Primary Principal: <shiro:principal/> 五、用戶已經身份驗證經過,即沒有調用Subject.login進行登陸,包括記住我自動登陸的也屬於未進行身份驗證,與guest標籤的區別是,該標籤包含已記住用戶。: <shiro:notAuthenticated>內容</shiro:notAuthenticated> 六、<shiro:principal type="java.lang.String"/> 至關於Subject.getPrincipals().oneByType(String.class)。 七、<shiro:principal property="username"/> 至關於((User)Subject.getPrincipals()).getUsername()。 八、若是當前Subject有角色將顯示body體內容: <shiro:hasRole name="角色名">內容</shiro:hasRole> 九、<shiro:hasAnyRoles name="角色名1,角色名2…">內容</shiro:hasAnyRoles> 若是當前Subject有任意一個角色(或的關係)將顯示body體內容。 十、<shiro:lacksRole name="角色名">內容</shiro:lacksRole> 若是當前Subject沒有角色將顯示body體內容。 十一、<shiro:hasPermission name="權限名">內容</shiro:hasPermission> 若是當前Subject有權限將顯示body體內容。 十二、<shiro:lacksPermission name="權限名">內容</shiro:lacksPermission> 若是當前Subject沒有權限將顯示body體內容。
補充:
1).須要在templates下建立login.html、index.html、unauthorized.html頁面。 2).自定義Filter,ShiroSessionListener和自定義密碼校驗以及ShiroConfig中的配置項,按需添加便可。