1. 程式人生 > >SpringBoot+shiro整合學習之登入認證和許可權控制

SpringBoot+shiro整合學習之登入認證和許可權控制

學習任務目標

  1. 使用者必須要登陸之後才能訪問定義連結,否則跳轉到登入頁面。

  2. 對連結進行許可權控制,只有噹噹前登入使用者有這個連結訪問許可權才可以訪問,否則跳轉到指定頁面。

  3. 輸入錯誤密碼使用者名稱或則使用者被設定為靜止登入,返回相應json串資訊

    匯入shiro依賴包到pom.xml

    <!-- shiro許可權控制框架 -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.3.2</version>
    </dependency>
    

    採用RBAC模式建立資料庫

    RBAC 是基於角色的訪問控制(Role-Based Access Control )在 RBAC 中,許可權與角色相關聯,使用者通過成為適當角色的成員而得到這些角色的許可權。這就極大地簡化了許可權的管理。這樣管理都是層級相互依賴的,許可權賦予給角色,而把角色又賦予使用者,這樣的許可權設計很清楚,管理起來很方便。

    /*表結構插入*/
    DROP TABLE IF EXISTS `u_permission`;
    
    CREATE TABLE `u_permission` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `url` varchar(256) DEFAULT NULL COMMENT 'url地址',
      `name` varchar(64) DEFAULT NULL COMMENT 'url描述',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;
    
    /*Table structure for table `u_role` */
    
    DROP TABLE IF EXISTS `u_role`;
    
    CREATE TABLE `u_role` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `name` varchar(32) DEFAULT NULL COMMENT '角色名稱',
      `type` varchar(10) DEFAULT NULL COMMENT '角色型別',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
    
    /*Table structure for table `u_role_permission` */
    
    DROP TABLE IF EXISTS `u_role_permission`;
    
    CREATE TABLE `u_role_permission` (
      `rid` bigint(20) DEFAULT NULL COMMENT '角色ID',
      `pid` bigint(20) DEFAULT NULL COMMENT '許可權ID'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    /*Table structure for table `u_user` */
    
    DROP TABLE IF EXISTS `u_user`;
    
    CREATE TABLE `u_user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `nickname` varchar(20) DEFAULT NULL COMMENT '使用者暱稱',
      `email` varchar(128) DEFAULT NULL COMMENT '郵箱|登入帳號',
      `pswd` varchar(32) DEFAULT NULL COMMENT '密碼',
      `create_time` datetime DEFAULT NULL COMMENT '建立時間',
      `last_login_time` datetime DEFAULT NULL COMMENT '最後登入時間',
      `status` bigint(1) DEFAULT '1' COMMENT '1:有效,0:禁止登入',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8;
    
    /*Table structure for table `u_user_role` */
    
    DROP TABLE IF EXISTS `u_user_role`;
    
    CREATE TABLE `u_user_role` (
      `uid` bigint(20) DEFAULT NULL COMMENT '使用者ID',
      `rid` bigint(20) DEFAULT NULL COMMENT '角色ID'
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    Dao層程式碼的編寫

    Dao層的entity,service,mapper等我是採用mybatisplus的程式碼自動生成工具生成的,具備了單表的增刪改查功能和分頁功能,比較方便,這裡我就不貼程式碼了。

    配置shiro

    ShiroConfig.java

    /**
     * @author 作者 z77z
     * @date 建立時間:2017年2月10日 下午1:16:38
     * 
     */
    @Configuration
    public class ShiroConfig {
        /**
         * ShiroFilterFactoryBean 處理攔截資原始檔問題。
         * 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,以為在
         * 初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager
         *
         * Filter Chain定義說明 1、一個URL可以配置多個Filter,使用逗號分隔 2、當設定多個過濾器時,全部驗證通過,才視為通過
         * 3、部分過濾器可指定引數,如perms,roles
         *
         */
        @Bean
        public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    
            // 必須設定 SecurityManager
            shiroFilterFactoryBean.setSecurityManager(securityManager);
    
            // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
            shiroFilterFactoryBean.setLoginUrl("/login");
            // 登入成功後要跳轉的連結
            shiroFilterFactoryBean.setSuccessUrl("/index");
            // 未授權介面;
            shiroFilterFactoryBean.setUnauthorizedUrl("/403");
    
            // 攔截器.
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
            // 配置不會被攔截的連結 順序判斷
            filterChainDefinitionMap.put("/static/**", "anon");
            filterChainDefinitionMap.put("/ajaxLogin", "anon");
    
            // 配置退出過濾器,其中的具體的退出程式碼Shiro已經替我們實現了
            filterChainDefinitionMap.put("/logout", "logout");
    
            filterChainDefinitionMap.put("/add", "perms[許可權新增]");
    
            // <!-- 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 -->:這是一個坑呢,一不小心程式碼就不好使了;
            // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
            filterChainDefinitionMap.put("/**", "authc");
    
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            System.out.println("Shiro攔截器工廠類注入成功");
            return shiroFilterFactoryBean;
        }
    
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 設定realm.
            securityManager.setRealm(myShiroRealm());
            return securityManager;
        }
    
        /**
         * 身份認證realm; (這個需要自己寫,賬號密碼校驗;許可權等)
         * 
         * @return
         */
        @Bean
        public MyShiroRealm myShiroRealm() {
            MyShiroRealm myShiroRealm = new MyShiroRealm();
            return myShiroRealm;
        }
    }
    

    登入認證實現

    在認證、授權內部實現機制中都有提到,最終處理都將交給Real進行處理。因為在Shiro中,最終是通過Realm來獲取應用程式中的使用者、角色及許可權資訊的。通常情況下,在Realm中會直接從我們的資料來源中獲取Shiro需要的驗證資訊。可以說,Realm是專用於安全框架的DAO.

    Shiro的認證過程最終會交由Realm執行,這時會呼叫Realm的getAuthenticationInfo(token)方法。 該方法主要執行以下操作:

    1、檢查提交的進行認證的令牌資訊

    2、根據令牌資訊從資料來源(通常為資料庫)中獲取使用者資訊

    3、對使用者資訊進行匹配驗證。

    4、驗證通過將返回一個封裝了使用者資訊的AuthenticationInfo例項。

    5、驗證失敗則丟擲AuthenticationException異常資訊。

    而在我們的應用程式中要做的就是自定義一個Realm類,繼承AuthorizingRealm抽象類,過載doGetAuthenticationInfo (),重寫獲取使用者資訊的方法。

    doGetAuthenticationInfo的重寫

    /**
    * 認證資訊.(身份驗證) : Authentication 是用來驗證使用者身份
     * 
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authcToken) throws AuthenticationException {
        System.out.println("身份認證方法:MyShiroRealm.doGetAuthenticationInfo()");
    
        ShiroToken token = (ShiroToken) authcToken;
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("nickname", token.getUsername());
        map.put("pswd", token.getPswd());
        SysUser user = null;
        // 從資料庫獲取對應使用者名稱密碼的使用者
        List<SysUser> userList = sysUserService.selectByMap(map);
        if(userList.size()!=0){
            user = userList.get(0);
        }
        if (null == user) {
            throw new AccountException("帳號或密碼不正確!");
        }else if(user.getStatus()==0){
            /**
             * 如果使用者的status為禁用。那麼就丟擲<code>DisabledAccountException</code>
             */
            throw new DisabledAccountException("帳號已經禁止登入!");
        }else{
            //更新登入時間 last login time
            user.setLastLoginTime(new Date());
            sysUserService.updateById(user);
        }
        return new SimpleAuthenticationInfo(user, user.getPswd(), getName());
    }
    

    通俗的說,這個的重寫就是我們第一個學習目標的實現。

    連結許可權的實現

    shiro的許可權授權是通過繼承AuthorizingRealm抽象類,過載doGetAuthorizationInfo();

    當訪問到頁面的時候,連結配置了相應的許可權或者shiro標籤才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有許可權的控制的話,那麼這個方法可以不進行實現,直接返回null即可。

    在這個方法中主要是使用類:SimpleAuthorizationInfo

    進行角色的新增和許可權的新增。

    authorizationInfo.addRole(role.getRole());

    authorizationInfo.addStringPermission(p.getPermission());

    當然也可以新增set集合:roles是從資料庫查詢的當前使用者的角色,stringPermissions是從資料庫查詢的當前使用者對應的許可權

    authorizationInfo.setRoles(roles);

    authorizationInfo.setStringPermissions(stringPermissions);

    就是說如果在shiro配置檔案中添加了filterChainDefinitionMap.put("/add", "perms[許可權新增]"); 就說明訪問/add這個連結必須要有“許可權新增”這個許可權才可以訪問,

    如果在shiro配置檔案中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[許可權新增]"); 就說明訪問/add這個連結必須要有“許可權新增”這個許可權和具有“100002”這個角色才可以訪問。

    /**
    * 授權
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        System.out.println("許可權認證方法:MyShiroRealm.doGetAuthenticationInfo()");
        SysUser token = (SysUser)SecurityUtils.getSubject().getPrincipal();
        String userId = token.getId();
        SimpleAuthorizationInfo info =  new SimpleAuthorizationInfo();
        //根據使用者ID查詢角色(role),放入到Authorization裡。
        /*Map<String, Object> map = new HashMap<String, Object>();
        map.put("user_id", userId);
        List<SysRole> roleList = sysRoleService.selectByMap(map);
        Set<String> roleSet = new HashSet<String>();
        for(SysRole role : roleList){
            roleSet.add(role.getType());
        }*/
        //實際開發,當前登入使用者的角色和許可權資訊是從資料庫來獲取的,我這裡寫死是為了方便測試
        Set<String> roleSet = new HashSet<String>();
        roleSet.add("100002");
        info.setRoles(roleSet);
        //根據使用者ID查詢許可權(permission),放入到Authorization裡。
        /*List<SysPermission> permissionList = sysPermissionService.selectByMap(map);
        Set<String> permissionSet = new HashSet<String>();
        for(SysPermission Permission : permissionList){
            permissionSet.add(Permission.getName());
        }*/
        Set<String> permissionSet = new HashSet<String>();
        permissionSet.add("許可權新增");
        info.setStringPermissions(permissionSet);
           return info;
    }
    

    這個類的實現是完成了我們學習目標的第二個任務。

    編寫web層的程式碼

    登入頁面:

    controller

    //跳轉到登入表單頁面
    @RequestMapping(value="login")
    public String login() {
        return "login";
    }
    
    /**
     * ajax登入請求
     * @param username
     * @param password
     * @return
     */
    @RequestMapping(value="ajaxLogin",method=RequestMethod.POST)
    @ResponseBody
    public Map<String,Object> submitLogin(String username, String password,Model model) {
        Map<String, Object> resultMap = new LinkedHashMap<String, Object>();
        try {
            
            ShiroToken token = new ShiroToken(username, password);
            SecurityUtils.getSubject().login(token);
            resultMap.put("status", 200);
            resultMap.put("message", "登入成功");
    
        } catch (Exception e) {
            resultMap.put("status", 500);
            resultMap.put("message", e.getMessage());
        }
        return resultMap;
    }
    

    jsp

    <%@ page language="java" contentType="text/html; charset=utf-8"
        pageEncoding="utf-8"%>
    <%
        String path = request.getContextPath();
        String basePath = request.getScheme() + "://"
                + request.getServerName() + ":" + request.getServerPort()
                + path;
    %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script type="text/javascript"
        src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
    <title>登入</title>
    </head>
    <body>
        錯誤資訊:
        <h4 id="erro"></h4>
        <form>
            <p>
                賬號:<input type="text" name="username" id="username" value="admin" />
            </p>
            <p>
                密碼:<input type="text" name="password" id="password" value="123" />
            </p>
            <p>
                <input type="button" id="ajaxLogin" value="登入" />
            </p>
        </form>
    </body>
    <script>
        var username = $("#username").val();
        var password = $("#password").val();
        $("#ajaxLogin").click(function() {
            $.post("/ajaxLogin", {
                "username" : username,
                "password" : password
            }, function(result) {
                if (result.status == 200) {
                    location.href = "/index";
                } else {
                    $("#erro").html(result.message);
                }
            });
        });
    </script>
    </html>
    

    主頁頁面

    controller

    //跳轉到主頁
    @RequestMapping(value="index")
    public String index() {
        return "index";
    }
    
    /**
    * 退出
     * @return
     */
    @RequestMapping(value="logout",method =RequestMethod.GET)
    @ResponseBody
    public Map<String,Object> logout(){
        Map<String, Object> resultMap = new LinkedHashMap<String, Object>();
        try {
            //退出
            SecurityUtils.getSubject().logout();
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
        return resultMap;
    }
    

    jsp

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%
        String path = request.getContextPath();
        String basePath = request.getScheme() + "://"
                + request.getServerName() + ":" + request.getServerPort()
                + path;
    %>
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <script type="text/javascript"
        src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
    <title>Insert title here</title>
    </head>
    <body>
        helloJsp
        <input type="button" id="logout" value="退出登入" />
    </body>
    <script type="text/javascript">
        $("#logout").click(function(){
            location.href="/logout";
        });
    </script>
    </html>
    

    新增操作頁面

    controller

    @RequestMapping(value="add")
    public String add() {
        return "add";
    }
    

    jsp

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%
        String path = request.getContextPath();
        String basePath = request.getScheme() + "://"
                + request.getServerName() + ":" + request.getServerPort()
                + path;
    %>
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <script type="text/javascript"
        src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
    <title>Insert title here</title>
    </head>
    <body>
    具有新增許可權
    </body>
    </html>
    

    測試

    任務一

    編寫好後就可以啟動程式,訪問index頁面,由於沒有登入就會跳轉到login頁面。

    登入之後就會跳轉到index頁面,點選退出登入後,有直接在瀏覽器中輸入index頁面訪問,又會跳轉到login頁面

    上面這些操作時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登入認證的方法。

    任務二

    登入之後訪問add頁面成功訪問,在shiro配置檔案中改變add的訪問許可權為

    filterChainDefinitionMap.put("/add","perms[許可權刪除]");

    再重新啟動程式,登入後訪問,會重定向到/403頁面,由於沒有編寫403頁面,報404錯誤。

    上面這些操作,會觸發許可權認證方法:MyShiroRealm.doGetAuthorizationInfo(),每訪問一次就會觸發一次。

    任務三

    輸入錯誤的使用者名稱或則密碼,返回“帳號或密碼不正確!”的錯誤資訊,在資料庫中把一個使用者的狀態改為被禁用,再登陸,提示“帳號已經禁止登入!”的錯誤資訊

    上面的操作,是在MyShiroRealm.doGetAuthenticationInfo()登入認證的方法中實現的,通過查詢資料庫判斷當前登入使用者是否被禁用,具體可以去看原始碼。

    總結

    當然shiro很強大,這僅僅是完成了登入認證和許可權管理這兩個功能,接下來我會繼續學習和分享,說說接下來的學習路線吧:

  4. shiro+redis整合,避免每次訪問有許可權的連結都會去執行MyShiroRealm.doGetAuthenticationInfo()方法來查詢當前使用者的許可權,因為實際情況中許可權是不會經常變得,這樣就可以使用redis進行許可權的快取。

  5. 實現shiro連結許可權的動態載入,之前要新增一個連結的許可權,要在shiro的配置檔案中新增filterChainDefinitionMap.put("/add", "roles[100002],perms[許可權新增]"),這樣很不方便管理,一種方法是將連結的許可權使用資料庫進行載入,另一種是通過init配置檔案的方式讀取。

  6. Shiro 登入後跳轉到最後一個訪問的頁面  

  7. Shiro 自定義許可權校驗Filter定義,及功能實現。

  8. Shiro Ajax請求許可權不滿足,攔截後解決方案。這裡有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉,所以Ajax請求假如沒登入,那麼這個請求給使用者的感覺就是沒有任何反應,而使用者又不知道使用者已經退出了。

  9. Shiro JSP標籤使用。

  10. Shiro 登入後跳轉到最後一個訪問的頁面

  11. 線上顯示,線上使用者管理(踢出登入)。

  12. 登入註冊密碼加密傳輸。

  13. 整合驗證碼。

  14. 記住我的功能。關閉瀏覽器後還是登入狀態。

  15. 還有沒有想到的後面再說,歡迎大家提出一些建議。