Spring boot 入門(四):集成 Shiro 實現登陸認證和權限管理
本文是接著上篇博客寫的:Spring boot 入門(三):SpringBoot 集成結合 AdminLTE(Freemarker),利用 generate 自動生成代碼,利用 DataTable 和 PageHelper 進行分頁顯示。按照前面的博客,已經可以搭建一個簡單的 Spring Boot 系統,本篇博客繼續對此系統進行改造,主要集成了 Shiro 權限認證框架,關於 Shiro 部分,在本人之前的博客(認證與Shiro安全框架)有介紹到,這裏就不做累贅的介紹。
此系列的博客為實踐部分,以代碼和搭建系統的過程為主,如遇到專業名詞,自行查找其含義。
1.Shiro 配置類
系統搭建到目前為止,主要用到了3個配置類,均與 Shiro 有關,後期隨著項目的擴大,配置文件也會隨之增多。
- FreeMarkerConfig:主要針對 FreeMarker 頁面顯示的配置,關於 Shiro 部分,為 Shiro 標簽設置了共享變量
,如果不設置此變量,FreeMarker 頁面將不能識別 Shiro 的標簽
,其主要代碼如下:
1 configuration.setSharedVariable("shiro", new ShiroTags());
- MShiroFilterFactoryBean:設置了過濾器,當然也可以在 Config 文件裏面配置過濾器,其缺點是:
在每次請求裏面都做了 session 的讀取和更新訪問時間等操作,這樣在集群部署 session 共享的情況下,數量級的加大了處理量負載
1 private final class MSpringShiroFilter extends AbstractShiroFilter { 2 protected MSpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) { 3 super(); 4 if (webSecurityManager == null) { 5 throw new IllegalArgumentException("WebSecurityManager property cannot be null."); 6 } 7 setSecurityManager(webSecurityManager); 8 if (resolver != null) { 9 setFilterChainResolver(resolver); 10 } 11 } 12 13 @Override 14 protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, 15 FilterChain chain) throws ServletException, IOException { 16 HttpServletRequest request = (HttpServletRequest) servletRequest; 17 String str = request.getRequestURI().toLowerCase(); 18 boolean flag = true; 19 int idx = 0; 20 if ((idx = str.indexOf(".")) > 0) { 21 str = str.substring(idx); 22 if (ignoreExt.contains(str.toLowerCase())) 23 flag = false; 24 } 25 if (flag) { 26 super.doFilterInternal(servletRequest, servletResponse, chain); 27 } else { 28 chain.doFilter(servletRequest, servletResponse); 29 } 30 } 31 32 }
- ShiroConfiguration:通用配置文件,此配置文件為 Shiro 的基礎通用配置文件,只要是集成 Shiro,必有此文件,主要配置 Shiro 的登錄認證相關的信息,其代碼如下:
1 /** 2 * 設置shiro的緩存,緩存參數均配置在xml文件中 3 * @return 4 */ 5 @Bean 6 public EhCacheManager getEhCacheManager() { 7 EhCacheManager em = new EhCacheManager(); 8 em.setCacheManagerConfigFile("classpath:ehcache/ehcache-shiro.xml"); 9 return em; 10 } 11 /** 12 * 憑證匹配器 13 * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 14 * 所以我們需要修改下doGetAuthenticationInfo中的代碼; 15 * ) 16 * @return 17 */ 18 @Bean 19 public HashedCredentialsMatcher hashedCredentialsMatcher(){ 20 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); 21 hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法; 22 hashedCredentialsMatcher.setHashIterations(1);//散列的次數,比如散列兩次,相當於 md5(md5("")); 23 return hashedCredentialsMatcher; 24 } 25 /** 26 * 27 * 主文件 28 */ 29 @Bean(name = "myShiroRealm") 30 public UserRealm myShiroRealm(EhCacheManager cacheManager) { 31 UserRealm realm = new UserRealm(); 32 realm.setCacheManager(cacheManager); 33 realm.setCredentialsMatcher(hashedCredentialsMatcher()); 34 return realm; 35 } 36 //會話ID生成器 37 @Bean(name = "sessionIdGenerator") 38 public JavaUuidSessionIdGenerator javaUuidSessionIdGenerator(){ 39 JavaUuidSessionIdGenerator javaUuidSessionIdGenerator = new JavaUuidSessionIdGenerator(); 40 return javaUuidSessionIdGenerator; 41 } 42 @Bean(name = "sessionIdCookie") 43 public SimpleCookie getSessionIdCookie(){ 44 SimpleCookie sessionIdCookie = new SimpleCookie("sid"); 45 sessionIdCookie.setHttpOnly(true); 46 sessionIdCookie.setMaxAge(-1); 47 return sessionIdCookie; 48 49 } 50 /*<!-- 會話DAO -->*/ 51 @Bean(name = "sessionDAO") 52 public EnterpriseCacheSessionDAO enterpriseCacheSessionDAO(){ 53 EnterpriseCacheSessionDAO sessionDao = new EnterpriseCacheSessionDAO(); 54 sessionDao.setSessionIdGenerator(javaUuidSessionIdGenerator()); 55 sessionDao.setActiveSessionsCacheName("shiro-activeSessionCache"); 56 return sessionDao; 57 } 58 @Bean(name = "sessionValidationScheduler") 59 public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() { 60 ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler(); 61 scheduler.setInterval(1800000); 62 return scheduler; 63 } 64 @Bean(name = "sessionManager") 65 public DefaultWebSessionManager sessionManager(EnterpriseCacheSessionDAO sessionDAO){ 66 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); 67 sessionManager.setGlobalSessionTimeout(1800000); 68 sessionManager.setDeleteInvalidSessions(true); 69 sessionManager.setSessionValidationSchedulerEnabled(true); 70 sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler()); 71 sessionManager.setSessionDAO(sessionDAO); 72 sessionManager.setSessionIdCookieEnabled(true); 73 sessionManager.setSessionIdCookie(getSessionIdCookie()); 74 return sessionManager; 75 } 76 @Bean(name = "lifecycleBeanPostProcessor") 77 public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { 78 return new LifecycleBeanPostProcessor(); 79 } 80 @Bean 81 public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { 82 DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); 83 daap.setProxyTargetClass(true); 84 return daap; 85 } 86 @Bean(name = "securityManager") 87 public DefaultWebSecurityManager getDefaultWebSecurityManager(UserRealm myShiroRealm, DefaultWebSessionManager sessionManager) { 88 DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager(); 89 dwsm.setRealm(myShiroRealm); 90 // <!-- 用戶授權/認證信息Cache, 采用EhCache 緩存 --> 91 dwsm.setCacheManager(getEhCacheManager()); 92 dwsm.setSessionManager(sessionManager); 93 return dwsm; 94 } 95 /** 96 * 開啟shiro aop註解支持. 97 * 使用代理方式;所以需要開啟代碼支持; 98 * @param securityManager 99 * @return 100 */ 101 @Bean 102 public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { 103 AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); 104 aasa.setSecurityManager(securityManager); 105 return aasa; 106 } 107 /** 108 * ShiroFilter<br/> 109 * 註意這裏參數中的 StudentService 和 IScoreDao 只是一個例子,因為我們在這裏可以用這樣的方式獲取到相關訪問數據庫的對象, 110 * 然後讀取數據庫相關配置,配置到 shiroFilterFactoryBean 的訪問規則中。實際項目中,請使用自己的Service來處理業務邏輯。 111 * 112 */ 113 @Bean(name = "shiroFilter") 114 public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { 115 ShiroFilterFactoryBean shiroFilterFactoryBean = new MShiroFilterFactoryBean(); 116 // 必須設置 SecurityManager 117 shiroFilterFactoryBean.setSecurityManager(securityManager); 118 // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 119 shiroFilterFactoryBean.setLoginUrl("/login"); 120 // 登錄成功後要跳轉的連接 121 shiroFilterFactoryBean.setSuccessUrl("/certification"); 122 //shiroFilterFactoryBean.setSuccessUrl("/index"); 123 shiroFilterFactoryBean.setUnauthorizedUrl("/403"); 124 loadShiroFilterChain(shiroFilterFactoryBean); 125 return shiroFilterFactoryBean; 126 } 127 /** 128 * 加載shiroFilter權限控制規則(從數據庫讀取然後配置) 129 * 130 */ 131 private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){ 132 /////////////////////// 下面這些規則配置最好配置到配置文件中 /////////////////////// 133 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); 134 // authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter 135 filterChainDefinitionMap.put("/login", "authc"); 136 filterChainDefinitionMap.put("/logout", "logout"); 137 // anon:它對應的過濾器裏面是空的,什麽都沒做 138 logger.info("##################從數據庫讀取權限規則,加載到shiroFilter中##################"); 139 // filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]");// 這裏為了測試,固定寫死的值,也可以從數據庫或其他配置中讀取 140 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 141 }
2.登錄認證與權限管理
主要重寫了 Realm域,完成權限認證和權限管理:
1 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 2 //如果沒有做權限驗證,此處只需要return null即可 3 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); 4 String userName = (String) principals.getPrimaryPrincipal(); 5 Result<TUser> list = userService.getUserByUsername(userName); 6 if(list.isStatus()) { 7 //獲取該用戶所屬的角色 8 Result<List<TRole>> resultRole = roleService.getRoleByUserId(list.getResultData().getUserId()); 9 if(resultRole.isStatus()) { 10 HashSet<String> role = new HashSet<String>(); 11 for(TRole tRole : resultRole.getResultData()) { 12 role.add(tRole.getRoleId()+""); 13 } 14 //獲取該角色擁有的權限 15 Result<List<TPermission>> resultPermission = permissionService.getPermissionsByRoleId(role); 16 if(resultPermission.isStatus()) { 17 HashSet<String> permissions = new HashSet<String>(); 18 for(TPermission tPermission : resultPermission.getResultData()) { 19 permissions.add(tPermission.getPermissionsValue()); 20 } 21 System.out.println("權限:"+permissions); 22 authorizationInfo.setStringPermissions(permissions); 23 } 24 } 25 } 26 //return null; 27 return authorizationInfo; 28 } 29 30 @Override 31 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { 32 //認證登錄 33 String username = (String) authenticationToken.getPrincipal(); 34 //String password = new String((char[]) authenticationToken.getCredentials()); 35 Result<TUser> result = userService.getUserByUsername(username); 36 if (result.isStatus()) { 37 TUser user = result.getResultData(); 38 return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName()); 39 } 40 //return new SimpleAuthenticationInfo(user., "123456", getName()); 41 return null; 42 } 43 }
2.1.登錄認證
首先創建一個前端登錄界面,做一個簡單的登錄 Form 表單
點擊登錄即想後臺發送一個請求,必須是Post請求,否則Shiro不能識別
,認證部分主要在 Ream 中完成,新建一個類,繼承 AuthorizingRealm ,然後在重寫 doGetAuthenticationInfo 方法:
new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName())
),我們可以自己重新定義密碼比較器,密碼比較器的寫法較多,在認證與Shiro安全框架中,直接將密碼比較器寫入到Ream中,耦合度太高,本項目通過配置的方式重寫密碼比較器,具體代碼請參考參考ShiroConfiguration配置類:
在具體的 Login 方法中,寫入一些登錄失敗的異常即可,主要用戶將此失敗結果存入 Session,並顯示在頁面上:
1 @RequestMapping(value = "/login", method = RequestMethod.POST) 2 public String postLogin(RedirectAttributes redirectAttributes, HttpServletRequest request, HttpSession session) { 3 // 登錄失敗從request中獲取shiro處理的異常信息。 4 // shiroLoginFailure:就是shiro異常類的全類名. 5 String exception = (String) request.getAttribute("shiroLoginFailure"); 6 7 System.out.println("exception=" + exception); 8 String msg = ""; 9 if (exception != null) { 10 if (UnknownAccountException.class.getName().equals(exception)) { 11 System.out.println("UnknownAccountException -- > 賬號不存在:"); 12 msg = "用戶不存在!"; 13 } else if (IncorrectCredentialsException.class.getName().equals(exception)) { 14 System.out.println("IncorrectCredentialsException -- > 密碼不正確:"); 15 msg = "密碼不正確!"; 16 } else if ("kaptchaValidateFailed".equals(exception)) { 17 System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤"); 18 msg = "驗證碼錯誤!"; 19 } else { 20 //msg = "else >> "+exception; 21 msg = "密碼不正確!"; 22 System.out.println("else -- >" + exception); 23 } 24 } 25 redirectAttributes.addFlashAttribute("msg", msg); 26 session.setAttribute("msg", msg); 27 //return redirect("/login"); 28 return "redirect:login"; 29 //return msg; 30 }
此時登錄認證部門已經完成:一個頁面+後臺2個函數(1個認證函數+1個Login函數)
2.2.權限管理
總體來說,權限管理只需要在界面增加 Shiro 的權限標簽即可,可以使用角色的標簽,也可以使用權限的標簽,一般情況下2種標簽配合使用,效果最好 <@shiro.hasPermission name="xtgl-yhgl:read">
<@shiro.hasRolen name="xtgl-yhgl:read">
此外,在 Realm 中,需要重寫權限認證的業務邏輯,通常情況下通過用戶 ID 找到該用戶所屬的角色,然後通過角色 ID 找到該角色擁有的權限,並將角色或者權限寫入的 Shiro 中即可:
authorizationInfo.setStringPermissions(permissions);
authorizationInfo.setRoles(role);
本項目也是通過此邏輯完成權限管理的
上面2張截圖表示的是一個函數。
到此,Spring Boot集成Shiro框架的權限認證已經搭建完畢,可以實現簡單的權限管理。
3.新增文件
較上一篇博客,Shiro 部分新增加的文件
Spring boot 入門(四):集成 Shiro 實現登陸認證和權限管理