Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理

從入門到放棄的攻城獅發表於2019-03-01

本文是接著上篇部落格寫的:Spring boot 入門(三):SpringBoot 整合結合 AdminLTE(Freemarker),利用 generate 自動生成程式碼,利用 DataTable 和 PageHelper 進行分頁顯示。按照前面的部落格,已經可以搭建一個簡單的 Spring Boot 系統,本篇部落格繼續對此係統進行改造,主要整合了 Shiro 許可權認證框架,關於 Shiro 部分,在本人之前的部落格(認證與Shiro安全框架)有介紹到,這裡就不做累贅的介紹。

此係列的部落格為實踐部分,以程式碼和搭建系統的過程為主,如遇到專業名詞,自行查詢其含義。

1.Shiro 配置類

系統搭建到目前為止,主要用到了3個配置類,均與 Shiro 有關,後期隨著專案的擴大,配置檔案也會隨之增多。

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理

  • FreeMarkerConfig:主要針對 FreeMarker 頁面顯示的配置,關於 Shiro 部分,為 Shiro 標籤設定了共享變數,如果不設定此變數,FreeMarker 頁面將不能識別 Shiro 的標籤,其主要程式碼如下:
configuration.setSharedVariable("shiro", new ShiroTags());
複製程式碼
  • MShiroFilterFactoryBean:設定了過濾器,當然也可以在 Config 檔案裡面配置過濾器,其缺點是:在每次請求裡面都做了 session 的讀取和更新訪問時間等操作,這樣在叢集部署 session 共享的情況下,數量級的加大了處理量負載。本專案後期將用到分散式,因此這裡就直接將過濾器與 Config 配置檔案分離,提高效率。
private final class MSpringShiroFilter extends AbstractShiroFilter {
        protected MSpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) {
            super();
            if (webSecurityManager == null) {
                throw new IllegalArgumentException("WebSecurityManager property cannot be null.");
            }
            setSecurityManager(webSecurityManager);
            if (resolver != null) {
                setFilterChainResolver(resolver);
            }
        }

        @Override
        protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,
                                        FilterChain chain) throws ServletException, IOException {
            HttpServletRequest request = (HttpServletRequest) servletRequest;
            String str = request.getRequestURI().toLowerCase();
            boolean flag = true;
            int idx = 0;
            if ((idx = str.indexOf(".")) > 0) {
                str = str.substring(idx);
                if (ignoreExt.contains(str.toLowerCase()))
                    flag = false;
            }
            if (flag) {
                super.doFilterInternal(servletRequest, servletResponse, chain);
            } else {
                chain.doFilter(servletRequest, servletResponse);
            }
        }

    }
複製程式碼
  • ShiroConfiguration:通用配置檔案,此配置檔案為 Shiro 的基礎通用配置檔案,只要是整合 Shiro,必有此檔案,主要配置 Shiro 的登入認證相關的資訊,其程式碼如下:
/**
     * 設定shiro的快取,快取引數均配置在xml檔案中
     * @return
     */
    @Bean
    public EhCacheManager getEhCacheManager() {
        EhCacheManager em = new EhCacheManager();
        em.setCacheManagerConfigFile("classpath:ehcache/ehcache-shiro.xml");  
        return em;  
    }
    /**
     * 憑證匹配器
     * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了
     *  所以我們需要修改下doGetAuthenticationInfo中的程式碼;
     * )
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
       HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
       hashedCredentialsMatcher.setHashAlgorithmName("md5");//雜湊演算法:這裡使用MD5演算法;
       hashedCredentialsMatcher.setHashIterations(1);//雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
       return hashedCredentialsMatcher;
    }
    /**
     * 
     * 主檔案
     */
    @Bean(name = "myShiroRealm")
    public UserRealm myShiroRealm(EhCacheManager cacheManager) {
        UserRealm realm = new UserRealm(); 
        realm.setCacheManager(cacheManager);
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }
   //會話ID生成器
    @Bean(name = "sessionIdGenerator")
    public JavaUuidSessionIdGenerator javaUuidSessionIdGenerator(){
    	JavaUuidSessionIdGenerator javaUuidSessionIdGenerator = new JavaUuidSessionIdGenerator();
    	return javaUuidSessionIdGenerator;
    }
    @Bean(name = "sessionIdCookie")
    public SimpleCookie getSessionIdCookie(){
    	SimpleCookie sessionIdCookie = new SimpleCookie("sid");
    	sessionIdCookie.setHttpOnly(true);
    	sessionIdCookie.setMaxAge(-1);
    	return sessionIdCookie;
    	
    }
    /*<!-- 會話DAO -->*/
    @Bean(name = "sessionDAO")
    public EnterpriseCacheSessionDAO enterpriseCacheSessionDAO(){
    	EnterpriseCacheSessionDAO sessionDao = new EnterpriseCacheSessionDAO();
    	sessionDao.setSessionIdGenerator(javaUuidSessionIdGenerator());
    	sessionDao.setActiveSessionsCacheName("shiro-activeSessionCache");
    	return sessionDao;
    }
	@Bean(name = "sessionValidationScheduler")
	public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() {
		ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
		scheduler.setInterval(1800000);
		return scheduler;
	}
    @Bean(name = "sessionManager")
    public DefaultWebSessionManager sessionManager(EnterpriseCacheSessionDAO sessionDAO){
    	DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    	sessionManager.setGlobalSessionTimeout(1800000);
    	sessionManager.setDeleteInvalidSessions(true);
    	sessionManager.setSessionValidationSchedulerEnabled(true);
    	sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
    	sessionManager.setSessionDAO(sessionDAO);
    	sessionManager.setSessionIdCookieEnabled(true);
    	sessionManager.setSessionIdCookie(getSessionIdCookie());
    	return sessionManager;
    }
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
        daap.setProxyTargetClass(true);
        return daap;
    }
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(UserRealm myShiroRealm, DefaultWebSessionManager sessionManager) {
        DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
        dwsm.setRealm(myShiroRealm);
//      <!-- 使用者授權/認證資訊Cache, 採用EhCache 快取 --> 
        dwsm.setCacheManager(getEhCacheManager());
        dwsm.setSessionManager(sessionManager);
        return dwsm;
    }
    /**
     *  開啟shiro aop註解支援.
     *  使用代理方式;所以需要開啟程式碼支援;
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(securityManager);
        return aasa;
    }
    /**
     * ShiroFilter<br/>
     * 注意這裡引數中的 StudentService 和 IScoreDao 只是一個例子,因為我們在這裡可以用這樣的方式獲取到相關訪問資料庫的物件,
     * 然後讀取資料庫相關配置,配置到 shiroFilterFactoryBean 的訪問規則中。實際專案中,請使用自己的Service來處理業務邏輯。
     *
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new MShiroFilterFactoryBean();
        // 必須設定 SecurityManager  
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登入成功後要跳轉的連線
        shiroFilterFactoryBean.setSuccessUrl("/certification");
        //shiroFilterFactoryBean.setSuccessUrl("/index");
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        loadShiroFilterChain(shiroFilterFactoryBean);
        return shiroFilterFactoryBean;
    }
    /**
     * 載入shiroFilter許可權控制規則(從資料庫讀取然後配置)
     *
     */
    private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean){
        /////////////////////// 下面這些規則配置最好配置到配置檔案中 ///////////////////////
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內建的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
        filterChainDefinitionMap.put("/login", "authc");
        filterChainDefinitionMap.put("/logout", "logout");
        // anon:它對應的過濾器裡面是空的,什麼都沒做
        logger.info("##################從資料庫讀取許可權規則,載入到shiroFilter中##################");
//        filterChainDefinitionMap.put("/user/edit/**", "authc,perms[user:edit]");// 這裡為了測試,固定寫死的值,也可以從資料庫或其他配置中讀取
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    }
複製程式碼

2.登入認證與許可權管理

主要重寫了 Realm域,完成許可權認證和許可權管理:

	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		//如果沒有做許可權驗證,此處只需要return null即可
		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
		String userName = (String) principals.getPrimaryPrincipal();
		Result<TUser> list = userService.getUserByUsername(userName);
		if(list.isStatus()) {
			//獲取該使用者所屬的角色
			Result<List<TRole>> resultRole = roleService.getRoleByUserId(list.getResultData().getUserId());
			if(resultRole.isStatus()) {
				HashSet<String> role = new HashSet<String>();
				for(TRole tRole : resultRole.getResultData()) {
					role.add(tRole.getRoleId()+"");
				}
				//獲取該角色擁有的許可權
				Result<List<TPermission>> resultPermission = permissionService.getPermissionsByRoleId(role);
				if(resultPermission.isStatus()) {
					HashSet<String> permissions = new HashSet<String>();
					for(TPermission tPermission : resultPermission.getResultData()) {
						permissions.add(tPermission.getPermissionsValue());
					}
					System.out.println("許可權:"+permissions);
					authorizationInfo.setStringPermissions(permissions);
				}
			}
		}
		//return null;
		return authorizationInfo;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		//認證登入
		String username = (String) authenticationToken.getPrincipal();
		//String password = new String((char[]) authenticationToken.getCredentials());
		Result<TUser> result = userService.getUserByUsername(username);
		if (result.isStatus()) {
			TUser user = result.getResultData();
			return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
		}
		//return new SimpleAuthenticationInfo(user., "123456", getName());
		return null;
	}
}
複製程式碼

2.1.登入認證

首先建立一個前端登入介面,做一個簡單的登入 Form 表單

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理
點選登入即想後臺傳送一個請求,必須是Post請求,否則Shiro不能識別,認證部分主要在 Ream 中完成,新建一個類,繼承 AuthorizingRealm ,然後在重寫 doGetAuthenticationInfo 方法:

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理
只需要通過介面上的使用者名稱查詢到資料庫儲存的相關資訊即可,具體的認證是 Shiro 內部自己完成的,我們只需要傳入資料庫中儲存的使用者名稱和密碼個認證函式即可(new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName())),我們可以自己重新定義密碼比較器,密碼比較器的寫法較多,在認證與Shiro安全框架中,直接將密碼比較器寫入到Ream中,耦合度太高,本專案通過配置的方式重寫密碼比較器,具體程式碼請參考參考ShiroConfiguration配置類:

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理

在具體的 Login 方法中,寫入一些登入失敗的異常即可,主要使用者將此失敗結果存入 Session,並顯示在頁面上:

	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public String postLogin(RedirectAttributes redirectAttributes, HttpServletRequest request, HttpSession session) {
		// 登入失敗從request中獲取shiro處理的異常資訊。
		// shiroLoginFailure:就是shiro異常類的全類名.
		String exception = (String) request.getAttribute("shiroLoginFailure");

		System.out.println("exception=" + exception);
		String msg = "";
		if (exception != null) {
			if (UnknownAccountException.class.getName().equals(exception)) {
				System.out.println("UnknownAccountException -- > 賬號不存在:");
				msg = "使用者不存在!";
			} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
				System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
				msg = "密碼不正確!";
			} else if ("kaptchaValidateFailed".equals(exception)) {
				System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤");
				msg = "驗證碼錯誤!";
			} else {
				//msg = "else >> "+exception;
				msg = "密碼不正確!";
				System.out.println("else -- >" + exception);
			}
		}
		redirectAttributes.addFlashAttribute("msg", msg);
		session.setAttribute("msg", msg);
		//return redirect("/login");
		return "redirect:login";
		//return msg;
	}
複製程式碼

此時登入認證部門已經完成:一個頁面+後臺2個函式(1個認證函式+1個Login函式)

2.2.許可權管理

總體來說,許可權管理只需要在介面增加 Shiro 的許可權標籤即可,可以使用角色的標籤,也可以使用許可權的標籤,一般情況下2種標籤配合使用,效果最好 <@shiro.hasPermission name="xtgl-yhgl:read"> <@shiro.hasRolen name="xtgl-yhgl:read">

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理
此外,在 Realm 中,需要重寫許可權認證的業務邏輯,通常情況下通過使用者 ID 找到該使用者所屬的角色,然後通過角色 ID 找到該角色擁有的許可權,並將角色或者許可權寫入的 Shiro 中即可: authorizationInfo.setStringPermissions(permissions); authorizationInfo.setRoles(role);

本專案也是通過此邏輯完成許可權管理的

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理
Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理
上面2張截圖表示的是一個函式。

到此,Spring Boot整合Shiro框架的許可權認證已經搭建完畢,可以實現簡單的許可權管理。

3.新增檔案

較上一篇部落格,Shiro 部分新增加的檔案

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理

Spring boot 入門(四):整合 Shiro 實現登陸認證和許可權管理

相關文章