Shiro的認證和權限控制

權限控制的方式

從類別上分,有兩大類:
- 認證:你是誰?–識別用戶身份。
- 授權:你能做什麼?–限制用戶使用的功能。
權限的控制級別

從控制級別(模型)上分:
- URL級別-粗粒度
- 方法級別-細粒度
- 頁面級別-自定義標籤(顯示)
- 數據級別-最細化的(數據)
URL級別的權限控制-粗粒度

在web.xml中配置一個過濾器filter,在過濾器中,對請求的地址進行解析,字符串截取:
url.substring()…把上下文前面的路徑都截取掉,剩下user_login.action。
過濾器代碼:
以通過查詢數據庫,來判斷,當前登錄用戶,是否可以訪問user_login.action。
url級別控制,每次請求過程中只控制一次 ,相比方法級別權限控制 是粗粒度的 !URL級別權限控制,基於Filter實現。
方法級別的權限控制-細粒度

aop面向切面的編程,在方法執行之前,進行權限判斷,如果沒有權限,拋出異常,終止方法的繼續運行。
自定義註解 在需要權限控制方法上, 添加需要的權限信息
代理 (Spring AOP ),在目標方法運行時 進行增強 ,通過反射技術獲取目標方法上註解中權限 , 查詢數據庫獲取當前登陸用戶具有權限,進行比較。
相比URL級別權限控制, 可以控制到服務器端執行的每個方法,一次請求中可以控制多次。
頁面(顯示)級別的權限控制-自定義標籤

頁面顯示的權限控制,通常是通過 自定義標籤來實現
數據級別的權限控制

在每條數據上增加一個字段,該字段記錄了權限的值。數據和權限綁定。
代碼,你在查詢數據的時候,需要去權限和用戶對應表中,通過當前登錄用戶的條件,查詢出你的數據權限。然後再將數據權限作爲一個條件,放到業務表中進行查詢。從而限制了數據的訪問。
權限系統的數據表設計

    資源:用戶要訪問的目標,通常是服務中的程序或文件
    權限:用戶具有訪問某資源的能力
    角色:權限的集合,爲了方便給用戶授權。
    用戶:訪問系統的’人’。

表對象實體:
- 用戶(User)表:訪問系統的用戶,比如用戶登錄要用
- 權限(Function)表:系統某個功能允許訪問而對應的權限
- 角色(Role)表:角色是權限的集合(權限組),方便用戶授權。

表對象之間的關係:
- 用戶和角色關係表:一個用戶對應N個角色,一個角色可以授予N個用戶—》多對多關係
- 角色和權限關係表:一個角色包含N個權限,一個權限可以屬於N個角色—》多對多關係

完整的權限相關表:
URL級別權限控制包含:資源表、權限表、角色表、用戶表,以及相關關係(都是多對多),共7張表。
方法級別的權限控制包含:功能權限、角色、用戶,以及相關關係(都是多對多),共5張表。

但Apache Shiro框架支持的URL級別權限控制,是將資源和資源權限對應關係配置到了配置文件中,不需要表的支撐,只需要5張表了。
Apache Shiro權限控制

Apache Shiro 可以不依賴任何技術使用, 可以直接和web整合,通常在企業中和Spring 結合使用。

Authentication: 認證 — 用戶登錄
Authorization : 授權 —- 功能權限管理
通過引入Maven座標導入shiro

官方建議:不推薦直接引入shiro-all,依賴比較多,原因怕有jar衝突。官方推薦根據需要單獨導入jar。
Shiro基本原理

Shiro的框架的體系結構:

Shiro權限控制流程的原理:

    應用代碼 —- 調用Subject (shiro的Subject 就代表當前登陸用戶) 控制權限 —- Subject 在shiro框架內部 調用 Shiro SecurityManager 安全管理器 —– 安全管理器調用 Realm (程序和安全數據連接器 )。
    Subject要進行任何操作,都必須要調用安全管理器(對我們來說是自動的)。
    而安全管理器會調用指定的Realms對象,來連接安全數據。
    Realms用來編寫安全代碼邏輯和訪問安全數據,是連接程序和安全數據的橋樑。

URL級別的權限控制
配置整合和url級別認證

配置過濾器web.xml:放在struts的前端控制器之前配置,但放在openEntitymanage之後。

    <!-- shiro權限過濾器 -->
        <filter>
            <!-- 這裏的 filter-name 要和 spring 的 applicationContext-shiro.xml 裏的 org.apache.shiro.spring.web.ShiroFilterFactoryBean
                的 bean name 相同 -->
            <filter-name>shiroSecurityFilter</filter-name>
            <!-- spring的代理過濾器類:以前的過濾器 -->
            <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>shiroSecurityFilter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>

配置ApplicationContext.xml:(shiro權限控制過濾器+ shiro安全管理器)

        <!-- shiro權限控制過濾器bean -->
        <bean id="shiroSecurityFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
            <!-- shiro 的核心安全接口 -->
            <property name="securityManager" ref="securityManager" />
            <!-- 要求登錄時的鏈接 -->
            <property name="loginUrl" value="/login.jsp" />
            <!-- 登陸成功後要跳轉的連接 -->
            <property name="successUrl" value="/index.jsp" />
            <!-- 未授權時要跳轉的連接,權限不足的跳轉路徑 -->
            <property name="unauthorizedUrl" value="/unauthorized.jsp" />
            <!-- shiro 連接約束配置(URL級別的權限控制),即URL和filter的關係,URL控制規則:路徑=規則名 -->
            <property name="filterChainDefinitions">
                <value>
                    <!--按需求配置-->
                    /login.jsp = anon
                    /validatecode.jsp = anon
                    /js/** = anon
                    /css/** = anon
                    /images/** = anon
                    /user_login.action* = anon
                    /page_base_staff.action = anon
                    /page_base_region.action = perms["user"]
                    /page_base_subarea.action = roles["operator"]
                    /** = authc
                </value>
            </property>
        </bean>
        <!-- shiro安全管理器 -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <!-- 注入 Realm連接安全數據-->
        </bean>

配置shiroFilter 其實是一個過濾器鏈,含有10個Filter(校驗功能)。

常用:
認證
- anon不用認證(登錄)就能訪問(單詞注意大小寫)
- authc: 需要認證(登錄)才能使用,例如/admins/user/**=authc,沒有參數。
授權:
- perms:需要擁有某權限才能使用,如具體允許的權限:/page_base_region.action =perms[“user”],如果要訪問該action,當前登錄用戶必須擁有user名字的權限。
- roles:需要擁有某角色才能使用,如具體允許的角色:/page_base_subarea.action = roles[“operator”]如果要訪問該action,當前用戶必須擁有operator權限。
用戶認證(登錄)—自定義Realm

Shiro實現登錄邏輯
用戶輸入用戶名和密碼 —- 應用程序調用Subject的login方法 —- Subject 調用SecurityManager的方法 —- SecurityManager 調用Realm的認證方法 —- 認證方法根據登錄用戶名查詢密碼 ,返回用戶的密碼 —- SecurityManager 比較用戶輸入的密碼和真實密碼是否一致 。
編寫Shiro的認證登錄邏輯

        @Action(value="user_login",results={@Result(name=SUCCESS,type="redirect",location="/index.jsp"),@Result(name=LOGIN,location="/login.jsp")})
        @InputConfig(resultName="login")
        public String login() throws Exception {        
            //shrio:登陸邏輯
            //獲取認證對象的包裝對象
            Subject subject = SecurityUtils.getSubject();

            //獲取一個認證的令牌:
            //直接獲取頁面的用戶和密碼進行校驗
            AuthenticationToken authenticationToken = new UsernamePasswordToken(model.getUsername(),MD5Utils.md5(model.getPassword()));
            //認證過程
            try {
                // 如果成功,就不拋出異常,會自動將用戶放入session的一個屬性
                subject.login(authenticationToken);
                //成功,返回首頁
                return SUCCESS;
            }catch(UnknownAccountException e){
                //用戶名錯誤
                addActionError(getText("UserAction.usernamenotfound"));
                //返回登陸頁面
                return LOGIN;
            }catch (IncorrectCredentialsException e) {
                //密碼錯誤
                addActionError(getText("UserAction.passwordinvalid"));
                //返回登陸頁面
                return LOGIN;
            }
            catch (AuthenticationException e) {
                //認證失敗
                e.printStackTrace();
                //頁面上進行提示
                addActionError(getText("UserAction.loginfail"));
                //返回登陸頁面
                return LOGIN;
            }
        }

   

編寫Realm,給SecurityManager提供

JdbcRealm和jndiLdapRealm,直接連接jdbc或jndi或ldap。
相當於dao和reaml整合了,能直接讀取數據庫,邏輯代碼都實現好了。

        /**
         *  實現認證和授權功能
         *自定義的realm,作用從數據庫查詢數據,並返回數據庫認證的信息
         */
        @Component("bosRealm")
        public class BosRealm extends AuthorizingRealm{

            //注入ehcache的緩存區域
            @Value("BosShiroCache")//注入緩存具體對象的名字,該名字在ehcache.xml中配置的
            public void setSuperAuthenticationCacheName(String authenticationCacheName){
                super.setAuthenticationCacheName(authenticationCacheName);
            }

            //注入service
            @Autowired
            private UserService userService;

            //注入角色dao
            @Autowired
            private RoleDao roleDao;

            //注入功能的dao
            @Autowired
            private FunctionDao functionDao;

            //授權方法:獲取用戶的權限信息
            //授權:回調方法
            //如果返回null,說明沒有權限,shiro會自動跳到<property name="unauthorizedUrl" value="/unauthorized.jsp" />
            //如果不返回null,根據配置/page_base_subarea.action = roles["weihu"],去自動匹配
            //給授權提供數據的
            @Override
            protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

                //給當前用戶授權的權限(功能權限、角色)
                SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
                //兩種方式:
                //方式1:工具類來獲取(首長-)
        //      User user=(User)SecurityUtils.getSubject().getPrincipal();
                //方式2:通過參數獲取首長(推薦)
                User user = (User) principals.getPrimaryPrincipal();

                //實際:需要根據當前用戶的角色和功能權限來構建一個授權信息對象,交給安全管理器

                if (user.getUsername().equals("admin")) {
                    //如果是超級管理員
                    //查詢出所有的角色,給認證信息對象
                    List<Role> roleList = roleDao.findAll();
                    for (Role role : roleList) {
                        authorizationInfo.addRole(role.getCode());
                    }
                    //查詢出所有的功能權限,給認證對象
                    List<Function> functionList = functionDao.findAll();
                    for (Function function : functionList) {
                        authorizationInfo.addStringPermission(function.getCode());
                    }
                } else {
                    //如果是普通用戶
                    List<Role> roleList = roleDao.findByUsers(user);
                    for (Role role : roleList) {
                        authorizationInfo.addRole(role.getCode());
                        //導航查詢,獲取某角色的擁有的功能權限
                        Set<Function> functions = role.getFunctions();
                        for (Function function : functions) {
                            authorizationInfo.addStringPermission(function.getCode());
                        }
                    }
                }

                return authorizationInfo;//將授權信息交給安全管理器接口。
            }

            //認證:回調,認證管理器會將認證令牌放到這裏(action層的令牌AuthenticationToken)
            //發現如果返回null,拋出用戶不存在的異常UnknownAccountException
            @Override
            protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
                //用戶名密碼令牌(action傳過來)
                UsernamePasswordToken upToken = (UsernamePasswordToken) token;
                //調用業務層來查詢(根據用戶名來查詢用戶,無需密碼)
                User user = userService.findByUsername(upToken.getUsername());
                //判斷用戶是否存在
                if (user == null) {
                    //用戶不存在
                    return null;//拋出異常
                } else {
                    //用戶名存在
                    //參數1:用戶對象,將來要放入session,數據庫查詢出來的用戶
                    //參數2:憑證(密碼):密碼校驗:校驗的動作交給shiro
                    //參數3:當前使用的Realm在Spring容器中的名字(bean的名字,自動在spring容器中尋找)
                    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), super.getName());
                    return authenticationInfo;//密碼校驗失敗,會自動拋出IncorrectCredentialsException
                }
            }

        }

  

ApplicatonContext.xml:

    <!-- service需要spring掃描 -->
    <context:component-scan base-package="cn.aric.bos.service,cn.aric.bos.web,cn.aric.bos.auth.realm" />

    <!-- shiro安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <!-- 注入 Realm連接安全數據-->
        <property name="realm" ref="bosRealm"></property>
        <!-- 注入shiro的緩存管理器 -->
        <property name="cacheManager" ref="shiroCacheManager"/>
    </bean>

  

用戶認證(退出)以及修改密碼

        /**
         * 用戶退出登錄
         * @return
         * @throws Exception
         */
        @Action(value="user_logout",results={@Result(name=LOGIN,type="redirect",location="/login.jsp")})
        public String logout() throws Exception {       
            //shiro退出
            Subject subject = SecurityUtils.getSubject();
            subject.logout();
            //跳轉登陸頁面
            return LOGIN;
        }

        /**
         * 用戶修改密碼
         * @return
         * @throws Exception
         */
    //  @Action(value="user_editPassword",results={@Result(name=JSON,type=JSON)})
        @Action("user_editPassword")
        public String editPassword() throws Exception {
            //獲取Principal就是獲取當前用戶
            User loginUser = (User) SecurityUtils.getSubject().getPrincipal();
            model.setId(loginUser.getId());

            //頁面結果
            HashMap<String,Object> resultMap = new HashMap<String,Object>();
            try {
                //調用service進行修改密碼
                userService.updateUserPassword(model);
                //修改成功
                resultMap.put("result", true);
            } catch (Exception e) {
                e.printStackTrace();
                //修改失敗
                resultMap.put("result", false);
            }
            //將結果壓入棧頂
            ActionContext.getContext().getValueStack().push(resultMap);
            //轉換爲json
            return JSON;
        }

   

用戶授權(授權)—自定義Ream

數據庫數據添加,applicationContext.xml配置

        <property name="filterChainDefinitions">
            <value>
                /login.jsp = anon
                /validatecode.jsp = anon
                /js/** = anon
                /css/** = anon
                /images/** = anon
                /user_login.action = anon
                /page_base_staff.action = anon
                /page_base_region.action = perms["region"]
                /page_base_subarea.action = roles["weihu"]
                /page_qupai_noticebill_add.action = perms["noticebill"]
                /page_qupai_quickworkorder.action = roles["kefu"]
                /** = authc
            </value>
        </property>

   

代碼在上面的BosRealm的中,protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
RoleDao省略。
方法級別的權限控制
啓用Shiro註解

需要 Shiro 的 Spring AOP 集成來掃描合適的註解類以及執行必要的安全邏輯。
ApplicationContext.xml

        <!-- 開啓權限控制的註解功能並且配置aop -->
        <!-- 後處理器:通過動態代理在某bean實例化的前增強。:自己去找權限註解 -->
        <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
        <!-- 切面自動代理:相當於以前的AOP標籤配置
        advisor:切面 advice:通知
        -->
        <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
        depends-on="lifecycleBeanPostProcessor">

        </bean>

        <!-- Advisor切面配置:授權屬性的切面 -->
        <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
            <!-- 注入安全管理器 -->
            <property name="securityManager" ref="securityManager"/>
        </bean>

  
   

在需要權限控制的目標方法上面使用shiro的註解:
@RequiresAuthentication 需要用戶登錄
subject.isAuthenticated() 必須返回true
@ RequiresUser
subject.isAuthenticated() 返回true 或者subject.isRemembered() 返回true
“Remember Me”服務:
認證機制 基於 session
被記憶機制 基於 cookie (subject.isAuthenticated() 返回 false )

@ RequiresGuest 與 @RequiresUser 相反,不能認證也不能被記憶。
@ RequiresRoles 需要角色
@RequiresPermissions 需要權限
異常
動態代理異常

解決方案:
配置ApplicationContext.xml,設置代理爲cglib代理(對目標類代理)

        <!-- 切面自動代理:相當於以前的AOP標籤配置 -->
        <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
        depends-on="lifecycleBeanPostProcessor" >
            <!-- 設置aop的代理使用CGLIB代理 -->
            <property name="proxyTargetClass" value="true"/>
        </bean>

  

方案二:

<aop:config proxy-target-class="true" />

    1

類型轉換異常

解決方案:遞歸向上尋找泛型的類型。

    //遞歸向上 查找
        Class actionClass =this.getClass();
        //向父類遞歸尋找泛型
        while(true){
            //得到帶有泛型的類型,如BaseAction<Userinfo>
            Type type = actionClass.getGenericSuperclass();

            if(type instanceof ParameterizedType){
                //轉換爲參數化類型
                ParameterizedType parameterizedType = (ParameterizedType) type;
                //獲取泛型的第一個參數的類型類,如Userinfo
                Class<T> modelClass = (Class<T>) parameterizedType.getActualTypeArguments()[0];
                //實例化模型對象
                try {
                    model=modelClass.newInstance();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                break;
            }
            //尋找父類
            actionClass=actionClass.getSuperclass();

        }

   

空指針異常

解決方案1:使用public 的Setter方法上的註解直接注入Service。
SubareaAction:

        //注入service
        private SubareaService subareaService;
        @Autowired
        public void setSubareaService(SubareaService subareaService) {
            this.subareaService = subareaService;
        }

   

解決方案2:
@Autowire還放到私有聲明上,
在struts.xml中覆蓋常量(開啓自動裝配策略):
值默認是false,struts2默認注入採用的是構造器注入(從spring中尋找的bean)
改成true,struts2會採用setter方法注入
頁面標籤(實現頁面內容定製顯示)

        <!-- 引入Shiro標籤 -->
        <%@ taglib uri="http://shiro.apache.org/tags" prefix="shiro"%>

   

    頁面拿Session中的user對象: 代表user對象。
    程序中拿Session中的user對象:SecurityUtils.getSubject().getPrincipal()

資源通配符和權限通配符可便捷開發。
代碼級別

使用代碼編程的方式,直接在程序中使用Subject對象,調用內部的一些API。(有代碼侵入)

    //代碼級別的權限控制(授權):功能權限和角色權限:兩套機制:boolean判斷,異常判斷
        //授權的權限控制
        //====布爾值判斷
        //功能權限
        if(subject.isPermitted("staff")){
            //必須擁有staff功能權限才能執行代碼
            System.out.println("我是一段代碼。。。。。");
        }
        //角色權限
        if(subject.hasRole("weihu")){
            //必須擁有staff功能權限才能執行代碼
            System.out.println("我是一段代碼。。。。。");
        }
        //====異常判斷
        //功能權限
        try {
            subject.checkPermission("staff");
            //有權限
        } catch (AuthorizationException e) {
            // 沒權限
            e.printStackTrace();
        }
        //角色權限
        try {
            subject.checkRole("weihu");
            //有權限
        } catch (AuthorizationException e) {
            // 沒權限
            e.printStackTrace();
        }
---------------------
作者:宏微
來源:CSDN
原文:https://blog.csdn.net/shuaicihai/article/details/58391481
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章