SpringBoot+shiro整合學習之登錄認證和權限控制

學習任務目標

  1. 用戶必須要登陸之后才能訪問定義鏈接,否則跳轉到登錄頁面。

  2. 對鏈接進行權限控制,只有當當前登錄用戶有這個鏈接訪問權限才可以訪問,否則跳轉到指定頁面。

  3. 輸入錯誤密碼用戶名或則用戶被設置為靜止登錄,返回相應json串信息。

我是用的是之前搭建的一個springboot+mybatisplus+jsp的一個基礎框架。在這之上進行shiro的整合。需要的同學可以去我的碼云下載。

個人博客:http://z77z.oschina.io/

此項目下載地址:https://git.oschina.net/z77z/springboot_mybatisplus

導入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很強大,這僅僅是完成了登錄認證和權限管理這兩個功能,接下來我會繼續學習和分享,說說接下來的學習路線吧:

  1. shiro+redis集成,避免每次訪問有權限的鏈接都會去執行MyShiroRealm.doGetAuthenticationInfo()方法來查詢當前用戶的權限,因為實際情況中權限是不會經常變得,這樣就可以使用redis進行權限的緩存。

  2. 實現shiro鏈接權限的動態加載,之前要添加一個鏈接的權限,要在shiro的配置文件中添加filterChainDefinitionMap.put("/add", "roles[100002],perms[權限添加]"),這樣很不方便管理,一種方法是將鏈接的權限使用數據庫進行加載,另一種是通過init配置文件的方式讀取。

  3. Shiro 自定義權限校驗Filter定義,及功能實現。

  4. Shiro Ajax請求權限不滿足,攔截后解決方案。這里有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉,所以Ajax請求假如沒登錄,那么這個請求給用戶的感覺就是沒有任何反應,而用戶又不知道用戶已經退出了。

  5. Shiro JSP標簽使用。

  6. Shiro 登錄后跳轉到最后一個訪問的頁面

  7. 在線顯示,在線用戶管理(踢出登錄)。

  8. 登錄注冊密碼加密傳輸。

  9. 集成驗證碼。

  10. 記住我的功能。關閉瀏覽器后還是登錄狀態。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容