shiro一款簡易的Java安全框架

這兩天因爲項目中一直有用到shiro這款框架,所以也是趁着休息的時間好好補課一下shiro。

一、什麼是shiro?

shiro是一個強大的Java安全框架,執行身份驗證。授權、密碼和會話管理的。使用shiro易於理解API,可以非常方便的集成到任何應用程序中。

在這裏也說明一下:後續會寫關於Spring Security這款安全框架,兩者在功能上是非常類似的,所以在學習完一種後最後把另一種也學習一下。

二、shiro的三大核心組件

1、subject:簡單理解爲表示當前操作用戶。其實它的深層表達的意思是第三方的進程。意味這與當前系統交互的"東西"。
那這裏人是直接操作我們的系統,所以一般情況下,subject就是當前用戶,可以通過subject輕鬆獲取需要登錄認證的用戶名密碼涉及到安全的操作數據。

2、securityManager : 它是shiro框架的核心,是一種典型的Facade模式,shiro通過SecurityManager來管理內部組件實例,並通過它來提供安全管理的各種服務。

3、Realm: Realm是shiro框架中與應用安全數據間的橋樑或者連接器,直白的說當用戶需要登錄認證/授權的時候,shiro會從應用配置的Realm中查找用戶及其權限信息。

其中值得注意的是:在配置shiro時,你必須至少指定一個Realm,用戶認證或者授權,可以配置多個Realm.

三、SpringBoot集成Shiro

<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.3.2</version>
</dependency>

在項目中我們一般會配置一個shiroConfig配置類,這裏面會配置我們shiro中非常重要的認證、訪問控制信息以及我們上面說的Realm的管理。

我把我寫的一個shiroConfig例子寫出來大家參考一下:

/**
 * 2020/05/20
 */
@Configuration
public class ShiroConfig {
    //配置Shiro的安全管理器
    @Bean
    public SecurityManager securityManager(Realm myRealm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        //設置一個Realm,這個Realm是最終用於完成我們的認證號和授權操作的具體對象
        securityManager.setRealm(myRealm);
        return securityManager;
    }
    //配置一個自定義的Realm的bean,最終將使用這個bean返回的對象來完成我們的認證和授權
    @Bean
    public Realm myRealm(){
        MyRealm realm=new MyRealm();
        return realm;
    }

    //配置一個Shiro的過濾器bean,這個bean將配置Shiro相關的一個規則的攔截
    //例如什麼樣的請求可以訪問什麼樣的請求不可以訪問等等
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        //創建Shiro的攔截的攔截器 ,用於攔截我們的用戶請求
        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
        //設置Shiro的安全管理,設置管理的同時也會指定某個Realm 用來完成我們權限分配
        shiroFilter.setSecurityManager(securityManager);
        //用於設置一個登錄的請求地址,這個地址可以是一個html或jsp的訪問路徑,也可以是一個控制器的路徑
        //作用是用於通知Shiro我們可以使用這裏路徑轉向到登錄頁面,但Shiro判斷到我們當前的用戶沒有登錄時就會自動轉換到這個路徑
        //要求用戶完成成功
        shiroFilter.setLoginUrl("/");
                //登錄成功後轉向頁面,由於用戶的登錄後期需要交給Shiro完成,因此就需要通知Shiro登錄成功之後返回到那個位置
        shiroFilter.setSuccessUrl("/success");
        //用於指定沒有權限的頁面,當用戶訪問某個功能是如果Shiro判斷這個用戶沒有對應的操作權限,那麼Shiro就會將請求
        //轉向到這個位置,用於提示用戶沒有操作權限
        shiroFilter.setUnauthorizedUrl("/noPermission");
        //定義一個Map集合,這個Map集合中存放的數據全部都是規則,用於設置通知Shiro什麼樣的請求可以訪問什麼樣的請求不可以訪問
        Map<String,String> map=new LinkedHashMap<String,String>();
        //  /login 表示某個請求的名字    anon 表示可以使用遊客什麼進行登錄(這個請求不需要登錄)
        map.put("/login","anon");
       //我們可以在這裏配置所有的權限規則這列數據真正是需要從數據庫中讀取出來
        //或者在控制器中添加Shiro的註解
        //  /admin/**  表示一個請求名字的通配, 以admin開頭的任意子孫路徑下的所有請求
        //  authc 表示這個請求需要進行認證(登錄),只有認證(登錄)通過才能訪問
        // 注意: ** 表示任意子孫路徑
        //       *  表示任意的一個路徑
        //       ? 表示 任意的一個字符
        map.put("/admin/**","authc");
        map.put("/user/**","authc");
        //表示所有的請求路徑全部都需要被攔截登錄,這個必須必須寫在Map集合的最後面,這個選項是可選的
        //如果沒有指定/** 那麼如果某個請求不符合上面的攔截規則Shiro將方行這個請求
//        map.put("/**","authc");
        shiroFilter.setFilterChainDefinitionMap(map);
        return shiroFilter;
    }
}

裏面配置了一些簡單的登錄URL、登錄成功的URL、和nopermission沒有權限的URL。然後又MyRealm對象交由spring來管理。後面的有一個類似於攔截器功能的/admin/** … 。作爲需要認證還是不需要認證、需要相應角色、權限等。

MyRealm:

/**
 * 2020/05/20
 */
public class MyRealm extends AuthorizingRealm  {

    /**
     *用戶認證的方法 這個方法不能手動調用 , Shiro會自動調用
     * @param authenticationToken 用戶身份 這裏存放的是用戶的賬號和密碼
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username=token.getUsername();//獲取頁面中傳遞的用戶賬號
        String password=new String(token.getPassword());//獲取頁面中的用戶密碼實際工作中基本不需要獲取
        System.out.println(username+" -----  "+password);

        /**
         * 認證賬號,這裏應該從數據庫中獲取數據,
         * 如果進入if表示賬號不存在要拋出異常
         */
        if(!"admin".equals(username)&&!"zhangsan".equals(username)&&!"user".equals(username)){
            throw new UnknownAccountException();//拋出賬號錯誤的異常
        }

        /**
         * 認證賬號,這裏應該根據從數據庫中獲取數來的數據進行邏輯判斷,判斷當前賬號是否可用
         * IP是否允許等等,根據不同的邏可以拋出不同的異常
         */
        if("zhangsan".equals(username)){
            throw new LockedAccountException();//拋出賬號鎖定異常
        }
        /**
         * 數據加密主要是防止數據在瀏覽器到後臺服務器之間的數據傳遞時被篡改或被截獲,因此應該在前臺到後臺的過程中進行加密
         * 注意:
         *   建議瀏覽器傳遞數據時就是加密數據,數據庫中存在的數據也是加密數據,我們必須保證前段傳遞的數據
         *   和數據主庫中存放的數據加密次數以及鹽一會規則都是完全相同的否則認證失敗
         */
        //設置讓當前登錄用戶中的密碼數據進行加密
//        HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher();
//        credentialsMatcher.setHashAlgorithmName("MD5");
//        credentialsMatcher.setHashIterations(2);
//
//        this.setCredentialsMatcher(credentialsMatcher);

        //對數據庫中的密碼進行加密
//        Object obj = new SimpleHash("MD5","123456","",1);
        return new SimpleAuthenticationInfo(username,"e10adc3949ba59abbe56e057f20f883e",getName());
    }

    /**
     * 用戶授權的方法,當用戶認證通過每次訪問需要訪問需要授權時都會執行這段代碼完成授權操作
     * 這裏用查詢數據庫來獲取當前用戶的所有角色和權限,並設置到shiro中
     * 注意:由於每次點擊需要授權的請求時,Shiro都會執行這個方法,因此如果這裏的數據時來自於數據庫中的
     *      那麼一定要控制好不能每次都從數據庫中獲取數據這樣效率太低了
     * @param principalCollection
     * @return
     */

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        //獲取用戶的賬號,根據賬號來從數據庫中獲取數據
        Object obj = principalCollection.getPrimaryPrincipal();
        //定義用戶角色的set集合這個集合應該來自數據庫
        Set<String> roles = new HashSet<>();
        if("admin".equals(obj)){
            System.out.println(" ---  授權了admin --------");
            roles.add("admin");
            roles.add("user");
        }
        if("user".equals(obj)){
            System.out.println(" ---  授權了user --------" + obj);
            roles.add("user");
        }
        Set<String> permissions = new HashSet<>();
        if("admin".equals(obj)){
            //添加一個權限admin:add 只是一種命名風格表示admin下的add功能
            permissions.add("admin:add");
        }
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();

        info.setRoles(roles);
        info.setStringPermissions(permissions);

        return info;
    }

那一個最基本的Realm類就創建好了,可以看到裏面的兩個方法:
1、doGetAuthenticationInfo shiro在 用戶認證的時候自動調用這個方法。可以看到這個方法的參數其實就用一個簡易的用戶身份令牌。這個用戶自然就是需要認證的用戶,從令牌中我們可以取出當前認證用戶的用戶名和登錄密碼。自然後面有密碼加密如MD5等等。經過多少次的加鹽迭代來最終判斷該用戶的身份。

2、doGetAuthorizationInfo 授權的回調方法。可以看到這個方法裏面就是給當前用戶授權的操作。

那邊方便大家的測試給大家寫了controller:

/**
 * 2020/05/20
 */

@Controller
public class TestController {

    @RequestMapping("/")
    public String index(){
        return "login";
    }

    @RequestMapping("/login")
    public  String login(String username, String password, Model model){
        //獲取權限操作對象,利用這個對象來完成登錄操作
        Subject subject= SecurityUtils.getSubject();
        //登出,進入這個請求用戶一定是要完成用戶登錄功能,因此我們就先登出,否則Shiro會有緩存不能重新登錄
        //注意:這麼做如果用戶是誤操作會重新指定一次登錄請求
        subject.logout();
        //用戶是否認證過(是否登錄過),進入if表示用戶沒有認證過需要進行認證
        if(!subject.isAuthenticated()){
            //創建用戶認證時的身份令牌,並設置我們從頁面中傳遞過來的賬號和密碼
            UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password);
            try {

                /**
                 * 指定登錄,會自動調用我們Realm對象中的認證方法
                 * 如果登錄失敗會拋出各種異常
                 */
                subject.login(usernamePasswordToken);
            } catch (UnknownAccountException e) {
//                e.printStackTrace();
                model.addAttribute("errorMessage","賬號錯誤!");
                return "login";
            }catch (LockedAccountException e) {
//                e.printStackTrace();
                model.addAttribute("errorMessage","賬號被鎖定!");
                return "login";
            }catch (IncorrectCredentialsException e) {
//                e.printStackTrace();
                model.addAttribute("errorMessage","密碼錯誤");
                return "login";
            }catch (AuthenticationException e) {
                e.printStackTrace();
                model.addAttribute("errorMessage","認證失敗!");
                return "login";
            }
        }

        return "redirect:/success";
    }

    @RequestMapping("/logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        //清空當前賬號shiro的緩存,否則無法重新登錄
        subject.logout();
        return "redirect:/";
    }

    @RequestMapping("/success")
    public String loginSuccess(){
        return "success";
    }
    @RequestMapping("/noPermission")
    public String noPermission(){
        return "noPermission";
    }


    @RequiresRoles(value = {"admin"})
    @RequestMapping("/admin/test")
    public @ResponseBody
    String adminTest(){
        return "/admin/test請求";
    }

    /**
     * 必須登錄認證纔可以訪問,不需要用戶擁有角色和權限
     * @return
     */
    @RequiresAuthentication
    @RequestMapping("/admin/test01")
    public @ResponseBody String adminTest01(){
        return "/admin/test01請求";
    }
    /**
     * @RequiresPermissions 用於判斷當前用戶是否有指定的一個或多個權限用法與RequiresRoles相同
     */
    @RequiresPermissions(value={"admin:add"})
    @RequestMapping("/admin/add")
    public @ResponseBody String adminAdd(){
        return "/admin/add請求";
    }

    @RequiresRoles(value = {"user"})
    @RequestMapping("/user/test")
    public @ResponseBody String userTest(){
        return "/user/test請求";
    }

    /**
     * 配置自定義異常的攔截需要攔截authorizationException或者shiroException
     */
    @ExceptionHandler(value={AuthorizationException.class})
    public String permissionError(Throwable throwable){
        //轉向到沒有權限的視圖頁面,可以利用參數throwable將錯誤信息寫入瀏覽器中
        //實際工作工作中應該根據參數的類型來判斷具體是什麼異常,然後根據同的異常來爲用戶提供不同的
        //提示信息
        return "noPermission";
    }
}

login.html登錄頁面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script th:src="@{|/js/jquery-1.11.3.min.js|}"></script>
    <script th:src="@{|/js/jQuery.md5.js|}"></script>
    <script>
        $(function(){
            $("#loginBut").bind("click",function(){
                var v_md5password=$.md5($("#password").val());
                $("#md5Password").val(v_md5password)
            })
        })
    </script>
</head>
<body>
<form action="login" method="post">
    賬號<input type="text" name="username"><br>
    密碼<input type="text"  id="password"><br>
    <input type="hidden" name="password" id="md5Password">
    <input type="submit" value="登錄" id="loginBut">
</form>

<span style="color: red" th:text="${errorMessage}"></span>

</body>
</html>

noPermission.html 沒有權限頁面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>對不起!您沒有權限操作!</h1>
</body>
</html>

success.html 登錄成功頁面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--基於屬性的shiro標籤-->
<h1 shiro:guest="true">  沒有登錄</h1>
<!--基於標籤的-->
<shiro:authenticated>
    <h1>登錄成功</h1><br>
</shiro:authenticated>

<a href="/logout">登出</a><br><br><br>

<a th:href="@{|/admin/test|}">需要有admin角色的功能</a><br>
<a th:href="@{|/admin/test01|}">需要有admin角色的功能test01</a><br>
<a th:href="@{|/admin/add|}">需要有admin角色的功能add</a><br>
<a th:href="@{|/user/test|}">需要有user角色的功能</a><br>
</body>
</html>

這樣一個最簡單的shiro項目就搭建成功了,但是我想說的是開發中shiro框架不會這樣使用。特別是在某些URL需要特定的角色和權限的時候,這樣在配置文件中大量配置,增加了項目的維護成本。後期URL地址特別多可能我們的shiroConfig這個配置類會非常龐大。因此下面一張我想說的是shiro的基於註解的開發以及常見的shiro標籤(shiro集成thymeleaf的用法)。

–未完待續

ok,週末愉快了~ ~ ~

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