SpringBoot+shiro整合學習之登入認證和許可權控制
學習任務目標
-
使用者必須要登陸之後才能訪問定義連結,否則跳轉到登入頁面。
-
對連結進行許可權控制,只有噹噹前登入使用者有這個連結訪問許可權才可以訪問,否則跳轉到指定頁面。
-
輸入錯誤密碼使用者名稱或則使用者被設定為靜止登入,返回相應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很強大,這僅僅是完成了登入認證和許可權管理這兩個功能,接下來我會繼續學習和分享,說說接下來的學習路線吧:
-
shiro+redis整合,避免每次訪問有許可權的連結都會去執行MyShiroRealm.doGetAuthenticationInfo()方法來查詢當前使用者的許可權,因為實際情況中許可權是不會經常變得,這樣就可以使用redis進行許可權的快取。
-
實現shiro連結許可權的動態載入,之前要新增一個連結的許可權,要在shiro的配置檔案中新增filterChainDefinitionMap.put("/add", "roles[100002],perms[許可權新增]"),這樣很不方便管理,一種方法是將連結的許可權使用資料庫進行載入,另一種是通過init配置檔案的方式讀取。
-
Shiro 登入後跳轉到最後一個訪問的頁面
-
Shiro 自定義許可權校驗Filter定義,及功能實現。
-
Shiro Ajax請求許可權不滿足,攔截後解決方案。這裡有一個前提,我們知道Ajax不能做頁面redirect和forward跳轉,所以Ajax請求假如沒登入,那麼這個請求給使用者的感覺就是沒有任何反應,而使用者又不知道使用者已經退出了。
-
Shiro JSP標籤使用。
-
Shiro 登入後跳轉到最後一個訪問的頁面
-
線上顯示,線上使用者管理(踢出登入)。
-
登入註冊密碼加密傳輸。
-
整合驗證碼。
-
記住我的功能。關閉瀏覽器後還是登入狀態。
-
還有沒有想到的後面再說,歡迎大家提出一些建議。