學習任務(wù)目標
-
用戶必須要登陸之后才能訪問定義鏈接,否則跳轉(zhuǎn)到登錄頁面,。
-
對鏈接進行權(quán)限控制,,只有當當前登錄用戶有這個鏈接訪問權(quán)限才可以訪問,否則跳轉(zhuǎn)到指定頁面,。
-
輸入錯誤密碼用戶名或則用戶被設(shè)置為靜止登錄,,返回相應(yīng)json串信息。
我是用的是之前搭建的一個springboot+mybatisplus+jsp的一個基礎(chǔ)框架。在這之上進行shiro的整合,。需要的同學可以去我的碼云下載,。
個人博客:http://z77z./
此項目下載地址:https://git.oschina.net/z77z/springboot_mybatisplus
導入shiro依賴包到pom.xml
1 2 3 4 5 6
|
<!-- shiro權(quán)限控制框架 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
|
采用RBAC模式建立數(shù)據(jù)庫
RBAC 是基于角色的訪問控制(Role-Based Access Control )在 RBAC 中,權(quán)限與角色相關(guān)聯(lián),,用戶通過成為適當角色的成員而得到這些角色的權(quán)限,。這就極大地簡化了權(quán)限的管理。這樣管理都是層級相互依賴的,,權(quán)限賦予給角色,,而把角色又賦予用戶,這樣的權(quán)限設(shè)計很清楚,,管理起來很方便,。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
/*表結(jié)構(gòu)插入*/ 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 '權(quán)限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 '創(chuàng)建時間', `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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
/** * @author 作者 z77z * @date 創(chuàng)建時間:2017年2月10日 下午1:16:38 * */ @Configuration public class ShiroConfig { /** * ShiroFilterFactoryBean 處理攔截資源文件問題,。 * 注意:單獨一個ShiroFilterFactoryBean配置是或報錯的,,以為在 * 初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager * * Filter Chain定義說明 1、一個URL可以配置多個Filter,,使用逗號分隔 2,、當設(shè)置多個過濾器時,全部驗證通過,,才視為通過 * 3,、部分過濾器可指定參數(shù),如perms,,roles * */ @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必須設(shè)置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不設(shè)置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登錄成功后要跳轉(zhuǎn)的鏈接 shiroFilterFactoryBean.setSuccessUrl("/index"); // 未授權(quán)界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 攔截器. Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的鏈接 順序判斷 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/ajaxLogin", "anon"); // 配置退出過濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實現(xiàn)了 filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/add", "perms[權(quán)限添加]"); // <!-- 過濾鏈定義,,從上向下順序執(zhí)行,一般將 /**放在最為下邊 -->:這是一個坑呢,,一不小心代碼就不好使了; // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問--> filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); System.out.println("Shiro攔截器工廠類注入成功"); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 設(shè)置realm. securityManager.setRealm(myShiroRealm()); return securityManager; } /** * 身份認證realm; (這個需要自己寫,,賬號密碼校驗;權(quán)限等) * * @return */ @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); return myShiroRealm; } }
|
登錄認證實現(xiàn)
在認證,、授權(quán)內(nèi)部實現(xiàn)機制中都有提到,,最終處理都將交給Real進行處理。因為在Shiro中,,最終是通過Realm來獲取應(yīng)用程序中的用戶,、角色及權(quán)限信息的,。通常情況下,在Realm中會直接從我們的數(shù)據(jù)源中獲取Shiro需要的驗證信息,??梢哉f,Realm是專用于安全框架的DAO.
Shiro的認證過程最終會交由Realm執(zhí)行,,這時會調(diào)用Realm的getAuthenticationInfo(token)方法,。
該方法主要執(zhí)行以下操作:
1、檢查提交的進行認證的令牌信息
2,、根據(jù)令牌信息從數(shù)據(jù)源(通常為數(shù)據(jù)庫)中獲取用戶信息
3,、對用戶信息進行匹配驗證。
4,、驗證通過將返回一個封裝了用戶信息的AuthenticationInfo實例,。
5、驗證失敗則拋出AuthenticationException異常信息,。
而在我們的應(yīng)用程序中要做的就是自定義一個Realm類,,繼承AuthorizingRealm抽象類,重載doGetAuthenticationInfo
(),,重寫獲取用戶信息的方法,。
doGetAuthenticationInfo的重寫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
/** * 認證信息.(身份驗證) : 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; // 從數(shù)據(jù)庫獲取對應(yīng)用戶名密碼的用戶 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("賬號已經(jīng)禁止登錄!"); }else{ //更新登錄時間 last login time user.setLastLoginTime(new Date()); sysUserService.updateById(user); } return new SimpleAuthenticationInfo(user, user.getPswd(), getName()); }
|
通俗的說,,這個的重寫就是我們第一個學習目標的實現(xiàn),。
鏈接權(quán)限的實現(xiàn)
shiro的權(quán)限授權(quán)是通過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();
當訪問到頁面的時候,,鏈接配置了相應(yīng)的權(quán)限或者shiro標簽才會執(zhí)行此方法否則不會執(zhí)行,,所以如果只是簡單的身份認證沒有權(quán)限的控制的話,那么這個方法可以不進行實現(xiàn),,直接返回null即可,。
在這個方法中主要是使用類:SimpleAuthorizationInfo
進行角色的添加和權(quán)限的添加。
authorizationInfo.addRole(role.getRole());
authorizationInfo.addStringPermission(p.getPermission());
當然也可以添加set集合:roles是從數(shù)據(jù)庫查詢的當前用戶的角色,,stringPermissions是從數(shù)據(jù)庫查詢的當前用戶對應(yīng)的權(quán)限
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
就是說如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權(quán)限添加]”);
就說明訪問/add這個鏈接必須要有“權(quán)限添加”這個權(quán)限才可以訪問,,
如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權(quán)限添加]”);
就說明訪問/add這個鏈接必須要有“權(quán)限添加”這個權(quán)限和具有“100002”這個角色才可以訪問,。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
/** * 授權(quán) */ @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { System.out.println("權(quán)限認證方法:MyShiroRealm.doGetAuthenticationInfo()"); SysUser token = (SysUser)SecurityUtils.getSubject().getPrincipal(); String userId = token.getId(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //根據(jù)用戶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()); }*/ //實際開發(fā),,當前登錄用戶的角色和權(quán)限信息是從數(shù)據(jù)庫來獲取的,,我這里寫死是為了方便測試 Set<String> roleSet = new HashSet<String>(); roleSet.add("100002"); info.setRoles(roleSet); //根據(jù)用戶ID查詢權(quán)限(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("權(quán)限添加"); info.setStringPermissions(permissionSet); return info; }
|
這個類的實現(xiàn)是完成了我們學習目標的第二個任務(wù),。
編寫web層的代碼
登錄頁面:
controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
//跳轉(zhuǎn)到登錄表單頁面 @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
<%@ 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./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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
//跳轉(zhuǎn)到主頁 @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
<%@ 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
1 2 3 4
|
@RequestMapping(value="add") public String add() { return "add"; }
|
jsp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
<%@ 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> 具有添加權(quán)限 </body> </html>
|
測試
任務(wù)一
編寫好后就可以啟動程序,,訪問index頁面,由于沒有登錄就會跳轉(zhuǎn)到login頁面,。
登錄之后就會跳轉(zhuǎn)到index頁面,,點擊退出登錄后,有直接在瀏覽器中輸入index頁面訪問,,又會跳轉(zhuǎn)到login頁面
上面這些操作時候觸發(fā)MyShiroRealm.doGetAuthenticationInfo()這個方法,,也就是登錄認證的方法。
任務(wù)二
登錄之后訪問add頁面成功訪問,,在shiro配置文件中改變add的訪問權(quán)限為
filterChainDefinitionMap.put(“/add”,”perms[權(quán)限刪除]”);
再重新啟動程序,,登錄后訪問,會重定向到/403頁面,,由于沒有編寫403頁面,,報404錯誤。
上面這些操作,,會觸發(fā)權(quán)限認證方法:MyShiroRealm.doGetAuthorizationInfo(),,每訪問一次就會觸發(fā)一次。
任務(wù)三
輸入錯誤的用戶名或則密碼,,返回“賬號或密碼不正確,!”的錯誤信息,在數(shù)據(jù)庫中把一個用戶的狀態(tài)改為被禁用,,再登陸,,提示“賬號已經(jīng)禁止登錄!”的錯誤信息
上面的操作,,是在MyShiroRealm.doGetAuthenticationInfo()登錄認證的方法中實現(xiàn)的,,通過查詢數(shù)據(jù)庫判斷當前登錄用戶是否被禁用,具體可以去看源碼,。
總結(jié)
當然shiro很強大,,這僅僅是完成了登錄認證和權(quán)限管理這兩個功能,接下來我會繼續(xù)學習和分享,,說說接下來的學習路線吧:
-
shiro+Redis集成,,避免每次訪問有權(quán)限的鏈接都會去執(zhí)行MyShiroRealm.doGetAuthenticationInfo()方法來查詢當前用戶的權(quán)限,因為實際情況中權(quán)限是不會經(jīng)常變得,,這樣就可以使用redis進行權(quán)限的緩存,。
-
實現(xiàn)shiro鏈接權(quán)限的動態(tài)加載,之前要添加一個鏈接的權(quán)限,,要在shiro的配置文件中添加filterChainDefinitionMap.put(“/add”, “roles[100002],,perms[權(quán)限添加]”),,這樣很不方便管理,一種方法是將鏈接的權(quán)限使用數(shù)據(jù)庫進行加載,,另一種是通過init配置文件的方式讀取,。
-
Shiro 自定義權(quán)限校驗Filter定義,及功能實現(xiàn),。
-
Shiro Ajax請求權(quán)限不滿足,,攔截后解決方案。這里有一個前提,,我們知道Ajax不能做頁面redirect和forward跳轉(zhuǎn),,所以Ajax請求假如沒登錄,那么這個請求給用戶的感覺就是沒有任何反應(yīng),,而用戶又不知道用戶已經(jīng)退出了,。
-
Shiro JSP標簽使用。
-
Shiro 登錄后跳轉(zhuǎn)到最后一個訪問的頁面
-
在線顯示,,在線用戶管理(踢出登錄),。
-
登錄注冊密碼加密傳輸。
-
集成驗證碼,。
-
記住我的功能,。關(guān)閉瀏覽器后還是登錄狀態(tài)。
-
還有沒有想到的后面再說,,歡迎大家提出一些建議,。
|