1. 程式人生 > >shiro登入認證系統的學習

shiro登入認證系統的學習

1、認識shiro架構

1.1外部瞭解:官方給出的架構圖

       

流程:應用程式(Application)請求進行驗證,將需要進行驗證的賬號密碼儲存在supject中,suoject將這些內容提交給SecurityManager。

【注】1)SecurityManager是shiro的核心,所有的認證,授權都要經過她的手。相當於 Spring MVC 中的 DispatcherServlet,是實現任務分配的核心。但是,注意,SecurityManager 僅僅實現任務的分配,它本身並不進行認證的工作,認證工作都是交給下 “下手” 去做的。

2)realm是一個數據源。在進行賬號密碼驗證時,我們資料庫中或者配置檔案裡必須存在正確的使用者名稱和密碼,realm需要做的就是獲取這些使用者名稱和密碼,和shiro提供的使用者名稱和密碼進行比較,然後將結果返回給SecurityManager。

1.2內部瞭解:官網給的一張架構圖

 由此可以看出shiro 是由 SecurityManager 支配的,可以根據不同的請求將任務分配給不同的模組實現。

1.3示例(使用者登入):

1)使用使用者的登入資訊建立令牌(token可以理解為使用者令牌,登入的過程被抽象為Shiro驗證令牌是否具有合法身份以及相關許可權。)

UsernamePasswordToken token = new UsernamePasswordToken(username, password);

2)執行登入動作

SecurityUtils.setSecurityManager(securityManager); // 注入SecurityManager
Subject subject = SecurityUtils.getSubject(); // 獲取Subject單例物件
subject.login(token); // 登陸

【注】(1)SecurityUtils物件,本質上就是一個工廠類似Spring中的ApplicationContext。

           (2)Subject中文翻譯:專案。它是你目前所設計的需要通過Shiro保護的專案的一個抽象概念。通過令牌(token)與專案(subject)的登陸(login)關係,Shiro保證了專案整體的安全。

3)判斷使用者

      Shiro本身無法知道所持有令牌的使用者是否合法,因為除了專案的設計人員恐怕誰都無法得知。因此Realm是整個框架中為數不多的必須由設計者自行實現的模組,當然Shiro提供了多種實現的途徑,本文只介紹最常見也最重要的一種實現方式——資料庫查詢。

4)AuthorizationInfo,AuthenticationInfo(兩個長得極其相似的英文單詞)    

       在解釋它們前首先必須要描述一下Shiro對於安全使用者的界定:和大多數作業系統一樣。使用者具有角色和許可權兩種最基本的屬性。例如,我的Windows登陸名稱是learnhow,它的角色是administrator,而administrator具有所有系統許可權。這樣learnhow自然就擁有了所有系統許可權。那麼其他人需要登入我的電腦怎麼辦,我可以開放一個guest角色,任何無法提供正確使用者名稱與密碼的未知使用者都可以通過guest來登入,而系統對於guest角色開放的許可權極其有限。

        同理,Shiro對使用者的約束也採用了這樣的方式。AuthenticationInfo代表了使用者的角色資訊集合,AuthorizationInfo代表了角色的許可權資訊集合。如此一來,當設計人員對專案中的某一個url路徑設定了只允許某個角色或具有某種許可權才可以訪問的控制約束的時候,Shiro就可以通過以上兩個物件來判斷。說到這裡,大家可能還比較困惑。先不要著急,繼續往後看就自然會明白了。

5)實現Realm

      關鍵詞:快取機制、雜湊演算法、加密演算法

      (1)快取機制

        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才可以生效。

        (2)雜湊演算法和加密演算法

               md5是本文會使用的雜湊演算法,加密演算法本文不會涉及。雜湊和加密本質上都是將一個Object變成一串無意義的字串,不同點是經過雜湊的物件無法復原,是一個單向的過程。例如,對密碼的加密通常就是使用雜湊演算法,因此使用者如果忘記密碼只能通過修改而無法獲取原始密碼。但是對於資訊的加密則是正規的加密演算法,經過加密的資訊是可以通過祕鑰解密和還原。

        (3)使用者註冊(保證使用者註冊的資訊不丟失,不洩密也是專案設計的重點。)

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);
    }
}

    【注】因為雜湊演算法無法復原,所以驗證時,需要將使用者輸入的密碼進行相同的演算法,再與資料庫中的對比

       (4)匹配

               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為匹配成功

     (5)獲取使用者的角色和許可權資訊

            Realm一個提供AuthorizationInfo和AuthenticationInfo的地方。

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作為聯合主鍵)。總之Shiro需要根據使用者名稱和密碼首先判斷登入的使用者是否合法,然後再對合法使用者授權。而這個過程就是Realm的實現過程。

(6)會話

              使用者的一次登入即為一次會話,Shiro也可以代替Tomcat等容器管理會話。目的是當用戶停留在某個頁面長時間無動作的時候,再次對任何連結的訪問都會被重定向到登入頁面要求重新輸入使用者名稱和密碼而不需要程式設計師在Servlet中不停的判斷Session中是否包含User物件。啟用Shiro會話管理的另一個用途是可以針對不同的模組採取不同的會話處理。以淘寶為例,使用者註冊淘寶以後可以選擇記住使用者名稱和密碼。之後再次訪問就無需登陸。但是如果你要訪問支付寶或購物車等連結依然需要使用者確認身份。當然,Shiro也可以建立使用容器提供的Session最為實現。

2、與sprinigMVC整合

1)配置前端過濾器

<?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/**使用者必須登入才能訪問、/**其他所有路徑任何人都可以訪問。

2)程式碼

@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");
    }
}