1. 程式人生 > >使用shiro攔截器鏈實現許可權管理

使用shiro攔截器鏈實現許可權管理

在開篇之前,先介紹一下shiro,那麼

什麼是shiro呢?

Apache Shiro(發音為“shee-roh”,日語“堡壘(Castle)”的意思)是一個強大易用的Java安全框架,提供了認證、授權、加密和會話管理功能,可為任何應用提供安全保障 - 從命令列應用、移動應用到大型網路及企業應用。

Shiro為解決下列問題(我喜歡稱它們為應用安全的四要素)提供了保護應用的API:
認證 - 使用者身份識別,常被稱為使用者“登入”;
授權 - 訪問控制;
密碼加密 - 保護或隱藏資料防止被偷窺;
會話管理 -每使用者相關的時間敏感的狀態。

Shiro還支援一些輔助特性,如Web應用安全、單元測試和多執行緒,它們的存在強化了上面提到的四個要素

由上面介紹可知,shiro可以實現認證及授權,這也是本篇的重點,使用shiro實現認證和授權。

使用shiro實現許可權管理

首先看一下我的專案結構:
這裡寫圖片描述
圖 1.專案結構

專案程式碼地址:GitHub地址

我們跳過無關緊要的內容,直接講最關建的shiro部分。

shiro

我們先看一下專案中和shiro有關的檔案,java檔案三個,分別是:MyRealm.javaShiroConfig.javaShiroManager.java
spring-mvc.xml檔案中,我們對shiro的bean註解開啟了配置

<context:component
-scan base-package="shiro"/>

web.xml檔案中,我們配置了shiro的過濾器

<!-- shiro過濾器定義 -->
  <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
      <!-- 該值預設為false,表示生命週期由SpringApplicationContext管理,設定為true則表示由ServletContainer管理 -->
<param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/a/*</url-pattern> </filter-mapping>

由此,我們開始整理shiro的實現脈路。

shiro攔截器鏈的實現原理

首先我們的請求通過web.xml的時候被shiro的攔截器過濾了一波,也就是

<!-- shiro過濾器定義 -->
  <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
      <!-- 該值預設為false,表示生命週期由SpringApplicationContext管理,設定為true則表示由ServletContainer管理 -->
      <param-name>targetFilterLifecycle</param-name>
      <param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/a/*</url-pattern>
  </filter-mapping>

在起效,這裡看一看到所有的/a開頭的url都需要經過shiro的過濾器。
spring-mvc.xml中的註解開啟配置就不說了,主要是shiro的java程式碼中涉及了比較多的註解配置,所以這條配置是必須的

<context:component-scan base-package="shiro"/>


接下來,我們看shiro的主體部分,也是本篇的重點
首先是MyRealm.java

package shiro;

import model.Role;
import model.User;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import service.RoleService;
import service.UserService;
import util.SplitUtil;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Created by yubotao on 2017/12/03.
 */
@SuppressWarnings("ALL")
@Component
public class MyRealm extends AuthorizingRealm{
    private static final Log log = LogFactory.getLog(MyRealm.class);
    public MyRealm(){
        super(new AllowAllCredentialsMatcher());
        setAuthenticationTokenClass(UsernamePasswordToken.class);
        //FIXME:暫時禁用Cache
        setCachingEnabled(false);
    }

    @Autowired
    UserService userService;
    @Autowired
    RoleService roleService;

    //驗證時呼叫
    //此方法呼叫subject.hasRole("admin")或subject.isPermitted("admin");
    //自己去呼叫這個是否有什麼角色或有什麼許可權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        User user1 = null;

        /*新建角色與許可權的set*/
        Set<String> shiroPermissions = new HashSet<>();
        Set<String> roleSet = new HashSet<>();
        try{
            /*1.通過userName獲取userId*/
            user1 = userService.getUserByName(user.getName());
            if(user1 != null){
                Role role = roleService.getRoleById(user.getRole());
                log.info("role permission : " + role.getPermission());
                SplitUtil splitUtil = new SplitUtil();
                List<Integer> permissionList = splitUtil.stringToIntegerList(role.getPermission());
                log.info("permisson list : " + permissionList);
                for (int i = 0; i < permissionList.size(); i++){
                    shiroPermissions.add(permissionList.get(i).toString());
                }
                authorizationInfo.setStringPermissions(shiroPermissions);
                log.info("shiroPermissions : " + shiroPermissions);
                return authorizationInfo;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    //登陸時呼叫
    //呼叫Subject currentUser = SecurityUtils.getSubject();
    //    currentUser.login(token);
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token){
        String username = (String) token.getPrincipal();
        log.info("username : " + username);
        User user = null;
        String password = null;

        try{
            /*通過username獲取User*/
            user = userService.getUserByName(username);
            log.info("user : " + user);
            password = new String((char[]) token.getCredentials());
            log.info("password : " + password);
            //賬號不存在
            if(user == null){
                throw new UnknownAccountException("賬號不正確");
            }
            //密碼錯誤
            //簡單起見,沒有加密,應該加上
            if(!password.equals(user.getPassword())){
                throw new UnknownAccountException("密碼錯誤");
            }
        }catch (Exception e){
            throw new AuthenticationException();
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,password,getName());

        return info;
    }

}

在講解該部分程式碼前,我們首先介紹一下Realm這個shiro的元件。

Realm

此部分內容參考該blog:Realm的介紹

Realm 是一個能夠訪問應用程式特定的安全資料(如使用者、角色及許可權)的元件。

Realm 通常和資料來源是一對一的對應關係,如關係資料庫,LDAP 目錄,檔案系統,或其他類似資源。Realm
實質上就是一個特定安全的DAO。

因為這些資料來源大多通常儲存身份驗證資料(如密碼的憑證)以及授權資料(如角色或許可權),每個Realm能夠執行身份驗證和授權操作。

由這篇blog我們可以整理出如下資訊:
1.shiro的認證最後是交給Realm的,呼叫的是doGetAuthenticationInfo()方法;
2.如果驗證通過,會返回一個非空的AuthenticationInfo例項來代表來自該資料來源的Subject賬戶資訊。

為了便於理解,這裡首先介紹倆個詞:
Authorization —— 授權
Authentication —— 認證
然後我們看一下我們自定義的MyRealm的內容,首先是建構函式

 public MyRealm(){
        super(new AllowAllCredentialsMatcher());
        setAuthenticationTokenClass(UsernamePasswordToken.class);
        //FIXME:暫時禁用Cache
        setCachingEnabled(false);
    }

這裡借用一張圖:該圖源自對於shiro有比較深入的介紹
這裡寫圖片描述
這張圖展示了shiro的認證匹配介面結構圖,這裡面就有我們提到的AllowAllCredentialsMatcher,它的含義是:只要該使用者名稱存在即可,不用去驗證密碼是否匹配。
然後我們在建構函式中將Cache設為暫不使用。

接下來是Realm中的認證內容

@Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
        User user = (User) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        User user1 = null;

        /*新建角色與許可權的set*/
        Set<String> shiroPermissions = new HashSet<>();
        Set<String> roleSet = new HashSet<>();
        try{
            /*1.通過userName獲取userId*/
            user1 = userService.getUserByName(user.getName());
            if(user1 != null){
                Role role = roleService.getRoleById(user.getRole());
                log.info("role permission : " + role.getPermission());
                SplitUtil splitUtil = new SplitUtil();
                List<Integer> permissionList = splitUtil.stringToIntegerList(role.getPermission());
                log.info("permisson list : " + permissionList);
                for (int i = 0; i < permissionList.size(); i++){
                    shiroPermissions.add(permissionList.get(i).toString());
                }
                authorizationInfo.setStringPermissions(shiroPermissions);
                log.info("shiroPermissions : " + shiroPermissions);
                return authorizationInfo;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

這裡可以看到,該程式碼塊的主要邏輯就是,獲取使用者對應角色所擁有的許可權,然後將這些許可權放到SimpleAuthorizationInfo的許可權認證書中,這個地方是為了之後的shiro許可權認證所做的鋪墊。
這裡使用了PrincipalCollection,這個介面是幹什麼用的呢,經過查閱,發現該值是唯一的,在所有使用者帳戶中唯一的字串使用者名稱。所以程式碼中的

User user = (User) principals.getPrimaryPrincipal();

使用它來獲取當前subject中的唯一使用者。
到此,我們已經完成了授權,接下來就就是認證了。

 @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token){
        String username = (String) token.getPrincipal();
        log.info("username : " + username);
        User user = null;
        String password = null;

        try{
            /*通過username獲取User*/
            user = userService.getUserByName(username);
            log.info("user : " + user);
            password = new String((char[]) token.getCredentials());
            log.info("password : " + password);
            //賬號不存在
            if(user == null){
                throw new UnknownAccountException("賬號不正確");
            }
            //密碼錯誤
            //簡單起見,沒有加密,應該加上
            if(!password.equals(user.getPassword())){
                throw new UnknownAccountException("密碼錯誤");
            }
        }catch (Exception e){
            throw new AuthenticationException();
        }

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,password,getName());

        return info;
    }

這部分的程式碼塊主要邏輯是:通過token.getPrincipal()方法獲取到Token中的使用者名稱,然後根據使用者名稱查詢資料庫,找出資料庫中記錄並進行對比,如果確實有該使用者,並且帳密無誤,那麼就會通過

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,password,getName());

生成一個認證資訊。
到此,我們的自定義Realm內容就完畢了。

ShiroManager

首先看一下ShiroManager.java的程式碼:

package shiro;

/**
 * Created by yubotao on 2017/12/03.
 */

import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;

/**
 * shiro Config Manager
 * */
public class ShiroManager {

    /**
     * 保證了實現shiro內部的lifecycle函式的bean執行
     * */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){return new LifecycleBeanPostProcessor();}

    @Bean(name = "defaultAdvisorAutoProxyCreator")
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(securityManager);
        return aasa;
    }

}

首先這個ShiroManager的名字就暴露出,這部分是管理shiro的一些bean的,那麼我們先來看第一個程式碼塊:

@Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){return new LifecycleBeanPostProcessor();}

檢視shiro的官方文件我們發現,這個LifecycleBeanPostProcessor是shiro和Spring整合的一個生命週期bean,自動使用init和destroy,無需我們進行管理;
接下來第二個程式碼塊

 @Bean(name = "defaultAdvisorAutoProxyCreator")
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

這裡關鍵的類時DefaultAdvisorAutoProxyCreator,該類時spring-aop包下的類,具體功能為:自動建立代理,它會搜尋所有的Advisor,並自動將Advisor應用到符合的PointCuts(切點)物件上。
通俗講就是我們生成了一個可以自動幫我們實現aop代理的bean,並且對相應的切點使用已有的Advisor。
接下來我們看第三個程式碼塊

@Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(securityManager);
        return aasa;
    }

這個類可以在shiro官方文件中檢視,我們通過檢視可以看到這個AuthorizationAttributeSourceAdvisor就是我們剛才提到的Advisor,並且它實際上使用了AopAllianceAnnotationsAuthorizingMethodInterceptor,而該類的官方文件解釋為:

Allows Shiro Annotations to work in any AOP Alliance specific implementation environment (for example, Spring).

使shiro註解能夠在任意的特定AOP聯合環境工作,比如我們現在使用的Spring。
至此,ShiroManager的講解也完畢了。

ShiroConfig

這裡是整個shiro攔截鏈的關鍵。
ShiroConfig.java

package shiro;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Import;

import java.util.LinkedHashMap;

/**
 * Created by yubotao on 2017/12/03.
 */
@Configuration
@Import(ShiroManager.class)
public class ShiroConfig {
    private static final Log log = LogFactory.getLog(ShiroConfig.class);

    @Bean(name = "realm")
    @DependsOn("lifecycleBeanPostProcessor")
    public Realm realm(){return new MyRealm();}

    /**
     * 使用者授權資訊Cache
     * */
    @Bean(name = "shiroCacheManager")
    public CacheManager cacheManager(){return new MemoryConstrainedCacheManager();}

    @Bean(name = "securityManager")
    public DefaultSecurityManager securityManager(){
        //這裡注意,需要實現DefaultWebSecurityManager
        DefaultSecurityManager sm = new DefaultWebSecurityManager();
        sm.setCacheManager(cacheManager());
        return sm;
    }

    /**
     * shiro核心,攔截器鏈,順序執行
     * */
    @Bean(name = "shiroFilter")
    @DependsOn("securityManager")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultSecurityManager securityManager,Realm realm){
        securityManager.setRealm(realm);

        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl("/a/login/fail");//未登入跳轉
        shiroFilter.setUnauthorizedUrl("/a/login/fail");//未授權跳轉
        LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/a/u/first","perms[1]");
        log.info("第一鏈");
        filterChainDefinitionMap.put("/a/u/second","perms[2]");
        log.info("第二鏈");
        filterChainDefinitionMap.put("/a/u/third","perms[3]");
        log.info("第三鏈");

        filterChainDefinitionMap.put("/a//**","anon");

        log.info("執行順序 : " + filterChainDefinitionMap);

        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilter;
    }


}

和之前一樣,我們還是逐個擊破。
首先這裡使用了註解@Import(ShiroManager.class),也就是引用了我們剛才配置的ShiroManager,也就是在這個類裡,完成了所有shiro配置的大匯合。
先看第一個程式碼塊

@Bean(name = "realm")
    @DependsOn("lifecycleBeanPostProcessor")
    public Realm realm(){return new MyRealm();}

這裡我們依賴了剛才配置的生命週期bean,並且配置出了我們自定義的MyRealm。
然後是第二個程式碼塊

/**
     * 使用者授權資訊Cache
     * */
    @Bean(name = "shiroCacheManager")
    public CacheManager cacheManager(){return new MemoryConstrainedCacheManager();}

這裡我們將使用使用者授權資訊的Cache。
第三個程式碼塊

 @Bean(name = "securityManager")
    public DefaultSecurityManager securityManager(){
        //這裡注意,需要實現DefaultWebSecurityManager
        DefaultSecurityManager sm = new DefaultWebSecurityManager();
        sm.setCacheManager(cacheManager());
        return sm;
    }

這裡需要注意的是需要實現DefaultWebSecurityManager,否則會在使用shiro攔截鏈的時候報錯,一開始的時候我在這裡沒有注意,少寫了個Web,然後就出錯了,一定要特別注意。
那麼為什麼這個類這麼重要呢?
我們可以參考該文章:對於DefaultWebSecurityManager的介紹
大概解釋如下

DefaultWebSecurityManager類主要定義了設定subjectDao,獲取會話模式,設定會話模式,設定會話管理器,是否是http會話模式等操作,它繼承了DefaultSecurityManager類,實現了WebSecurityManager介面

也就是說,如果沒有使用這個類,我們無法驗證會話中登陸的使用者,也就無法進行登陸、校驗、登出等操作;主要和Subject有關。
接下來就是本文的核心,shiro的攔截器鏈:

/**
     * shiro核心,攔截器鏈,順序執行
     * */
    @Bean(name = "shiroFilter")
    @DependsOn("securityManager")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultSecurityManager securityManager,Realm realm){
        securityManager.setRealm(realm);

        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl("/a/login/fail");//未登入跳轉
        shiroFilter.setUnauthorizedUrl("/a/login/fail");//未授權跳轉
        LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/a/u/first","perms[1]");
        log.info("第一鏈");
        filterChainDefinitionMap.put("/a/u/second","perms[2]");
        log.info("第二鏈");
        filterChainDefinitionMap.put("/a/u/third","perms[3]");
        log.info("第三鏈");

        filterChainDefinitionMap.put("/a//**","anon");

        log.info("執行順序 : " + filterChainDefinitionMap);

        shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilter;
    }

相信註解已經很清楚了,這裡的邏輯就是:新建shiro的攔截鏈,然後設定未登入和未授權的兩種跳轉,這裡為了簡便,我將兩種跳轉設定為一個了;然後向攔截鏈裡新增規則,最後返回shiro的攔截鏈資訊。

成果展示

經過如上的講解,我們檢驗一下效果。
當我們處於未登入狀態的時候,請求許可權url會跳轉至之前設定的失敗頁面
這裡寫圖片描述

當我們登陸後有對應許可權時
這裡寫圖片描述

當我們登陸後無許可權訪問時
這裡寫圖片描述

至此整個shiro的攔截器鏈實現許可權管理講解結束,如有疏漏或錯誤,還請不吝賜教。