30 分鐘學會如何使用 Shiro

 

做者:冷豪前端

1、架構java

要學習如何使用Shiro必須先從它的架構談起,做爲一款安全框架Shiro的設計至關精妙。Shiro的應用不依賴任何容器,它也能夠在JavaSE下使用。可是最經常使用的環境仍是JavaEE。下面以用戶登陸爲例:程序員

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

一、使用用戶的登陸信息建立令牌web

UsernamePasswordToken token = new UsernamePasswordToken(username, password);

token能夠理解爲用戶令牌,登陸的過程被抽象爲Shiro驗證令牌是否具備合法身份以及相關權限。面試

二、執行登錄動做算法

SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
Subject subject = SecurityUtils.getSubject(); // 獲取Subject單例對象
subject.login(token); // 登錄

Shiro的核心部分是SecurityManager,它負責安全認證與受權。Shiro自己已經實現了全部的細節,用戶能夠徹底把它當作一個黑盒來使用。SecurityUtils對象,本質上就是一個工廠相似Spring中的ApplicationContext。spring

Subject是初學者比較難於理解的對象,不少人覺得它能夠等同於User,其實否則。Subject中文翻譯:項目,而正確的理解也偏偏如此。它是你目前所設計的須要經過Shiro保護的項目的一個抽象概念。經過令牌(token)與項目(subject)的登錄(login)關係,Shiro保證了項目總體的安全。數據庫

我把歷史發佈過的實戰文章整理成了 PDF ,關注微信公衆號「Java後端」回覆 666 下載。apache

三、判斷用戶後端

Shiro自己沒法知道所持有令牌的用戶是否合法,由於除了項目的設計人員恐怕誰都沒法得知。所以Realm是整個框架中爲數很少的必須由設計者自行實現的模塊,固然Shiro提供了多種實現的途徑,本文只介紹最多見也最重要的一種實現方式——數據庫查詢。

四、兩條重要的英文

我在學習Shiro的過程當中遇到的第一個障礙就是這兩個對象的英文名稱:AuthorizationInfo,AuthenticationInfo。不用懷疑本身的眼睛,它們確實長的很像,不但長的像,就連意思都十分近似。

在解釋它們前首先必需要描述一下Shiro對於安全用戶的界定:和大多數操做系統同樣。用戶具備角色和權限兩種最基本的屬性。例如,個人Windows登錄名稱是learnhow,它的角色是administrator,而administrator具備全部系統權限。這樣learnhow天然就擁有了全部系統權限。那麼其餘人須要登陸個人電腦怎麼辦,我能夠開放一個guest角色,任何沒法提供正確用戶名與密碼的未知用戶均可以經過guest來登陸,而系統對於guest角色開放的權限極其有限。

同理,Shiro對用戶的約束也採用了這樣的方式。AuthenticationInfo表明了用戶的角色信息集合,AuthorizationInfo表明了角色的權限信息集合。如此一來,當設計人員對項目中的某一個url路徑設置了只容許某個角色或具備某種權限才能夠訪問的控制約束的時候,Shiro就能夠經過以上兩個對象來判斷。說到這裏,你們可能還比較困惑。先不要着急,繼續日後看就天然會明白了。

2、實現Realm

如何實現Realm是本文的重頭戲,也是比較費事的部分。這裏你們會接觸到幾個新鮮的概念:緩存機制、散列算法、加密算法。因爲本文不會專門介紹這些概念,因此這裏僅僅拋磚引玉的談幾點,能幫助你們更好的理解Shiro便可。

一、緩存機制

Ehcache是不少Java項目中使用的緩存框架,Hibernate就是其中之一。它的本質就是將本來只能存儲在內存中的數據經過算法保存到硬盤上,再根據需求依次取出。你能夠把Ehcache理解爲一個Map<String,Object>對象,經過put保存對象,再經過get取回對象。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache">
   <diskStore path="java.io.tmpdir" />
   
   <cache name="passwordRetryCache"
          maxEntriesLocalHeap="2000"
          eternal="false"
          timeToIdleSeconds="1800"
          timeToLiveSeconds="0"
          overflowToDisk="false"
          statistics="true">
   </cache>
</ehcache>

以上是ehcache.xml文件的基礎配置,timeToLiveSeconds爲緩存的最大生存時間,timeToIdleSeconds爲緩存的最大空閒時間,當eternal爲false時ttl和tti才能夠生效。更多配置的含義你們能夠去網上查詢。

二、散列算法與加密算法

md5是本文會使用的散列算法,加密算法本文不會涉及。散列和加密本質上都是將一個Object變成一串無心義的字符串,不一樣點是通過散列的對象沒法復原,是一個單向的過程。例如,對密碼的加密一般就是使用散列算法,所以用戶若是忘記密碼只能經過修改而沒法獲取原始密碼。可是對於信息的加密則是正規的加密算法,通過加密的信息是能夠經過祕鑰解密和還原。

三、用戶註冊

請注意,雖然咱們一直在談論用戶登陸的安全性問題,可是說到用戶登陸首先就是用戶註冊。如何保證用戶註冊的信息不丟失,不泄密也是項目設計的重點。

public class PasswordHelper {
   private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
   private String algorithmName = "md5";
   private final int hashIterations = 2;

   public void encryptPassword(User user) {
       // User對象包含最基本的字段Username和Password
       user.setSalt(randomNumberGenerator.nextBytes().toHex());
       // 將用戶的註冊密碼通過散列算法替換成一個不可逆的新密碼保存進數據,散列過程使用了鹽
       String newPassword = new SimpleHash(algorithmName, user.getPassword(),
               ByteSource.Util.bytes(user.getCredentialsSalt()), hashIterations).toHex();
       user.setPassword(newPassword);
   }
}

若是你不清楚什麼叫加鹽能夠忽略散列的過程,只要明白存儲在數據庫中的密碼是根據戶註冊時填寫的密碼所產生的一個新字符串就能夠了。通過散列後的密碼替換用戶註冊時的密碼,而後將User保存進數據庫。剩下的工做就丟給UserService來處理。

那麼這樣就帶來了一個新問題,既然散列算法是沒法復原的,當用戶登陸的時候使用當初註冊時的密碼,咱們又應該如何判斷?答案就是須要對用戶密碼再次以相同的算法散列運算一次,再同數據庫中保存的字符串比較。

四、匹配

CredentialsMatcher是一個接口,功能就是用來匹配用戶登陸使用的令牌和數據庫中保存的用戶信息是否匹配。固然它的功能不只如此。本文要介紹的是這個接口的一個實現類:HashedCredentialsMatcher

public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
   // 聲明一個緩存接口,這個接口是Shiro緩存管理的一部分,它的具體實現能夠經過外部容器注入
   private Cache<String, AtomicInteger> passwordRetryCache;

   public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
       passwordRetryCache = cacheManager.getCache("passwordRetryCache");
   }

   @Override
   public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
       String username = (String) token.getPrincipal();
       AtomicInteger retryCount = passwordRetryCache.get(username);
       if (retryCount == null) {
           retryCount = new AtomicInteger(0);
           passwordRetryCache.put(username, retryCount);
       }
       // 自定義一個驗證過程:當用戶連續輸入密碼錯誤5次以上禁止用戶登陸一段時間
       if (retryCount.incrementAndGet() > 5) {
           throw new ExcessiveAttemptsException();
       }
       boolean match = super.doCredentialsMatch(token, info);
       if (match) {
           passwordRetryCache.remove(username);
       }
       return match;
   }
}

能夠看到,這個實現裏設計人員僅僅是增長了一個不容許連續錯誤登陸的判斷。真正匹配的過程仍是交給它的直接父類去完成。連續登陸錯誤的判斷依靠Ehcache緩存來實現。顯然match返回true爲匹配成功。

五、獲取用戶的角色和權限信息

說了這麼多才到咱們的重點Realm,若是你已經理解了Shiro對於用戶匹配和註冊加密的全過程,真正理解Realm的實現反而比較簡單。咱們還得回到上文說起的兩個很是相似的對象AuthorizationInfo和AuthenticationInfo。由於Realm就是提供這兩個對象的地方。

public class UserRealm extends AuthorizingRealm {
   // 用戶對應的角色信息與權限信息都保存在數據庫中,經過UserService獲取數據
   private UserService userService = new UserServiceImpl();

   /**
    * 提供用戶信息返回權限信息
    */
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
       String username = (String) principals.getPrimaryPrincipal();
       SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
       // 根據用戶名查詢當前用戶擁有的角色
       Set<Role> roles = userService.findRoles(username);
       Set<String> roleNames = new HashSet<String>();
       for (Role role : roles) {
           roleNames.add(role.getRole());
       }
       // 將角色名稱提供給info
       authorizationInfo.setRoles(roleNames);
       // 根據用戶名查詢當前用戶權限
       Set<Permission> permissions = userService.findPermissions(username);
       Set<String> permissionNames = new HashSet<String>();
       for (Permission permission : permissions) {
           permissionNames.add(permission.getPermission());
       }
       // 將權限名稱提供給info
       authorizationInfo.setStringPermissions(permissionNames);

       return authorizationInfo;
   }

   /**
    * 提供帳戶信息返回認證信息
    */
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
       String username = (String) token.getPrincipal();
       User user = userService.findByUsername(username);
       if (user == null) {
           // 用戶名不存在拋出異常
           throw new UnknownAccountException();
       }
       if (user.getLocked() == 0) {
           // 用戶被管理員鎖定拋出異常
           throw new LockedAccountException();
       }
       SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(),
               user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), getName());
       return authenticationInfo;
   }
}

根據Shiro的設計思路,用戶與角色以前的關係爲多對多,角色與權限之間的關係也是多對多。在數據庫中須要所以創建5張表,分別是:

用戶表(存儲用戶名,密碼,鹽等)

角色表(角色名稱,相關描述等)

權限表(權限名稱,相關描述等)

用戶-角色對應中間表(以用戶ID和角色ID做爲聯合主鍵)

角色-權限對應中間表(以角色ID和權限ID做爲聯合主鍵)

具體dao與service的實現本文不提供。總之結論就是,Shiro須要根據用戶名和密碼首先判斷登陸的用戶是否合法,而後再對合法用戶受權。而這個過程就是Realm的實現過程。

六、會話

用戶的一次登陸即爲一次會話,Shiro也能夠代替Tomcat等容器管理會話。目的是當用戶停留在某個頁面長時間無動做的時候,再次對任何連接的訪問都會被重定向到登陸頁面要求從新輸入用戶名和密碼而不須要程序員在Servlet中不停的判斷Session中是否包含User對象。

啓用Shiro會話管理的另外一個用途是能夠針對不一樣的模塊採起不一樣的會話處理。以淘寶爲例,用戶註冊淘寶之後能夠選擇記住用戶名和密碼。以後再次訪問就無需登錄。可是若是你要訪問支付寶或購物車等連接依然須要用戶確認身份。固然,Shiro也能夠建立使用容器提供的Session最爲實現。

3、與SpringMVC集成

有了註冊模塊和Realm模塊的支持,下面就是如何與SpringMVC集成開發。有過框架集成經驗的同窗必定知道,所謂的集成基本都是一堆xml文件的配置,Shiro也不例外。

一、配置前端過濾器

先說一個題外話,Filter是過濾器,interceptor是攔截器。前者基於回調函數實現,必須依靠容器支持。由於須要容器裝配好整條FilterChain並逐個調用。後者基於代理實現,屬於AOP的範疇。

若是但願在WEB環境中使用Shiro必須首先在web.xml文件中配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://java.sun.com/xml/ns/javaee"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
   id="WebApp_ID" version="3.0">
   <display-name>Shiro_Project</display-name>
   <welcome-file-list>
       <welcome-file>index.jsp</welcome-file>
   </welcome-file-list>
   <servlet>
       <servlet-name>SpringMVC</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <init-param>
           <param-name>contextConfigLocation</param-name>
           <param-value>classpath:springmvc.xml</param-value>
       </init-param>
       <load-on-startup>1</load-on-startup>
       <async-supported>true</async-supported>
   </servlet>
   <servlet-mapping>
       <servlet-name>SpringMVC</servlet-name>
       <url-pattern>/</url-pattern>
   </servlet-mapping>
   <listener>
       <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
   <listener>
       <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
   </listener>
   <context-param>
       <param-name>contextConfigLocation</param-name>
       <!-- 將Shiro的配置文件交給Spring監聽器初始化 -->
       <param-value>classpath:spring.xml,classpath:spring-shiro-web.xml</param-value>
   </context-param>
   <context-param>
       <param-name>log4jConfigLoaction</param-name>
       <param-value>classpath:log4j.properties</param-value>
   </context-param>
   <!-- shiro配置 開始 -->
   <filter>
       <filter-name>shiroFilter</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
       <async-supported>true</async-supported>
       <init-param>
           <param-name>targetFilterLifecycle</param-name>
           <param-value>true</param-value>
       </init-param>
   </filter>
   <filter-mapping>
       <filter-name>shiroFilter</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>
   <!-- shiro配置 結束 -->
</web-app>

熟悉Spring配置的同窗能夠重點看有綠字註釋的部分,這裏是使Shiro生效的關鍵。因爲項目經過Spring管理,所以全部的配置原則上都是交給Spring。DelegatingFilterProxy的功能是通知Spring將全部的Filter交給ShiroFilter管理。

接着在classpath路徑下配置spring-shiro-web.xml文件

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:mvc="http://www.springframework.org/schema/mvc"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
                       http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
                       http://www.springframework.org/schema/context
                       http://www.springframework.org/schema/context/spring-context-3.1.xsd
                       http://www.springframework.org/schema/mvc
                       http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">

   <!-- 緩存管理器 使用Ehcache實現 -->
   <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
       <property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
   </bean>

   <!-- 憑證匹配器 -->
   <bean id="credentialsMatcher" class="utils.RetryLimitHashedCredentialsMatcher">
       <constructor-arg ref="cacheManager" />
       <property name="hashAlgorithmName" value="md5" />
       <property name="hashIterations" value="2" />
       <property name="storedCredentialsHexEncoded" value="true" />
   </bean>

   <!-- Realm實現 -->
   <bean id="userRealm" class="utils.UserRealm">
       <property name="credentialsMatcher" ref="credentialsMatcher" />
   </bean>

   <!-- 安全管理器 -->
   <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
       <property name="realm" ref="userRealm" />
   </bean>

   <!-- Shiro的Web過濾器 -->
   <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
       <property name="securityManager" ref="securityManager" />
       <property name="loginUrl" value="/" />
       <property name="unauthorizedUrl" value="/" />
       <property name="filterChainDefinitions">
           <value>
               /authc/admin = roles[admin]
               /authc/** = authc
               /** = anon
           </value>
       </property>
   </bean>

   <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>

須要注意filterChainDefinitions過濾器中對於路徑的配置是有順序的,當找到匹配的條目以後容器不會再繼續尋找。所以帶有通配符的路徑要放在後面。三條配置的含義是:

 /authc/admin須要用戶有用admin權限

/authc/**用戶必須登陸才能訪問

/**其餘全部路徑任何人均可以訪問

說了這麼多,你們必定關心在Spring中引入Shiro以後到底如何編寫登陸代碼呢。

@Controller
public class LoginController {
   @Autowired
   private UserService userService;

   @RequestMapping("login")
   public ModelAndView login(@RequestParam("username") String username, @RequestParam("password") String password) {
       UsernamePasswordToken token = new UsernamePasswordToken(username, password);
       Subject subject = SecurityUtils.getSubject();
       try {
           subject.login(token);
       } catch (IncorrectCredentialsException ice) {
           // 捕獲密碼錯誤異常
           ModelAndView mv = new ModelAndView("error");
           mv.addObject("message", "password error!");
           return mv;
       } catch (UnknownAccountException uae) {
           // 捕獲未知用戶名異常
           ModelAndView mv = new ModelAndView("error");
           mv.addObject("message", "username error!");
           return mv;
       } catch (ExcessiveAttemptsException eae) {
           // 捕獲錯誤登陸過多的異常
           ModelAndView mv = new ModelAndView("error");
           mv.addObject("message", "times error");
           return mv;
       }
       User user = userService.findByUsername(username);
       subject.getSession().setAttribute("user", user);
       return new ModelAndView("success");
   }
}

登陸完成之後,當前用戶信息被保存進Session。這個Session是經過Shiro管理的會話對象,要獲取依然必須經過Shiro。傳統的Session中不存在User對象。

@Controller
@RequestMapping("authc")
public class AuthcController {
   // /authc/** = authc 任何經過表單登陸的用戶均可以訪問
   @RequestMapping("anyuser")
   public ModelAndView anyuser() {
       Subject subject = SecurityUtils.getSubject();
       User user = (User) subject.getSession().getAttribute("user");
       System.out.println(user);
       return new ModelAndView("inner");
   }

   // /authc/admin = user[admin] 只有具有admin角色的用戶才能夠訪問,不然請求將被重定向至登陸界面
   @RequestMapping("admin")
   public ModelAndView admin() {
       Subject subject = SecurityUtils.getSubject();
       User user = (User) subject.getSession().getAttribute("user");
       System.out.println(user);
       return new ModelAndView("inner");
   }
}

本篇內容大多總結自張開濤的《跟我學Shiro》原文地址:

http://jinnianshilongnian.iteye.com/blog/2018936

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

有熱門推薦????

酒後系列:被某廠面試官吊打後酒後整理的JVM乾貨

Google 6面,最終仍是掛了…

爲何單線程的Redis可以達到百萬級的QPS?

乾貨:一文讀懂客戶端請求是如何到達服務器的

 

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

相關文章
相關標籤/搜索