spring boot + mybatis + layui + shiro后台權限管理系統


后台管理系統

版本更新

后續版本更新內容

鏈接入口

  1. shiro並發登陸人數控制(超出登錄用戶最大配置數量,清理用戶)功能;
  2. 解決父子頁面判斷用戶未登錄之后,重定向到頁面中嵌套顯示登錄界面問題;
  3. 解決ajax請求,判斷用戶未登錄之后,重定向到登錄頁面問題;
  4. 解決完成了功能1,導致的session有效時間沖突問題等。

其他時間的版本更新,詳見本文末尾或git項目更新日志!

下期版本更新內容

  • 新建wyait-admin單數據源配置項目;
  • redis版本,實現用戶在線數量控制功能等;
  • 使用redis記錄驗證碼;

    業務場景

  • spring boot + mybatis后台管理系統框架;
  • layUI前端界面;
  • shiro權限控制,ehCache緩存;

開發背景

maven :3.3.3 
JDK : 1.8 
Intellij IDEA : 2017.2.5 開發工具 
spring boot :1.5.9.RELEASE 
mybatis 3.4.5 :dao層框架 
pageHelper : 5.1.2 
httpClient : 4.5.3
layui 2.2.3 :前端框架 
shiro 1.4.0 :權限控制框架 
druid 1.1.5 :druid連接池,監控數據庫性能,記錄SQL執行日志 
thymeleaf :2.1.4.RELEASE,thymeleaf前端html頁面模版 
log4j2 2.7 :日志框架 
EHCache : 2.5.0 
ztree : 3.5.31

項目框架

spring boot + mybatis + shiro + layui + ehcache 
項目源碼:(包含數據庫源碼) 
github源碼: https://github.com/wyait/manage.git 
碼雲:https://gitee.com/wyait/manage.git

基礎框架

spring boot + mybatis的整合,參考博客: 
https://blog.51cto.com/wyait/1969626

spring boot之靜態資源路徑配置

靜態資源路徑是指系統可以直接訪問的路徑,且路徑下的所有文件均可被用戶直接讀取。

在Springboot中默認的靜態資源路徑有:classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,從這里可以看出這里的靜態資源路徑都是在classpath中(也就是在項目路徑下指定的這幾個文件夾)

試想這樣一種情況:一個網站有文件上傳文件的功能,如果被上傳的文件放在上述的那些文件夾中會有怎樣的后果?

網站數據與程序代碼不能有效分離;
當項目被打包成一個.jar文件部署時,再將上傳的文件放到這個.jar文件中是有多么低的效率; 網站數據的備份將會很痛苦。

此時可能最佳的解決辦法是將靜態資源路徑設置到磁盤的某個目錄。與應用程序分離。

在Springboot中可以直接在配置文件中覆蓋默認的靜態資源路徑的配置信息:

application.properties配置文件如下:
# 靜態資源路徑配置 wyait.picpath=D:/demo-images/ spring.mvc.static-path-pattern=/** spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,file:${wyait.picpath}

注意wyait.picpath這個屬於自定義的屬性,指定了一個路徑,注意要以/結尾;

spring.mvc.static-path-pattern=/ 表示所有的訪問都經過靜態資源路徑;

spring.resources.static-locations 在這里配置靜態資源路徑,前面說了這里的配置是覆蓋默認配置,所以需要將默認的也加上否則static、public等這些路徑將不能被當作靜態資源路徑,在這個最末尾的file:${wyait.picpath} ==file:${wyait.picpath}==, 
加 file :是因為指定的是一個具體的硬盤路徑,其他的使用classpath指的是系統環境變量。

問題

圖片或靜態資源直接放在wyait.picpath=D:/demo-images/目錄下,訪問:http://127.0.0.1:8077/0.jpg,會報錯

[2018-04-08 22:05:32.095][http-nio-8077-exec-3][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: Error resolving template "0", template might not exist or might not be accessible by any of the configured Template Resolvers] with root cause org.thymeleaf.exceptions.TemplateInputException: Error resolving template "0", template might not exist or might not be accessible by any of the configured Template Resolvers at org.thymeleaf.TemplateRepository.getTemplate(TemplateRepository.java:246) ~[thymeleaf-2.1.6.RELEASE.jar:2.1.6.RELEASE]

原因應該是在項目集成shiro時,shiro對contextPath/后面的第一層path訪問時,對標點“.”進行了截取,實際請求變成了:http://127.0.0.1:8077/0 , 交給dispatcherServlet處理,沒有找到匹配的view視圖“0”,就報錯。具體原因抽空跟蹤下源碼。

解決方案:

這個file靜態資源配置,在項目開發訪問時,需要在wyait.picpath=D:/demo-images/配置的目錄下,再加一層或一層以上的目錄。如圖: 

比如:保存圖片時,一般會根據年月日進行分目錄,實際圖片保存在D:/demo-images/201804/0.jpg目錄下;訪問的時候,直接:http://127.0.0.1:8077/2018/0.jpg,即可訪問到圖片

添加一層或多層目錄之后,springboot會在靜態資源配置中依次找到匹配的目錄,然后加載靜態資源;

自定義靜態資源配置方法

自定義靜態資源配置方法,參考博客:https://blog.51cto.com/wyait/1971108 博客末尾處,提供了自定義靜態資源訪問方法,通過配置類設置對應的路徑進行靜態資源訪問。

總結

此配置解決了springboot+thymeleaf架構的獲取圖片(靜態資源)404的問題;之前的SpringMVC + jsp在讀取圖片的時候,本地或服務器在讀取用戶上傳的圖片時,需要配置nginx;spring boot在不更換域名的前提下,默認是根據application.xml文件的靜態資源路徑配置查找圖片等靜態資源;nginx配置是無效的,會導致圖片無法獲取(讀取404)。 
所以如果要對圖片或其他靜態資源進行應用程序分離時,需要使用以上配置,覆蓋原springboot默認配置,另外,不需要額外配置nginx,也是一個優點。

整合layui

layui官網:http://www.layui.com 
layui下載地址:https://github.com/sentsin/layui/

  1. 將下載的layui解壓后,復制到項目的static/目錄下: 

  2. 在templates/目錄下,新建index.html,根據layui官網的API(后台布局代碼),引入相關代碼: 

==注意: 
html頁面中的標簽必須要加上對應的閉合標簽或標簽內加上"/",比如:<meta></meta> 或 <meta/>等; 
在引入static/目錄下的css和js等文件時,路徑中不需要加"/static/",默認加載的是static/目錄下的文件;==

整合shiro權限控制

shiro簡介

Apache Shiro是一個功能強大、靈活的,開源的安全框架。它可以干凈利落地處理身份驗證、授權、企業會話管理和加密。

Apache Shiro的首要目標是易於使用和理解。安全通常很復雜,甚至讓人感到很痛苦,但是Shiro卻不是這樣子的。一個好的安全框架應該屏蔽復雜性,向外暴露簡單、直觀的API,來簡化開發人員實現應用程序安全所花費的時間和精力。

Shiro能做什么呢?

  • 驗證用戶身份
  • 用戶訪問權限控制,比如:1、判斷用戶是否分配了一定的安全角色。2、判斷用戶是否被授予完成某個操作的權限
  • 在非 web 或 EJB 容器的環境下可以任意使用Session API
  • 可以響應認證、訪問控制,或者 Session 生命周期中發生的事件
  • 可將一個或以上用戶安全數據源數據組合成一個復合的用戶 "view"(視圖)
  • 支持單點登錄(SSO)功能
  • 支持提供“Remember Me”服務,獲取用戶關聯信息而無需登錄

等等——都集成到一個有凝聚力的易於使用的API。根據官方的介紹,shiro提供了“身份認證”、“授權”、“加密”和“Session管理”這四個主要的核心功能
// TODO 百度

引入依賴

pom.xml中引入shiro依賴:

<!--spring boot 整合shiro依賴--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <!--shiro依賴--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>${shiro.version}</version> </dependency>

shiro.version版本為:1.3.1

shiro配置實體類

/** * @項目名稱:wyait-manage * @包名:com.wyait.manage.config * @類描述: * @創建人:wyait * @創建時間:2017-12-12 18:51 * @version:V1.0 */ @Configuration public class ShiroConfig { private static final Logger logger = LoggerFactory .getLogger(ShiroConfig.class); /** * ShiroFilterFactoryBean 處理攔截資源文件過濾器 * </br>1,配置shiro安全管理器接口securityManage; * </br>2,shiro 連接約束配置filterChainDefinitions; */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean( org.apache.shiro.mgt.SecurityManager securityManager) { //shiroFilterFactoryBean對象 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 配置shiro安全管理器 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 指定要求登錄時的鏈接 shiroFilterFactoryBean.setLoginUrl("/login"); // 登錄成功后要跳轉的鏈接 shiroFilterFactoryBean.setSuccessUrl("/index"); // 未授權時跳轉的界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // filterChainDefinitions攔截器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的鏈接 從上向下順序判斷 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/templates/**", "anon"); // 配置退出過濾器,具體的退出代碼Shiro已經替我們實現了 filterChainDefinitionMap.put("/logout", "logout"); //add操作,該用戶必須有【addOperation】權限 filterChainDefinitionMap.put("/add", "perms[addOperation]"); // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問【放行】--> filterChainDefinitionMap.put("/user/**", "authc"); shiroFilterFactoryBean .setFilterChainDefinitionMap(filterChainDefinitionMap); logger.debug("Shiro攔截器工廠類注入成功"); return shiroFilterFactoryBean; } /** * shiro安全管理器設置realm認證 * @return */ @Bean public org.apache.shiro.mgt.SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 設置realm. securityManager.setRealm(shiroRealm()); // //注入ehcache緩存管理器; securityManager.setCacheManager(ehCacheManager()); return securityManager; } /** * 身份認證realm; (賬號密碼校驗;權限等) * * @return */ @Bean public ShiroRealm shiroRealm() { ShiroRealm shiroRealm = new ShiroRealm(); return shiroRealm; } /** * ehcache緩存管理器;shiro整合ehcache: * 通過安全管理器:securityManager * @return EhCacheManager */ @Bean public EhCacheManager ehCacheManager() { logger.debug( "=====shiro整合ehcache緩存:ShiroConfiguration.getEhCacheManager()"); EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:config/ehcache.xml"); return cacheManager; } }

Filter Chain定義說明:

1、一個URL可以配置多個Filter,使用逗號分隔; 
2、當設置多個過濾器時,全部驗證通過,才視為通過; 
3、部分過濾器可指定參數,如perms,roles

Shiro內置的FilterChain:

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

anon : 所有url都都可以匿名訪問 
authc : 需要認證才能進行訪問 
user : 配置記住我或認證通過可以訪問

ShiroRealm認證實體類

/** * @項目名稱:wyait-manage * @包名:com.wyait.manage.shiro * @類描述: * @創建人:wyait * @創建時間:2017-12-13 13:53 * @version:V1.0 */ public class ShiroRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principalCollection) { //TODO return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authenticationToken) throws AuthenticationException { //TODO return null; } }

shiro使用ehcache緩存

  1. 導入依賴;
<!--shiro添加ehcache緩存 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.6</version> </dependency> <!-- 包含支持UI模版(Velocity,FreeMarker,JasperReports), 郵件服務, 腳本服務(JRuby), 緩存Cache(EHCache), 任務計划Scheduling(uartz)。 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency>
  1. 引入ehcache.xml配置文件;
<ehcache>
    <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="10000" timeToIdleSeconds="120" timeToLiveSeconds="120" maxElementsOnDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> <!-- 設定緩存的默認數據過期策略 --> <cache name="shiro" maxElementsInMemory="10000" timeToIdleSeconds="120" timeToLiveSeconds="120" maxElementsOnDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </cache> </ehcache>
  1. shiro配置類中整合ehcache做緩存管理;【參考:shiro配置實體類】

    整合thymeleaf

    • 導入pom依賴
<!--thymeleaf依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
  • 配置中禁用緩存
#關閉thymeleaf緩存 spring.thymeleaf.cache=false
  • springboot整合thymeleaf模版配置詳解:
參數 介紹
spring.thymeleaf.cache = true 啟用模板緩存(開發時建議關閉)
spring.thymeleaf.check-template = true 檢查模板是否存在,然后再呈現
spring.thymeleaf.check-template-location = true 檢查模板位置是否存在
spring.thymeleaf.content-type = text/html Content-Type值
spring.thymeleaf.enabled = true 啟用MVC Thymeleaf視圖分辨率
spring.thymeleaf.encoding = UTF-8 模板編碼
spring.thymeleaf.excluded-view-names = 應該從解決方案中排除的視圖名稱的逗號分隔列表
spring.thymeleaf.mode = HTML5 應用於模板的模板模式。另請參見StandardTemplateModeHandlers
spring.thymeleaf.prefix = classpath:/templates/ 在構建URL時預先查看名稱的前綴(默認/templates/)
spring.thymeleaf.suffix = .html 構建URL時附加查看名稱的后綴
spring.thymeleaf.template-resolver-order = 鏈中模板解析器的順序
spring.thymeleaf.view-names = 可以解析的視圖名稱的逗號分隔列表

org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties類里面有thymeleaf的默認配置。 
默認頁面映射路徑為classpath:/templates/*.html

shiro功能之記住我

shiro記住我的功能是基於瀏覽器中的cookie實現的;

  1. 在shiroConfig里面增加cookie配置
    • CookieRememberMeManager配置;
/** * 設置記住我cookie過期時間 * @return */ @Bean public SimpleCookie remeberMeCookie(){ logger.debug("記住我,設置cookie過期時間!"); //cookie名稱;對應前端的checkbox的name = rememberMe SimpleCookie scookie=new SimpleCookie("rememberMe"); //記住我cookie生效時間1小時 ,單位秒 [1小時] scookie.setMaxAge(3600); return scookie; } // 配置cookie記住我管理器 @Bean public CookieRememberMeManager rememberMeManager(){ logger.debug("配置cookie記住我管理器!"); CookieRememberMeManager cookieRememberMeManager=new CookieRememberMeManager(); cookieRememberMeManager.setCookie(remeberMeCookie()); return cookieRememberMeManager; } 
  • 將CookieRememberMeManager注入SecurityManager
//注入Cookie記住我管理器 securityManager.setRememberMeManager(rememberMeManager());
  1. 前端頁面新增rememberMe復選框
<input type="checkbox" name="rememberMe" lay-skin="primary" title="記住我"/>
  1. 登錄方法更改
//新增rememberMe參數 @RequestParam(value="rememberMe",required = false)boolean rememberMe ... ... // 1、 封裝用戶名、密碼、是否記住我到token令牌對象 [支持記住我] AuthenticationToken token = new UsernamePasswordToken( user.getMobile(), DigestUtils.md5Hex(user.getPassword()),rememberMe);
  1. 頁面cookie設置 
shiro功能之密碼錯誤次數限制

針對用戶在登錄時用戶名和密碼輸入錯誤進行次數限制,並鎖定; 
Shiro中用戶名密碼的驗證交給了CredentialsMatcher;
在CredentialsMatcher里面校驗用戶密碼,使用ehcache記錄登錄失敗次數就可以實現。

在驗證用戶名密碼之前先驗證登錄失敗次數,如果超過5次就拋出嘗試過多的異常,否則驗證用戶名密碼,驗證成功把嘗試次數清零,不成功則直接退出。這里依靠Ehcache自帶的timeToIdleSeconds來保證鎖定時間(帳號鎖定之后的最后一次嘗試間隔timeToIdleSeconds秒之后自動清除)。

  1. 自定義HashedCredentialsMatcher實現類
/** * @項目名稱:wyait-manage * @包名:com.wyait.manage.shiro * @類描述:shiro之密碼輸入次數限制6次,並鎖定2分鍾 * @創建人:wyait * @創建時間:2018年1月23日17:23:10 * @version:V1.0 */ public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { //集群中可能會導致出現驗證多過5次的現象,因為AtomicInteger只能保證單節點並發 //解決方案,利用ehcache、redis(記錄錯誤次數)和mysql數據庫(鎖定)的方式處理:密碼輸錯次數限制; 或兩者結合使用 private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { //讀取ehcache中配置的登錄限制鎖定時間 passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } /** * 在回調方法doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info)中進行身份認證的密碼匹配, * </br>這里我們引入了Ehcahe用於保存用戶登錄次數,如果登錄失敗retryCount變量則會一直累加,如果登錄成功,那么這個count就會從緩存中移除, * </br>從而實現了如果登錄次數超出指定的值就鎖定。 * @param token * @param info * @return */ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { //獲取登錄用戶名 String username = (String) token.getPrincipal(); //從ehcache中獲取密碼輸錯次數 // retryCount AtomicInteger retryCount = passwordRetryCache.get(username); if (retryCount == null) { //第一次 retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } //retryCount.incrementAndGet()自增:count + 1 if (retryCount.incrementAndGet() > 5) { // if retry count > 5 throw 超過5次 鎖定 throw new ExcessiveAttemptsException("username:"+username+" tried to login more than 5 times in period"); } //否則走判斷密碼邏輯 boolean matches = super.doCredentialsMatch(token, info); if (matches) { // clear retry count 清楚ehcache中的count次數緩存 passwordRetryCache.remove(username); } return matches; } } 

這里的邏輯也不復雜,在回調方法doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info) 
中進行身份認證的密碼匹配,這里我們引入了Ehcahe用於保存用戶登錄次數,如果登錄失敗retryCount變量則會一直累加,如果登錄成功,那么這個count就會從緩存中移除,從而實現了如果登錄次數超出指定的值就鎖定。

  1. ehcache中新增密碼重試次數緩存passwordRetryCache
<!-- 登錄記錄緩存 鎖定2分鍾 --> <cache name="passwordRetryCache" maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="0" overflowToDisk="false" statistics="false"> </cache>
  1. 在shiroConfig配置類中添加HashedCredentialsMatcher憑證匹配器
/** * 憑證匹配器 (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 * 所以我們需要修改下doGetAuthenticationInfo中的代碼,更改密碼生成規則和校驗的邏輯一致即可; ) * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher(ehCacheManager()); //new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:這里使用MD5算法; hashedCredentialsMatcher.setHashIterations(1);// 散列的次數,比如散列兩次,相當於 // md5(md5("")); return hashedCredentialsMatcher; }
  1. 設置ShiroRealm密碼匹配使用自定義的HashedCredentialsMatcher實現類
//使用自定義的CredentialsMatcher進行密碼校驗和輸錯次數限制 shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
  1. 更改ShiroRealm類doGetAuthenticationInfo登錄認證方法

更改密碼加密規則,和自定義的HashedCredentialsMatcher匹配器加密規則保持一致;

// 第一個參數 ,登陸后,需要在session保存數據 // 第二個參數,查詢到密碼(加密規則要和自定義的HashedCredentialsMatcher中的HashAlgorithmName散列算法一致) // 第三個參數 ,realm名字 new SimpleAuthenticationInfo(user, DigestUtils.md5Hex(user.getPassword()), getName());
  1. login方法的改動;

controller層獲取登錄失敗次數;登錄頁面新增用戶、密碼輸錯次數提醒;

//注入ehcache管理器 @Autowired private EhCacheManager ecm; ... ... //登錄方法中,獲取失敗次數,並設置友情提示信息 Cache<String, AtomicInteger> passwordRetryCache= ecm.getCache("passwordRetryCache"); if(null!=passwordRetryCache){ int retryNum=(passwordRetryCache.get(existUser.getMobile())==null?0:passwordRetryCache.get(existUser.getMobile())).intValue(); logger.debug("輸錯次數:"+retryNum); if(retryNum>0 && retryNum<6){ responseResult.setMessage("用戶名或密碼錯誤"+retryNum+"次,再輸錯"+(6-retryNum)+"次賬號將鎖定"); } }
  1. 后台新增用戶解鎖操作;清除ehcache中的緩存即可; 
    TODO 
    用戶列表,解鎖按鈕,點擊,彈出輸入框,讓用戶管理員輸入需要解鎖的用戶手機號,進行解鎖操作即可;
Cache<String, AtomicInteger> passwordRetryCache= ecm.getCache("passwordRetryCache"); //username是緩存key passwordRetryCache..remove(username); 

thymeleaf整合shiro

html頁面使用thymeleaf模版;

  • 導入pom依賴
<!--thymeleaf-shiro標簽--> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>1.2.1</version> </dependency>

thymeleaf整合shiro的依賴:thymeleaf-extras-shiro最新版本是2.0.0,配置使用報錯,所以使用1.2.1版本; 
該jar包的github地址:https://github.com/theborakompanioni/thymeleaf-extras-shiro

  • 配置shiroDirect
@Bean public ShiroDialect shiroDialect(){ return new ShiroDialect(); }

這段代碼放在ShiroConfig配置類里面即可。

  • 頁面中使用
<html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> ... ... <!-- 獲取shiro中登錄的用戶名 --> <shiro:principal property="username"></shiro:principal>

具體用法,參考:https://github.com/theborakompanioni/thymeleaf-extras-shiro

整合pageHelper

  • 導入pom依賴
<dependency>
    <!-- pageHelper分頁插件 --> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.3</version> </dependency>
  • 添加配置
# pagehelper參數配置 pagehelper.helperDialect=mysql pagehelper.reasonable=true pagehelper.supportMethodsArguments=true pagehelper.returnPageInfo=check pagehelper.params=count=countSql
  • 代碼中使用
//PageHelper放在查詢方法前即可 PageHelper.startPage(page, limit); List<UserRoleDTO> urList = userMapper.getUsers(userSearch); ... ... //獲取分頁查詢后的pageInfo對象數據 PageInfo<UserRoleDTO> pageInfo = new PageInfo<>(urList); //pageInfo中獲取到的總記錄數total: pageInfo.getTotal();

PageInfo對象中的數據和用法,詳見源碼!

整合ztree

詳見ztree官網:http://www.treejs.cn/v3/api.php

整合httpClient

  • 導入pom依賴
<!-- httpclient --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.3</version> </dependency> <!-- 提供FileBody、StringBody和MultipartEntity 使用httpClient上傳文件需要的類 --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpmime</artifactId> <version>4.5.3</version> </dependency>
  • 配置類
/** * @項目名稱:wyait-manage * @包名:com.wyait.manage.config * @類描述: * @創建人:wyait * @創建時間:2018-01-11 9:13 * @version:V1.0 */ @Configuration public class HttpClientConfig { private static final Logger logger = LoggerFactory .getLogger(ShiroConfig.class); /** * 連接池最大連接數 */ @Value("${httpclient.config.connMaxTotal}") private int connMaxTotal = 20; /** * */ @Value("${httpclient.config.maxPerRoute}") private int maxPerRoute = 20; /** * 連接存活時間,單位為s */ @Value("${httpclient.config.timeToLive}") private int timeToLive = 10; /** * 配置連接池 * @return */ @Bean(name="poolingClientConnectionManager") public PoolingHttpClientConnectionManager poolingClientConnectionManager(){ PoolingHttpClientConnectionManager poolHttpcConnManager = new PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS); // 最大連接數 poolHttpcConnManager.setMaxTotal(this.connMaxTotal); // 路由基數 poolHttpcConnManager.setDefaultMaxPerRoute(this.maxPerRoute); return poolHttpcConnManager; } @Value("${httpclient.config.connectTimeout}") private int connectTimeout = 3000; @Value("${httpclient.config.connectRequestTimeout}") private int connectRequestTimeout = 2000; @Value("${httpclient.config.socketTimeout}") private int socketTimeout = 3000; /** * 設置請求配置 * @return */ @Bean public RequestConfig config(){ return RequestConfig.custom() .setConnectionRequestTimeout(this.connectRequestTimeout) .setConnectTimeout(this.connectTimeout) .setSocketTimeout(this.socketTimeout) .build(); } @Value("${httpclient.config.retryTime}")// 此處建議采用@ConfigurationProperties(prefix="httpclient.config")方式,方便復用 private int retryTime; /** * 重試策略 * @return */ @Bean public HttpRequestRetryHandler httpRequestRetryHandler() { // 請求重試 final int retryTime = this.retryTime; return new HttpRequestRetryHandler() { public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { // Do not retry if over max retry count,如果重試次數超過了retryTime,則不再重試請求 if (executionCount >= retryTime) { return false; } // 服務端斷掉客戶端的連接異常 if (exception instanceof NoHttpResponseException) { return true; } // time out 超時重試 if (exception instanceof InterruptedIOException) { return true; } // Unknown host if (exception instanceof UnknownHostException) { return false; } // Connection refused if (exception instanceof ConnectTimeoutException) { return false; } // SSL handshake exception if (exception instanceof SSLException) { return false; } HttpClientContext clientContext = HttpClientContext.adapt(context); HttpRequest request = clientContext.getRequest(); if (!(request instanceof HttpEntityEnclosingRequest)) { return true; } return false; } }; } /** * 創建httpClientBuilder對象 * @param httpClientConnectionManager * @return */ @Bean(name = "httpClientBuilder") public HttpClientBuilder getHttpClientBuilder(@Qualifier("poolingClientConnectionManager")PoolingHttpClientConnectionManager httpClientConnectionManager){ return HttpClients.custom().setConnectionManager(httpClientConnectionManager) .setRetryHandler(this.httpRequestRetryHandler()) //.setKeepAliveStrategy(connectionKeepAliveStrategy()) //.setRoutePlanner(defaultProxyRoutePlanner()) .setDefaultRequestConfig(this.config()); } /** * 自動釋放連接 * @param httpClientBuilder * @return */ @Bean public CloseableHttpClient getCloseableHttpClient(@Qualifier("httpClientBuilder") HttpClientBuilder httpClientBuilder){ return httpClientBuilder.build(); }
  • 封裝公用類
    參考項目源碼:HttpService HttpResult
  • 使用

數據校驗

本項目中數據校驗,前台統一使用自定義的正則校驗;后台使用兩種校驗方式供大家選擇使用;

oval注解校驗

//TODO 
Google或百度

自定義正則校驗

參考:ValidateUtil.java和checkParam.js

數據庫設計

表結構

用戶user、角色role、權限permission以及中間表(user_role、role_permission)共五張表; 
實現按鈕級別的權限控制。 
建表SQL源碼:github

數據源配置

單庫(數據源)配置

spring boot默認自動加載單庫配置,只需要在application.properties文件中添加mysql配置即可;

# mysql spring.datasource.url=jdbc:mysql://localhost:3306/wyait?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver # 使用druid連接池 需要注意的是:spring.datasource.type舊的spring boot版本是不能識別的。 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource # mybatis mybatis.type-aliases-package=com.wyait.manage.pojo mybatis.mapper-locations=classpath:mapper/*.xml # 開啟駝峰映射 mybatis.configuration.map-underscore-to-camel-case=true 

多數據源配置

方式一:利用spring加載配置,注冊bean的邏輯進行多數據源配置
  • 配置文件:
# 多數據源配置 slave.datasource.names=test,test1 slave.datasource.test.driverClassName =com.mysql.jdbc.Driver slave.datasource.test.url=jdbc:mysql://localhost:3306/test?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true slave.datasource.test.username=root slave.datasource.test.password=123456 # test1 slave.datasource.test1.driverClassName =com.mysql.jdbc.Driver slave.datasource.test1.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true slave.datasource.test1.username=root slave.datasource.test1.password=123456
  • 配置類
/** * @項目名稱:wyait-manage * @類名稱:MultipleDataSource * @類描述:創建多數據源注冊到Spring中 * @創建人:wyait * @創建時間:2017年12月19日 下午2:49:34 * @version: */ //@Configuration @SuppressWarnings("unchecked") public class MultipleDataSource implements BeanDefinitionRegistryPostProcessor,EnvironmentAware{ //作用域對象. private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver(); //bean名稱生成器. private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator(); //如配置文件中未指定數據源類型,使用該默認值 private static final Object DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource"; // 存放DataSource配置的集合; private Map<String, Map<String, Object>> dataSourceMap = new HashMap<String, Map<String, Object>>(); @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { System.out.println("MultipleDataSourceBeanDefinitionRegistryPostProcessor.postProcessBeanFactory()"); //設置為主數據源; beanFactory.getBeanDefinition("dataSource").setPrimary(true); if(!dataSourceMap.isEmpty()){ //不為空的時候. BeanDefinition bd = null; Map<String, Object> dsMap = null; MutablePropertyValues mpv = null; for (Entry<String, Map<String, Object>> entry : dataSourceMap.entrySet()) { bd = beanFactory.getBeanDefinition(entry.getKey()); mpv = bd.getPropertyValues(); dsMap = entry.getValue(); mpv.addPropertyValue("driverClassName", dsMap.get("driverClassName")); mpv.addPropertyValue("url", dsMap.get("url")); mpv.addPropertyValue("username", dsMap.get("username")); mpv.addPropertyValue("password", dsMap.get("password")); } } } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { System.out.println("MultipleDataSourceBeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry()"); try { if(!dataSourceMap.isEmpty()){ //不為空的時候,進行注冊bean. for(Entry<String,Map<String,Object>> entry:dataSourceMap.entrySet()){ Object type = entry.getValue().get("type");//獲取數據源類型 if(type == null){ type= DATASOURCE_TYPE_DEFAULT; } registerBean(registry, entry.getKey(),(Class<? extends DataSource>)Class.forName(type.toString())); } } } catch (ClassNotFoundException e) { //異常捕捉. e.printStackTrace(); } } /** * 注意重寫的方法 setEnvironment 是在系統啟動的時候被執行。 * 這個方法主要是:加載多數據源配置 * 從application.properties文件中進行加載; */ @Override public void setEnvironment(Environment environment) { System.out.println("MultipleDataSourceBeanDefinitionRegistryPostProcessor.setEnvironment()"); /* * 獲取application.properties配置的多數據源配置,添加到map中,之后在postProcessBeanDefinitionRegistry進行注冊。 */ //獲取到前綴是"slave.datasource." 的屬性列表值. RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(environment,"slave.datasource."); //獲取到所有數據源的名稱. String dsPrefixs = propertyResolver.getProperty("names"); String[] dsPrefixsArr = dsPrefixs.split(","); for(String dsPrefix:dsPrefixsArr){ /* * 獲取到子屬性,對應一個map; * 也就是這個map的key就是 * type、driver-class-name等; */ Map<String, Object> dsMap = propertyResolver.getSubProperties(dsPrefix + "."); //存放到一個map集合中,之后在注入進行使用. dataSourceMap.put(dsPrefix, dsMap); } } /** * 注冊Bean到Spring */ private void registerBean(BeanDefinitionRegistry registry, String name, Class<?> beanClass) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); // 可以自動生成name String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, registry)); AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry); } }

接口:BeanDefinitionRegistryPostProcessor只要是注入bean, 
接口:接口 EnvironmentAware 重寫方法 setEnvironment ; 可以在工程啟動時,獲取到系統環境變量和application配置文件中的變量。

該配置類的加載順序是: 
setEnvironment()-->postProcessBeanDefinitionRegistry() --> postProcessBeanFactory()

  1. 在setEnvironment()方法中主要是讀取了application.properties的配置;
  1. 在postProcessBeanDefinitionRegistry()方法中主要注冊為spring的bean對象;

  2. 在postProcessBeanFactory()方法中主要是注入從setEnvironment方法中讀取的application.properties配置信息。

參考博客:http://412887952-qq-com.iteye.com/blog/2302997

方式二:使用配置類

注釋掉spring.datasource數據連接配置以及mybatis掃碼包和加載xml配置等,統一使用配置類進行配置實現;application.properties中的數據源配置,spring加載時默認是單數據源配置,所以相關的配置都注釋掉,統一使用Config配置類進行配置!具體配置方法如下:

  • 配置文件
# 多數據源配置 #slave.datasource.names=test,test1 slave.datasource.test.driverClassName =com.mysql.jdbc.Driver slave.datasource.test.url=jdbc:mysql://localhost:3306/test?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true slave.datasource.test.username=root slave.datasource.test.password=123456 # test1 slave.datasource.test1.driverClassName =com.mysql.jdbc.Driver slave.datasource.test1.url=jdbc:mysql://localhost:3306/test1?useUnicode=true&zeroDateTimeBehavior=convertToNull&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true slave.datasource.test1.username=root slave.datasource.test1.password=123456 # mybatis #mybatis.type-aliases-package=com.wyait.manage.pojo #mybatis.mapper-locations=classpath:mapper/*.xml # 開啟駝峰映射 #mybatis.configuration.map-underscore-to-camel-case=true
  • 配置類 
    多數據源多個配置類: 
    第一個數據源test配置DataSourceConfig:
/** * @項目名稱:wyait-common * @包名:com.wyait.manage.config * @類描述:數據源配置 * @創建人:wyait * @創建時間:2018-02-27 13:33 * @version:V1.0 */ @Configuration //指明了掃描dao層,並且給dao層注入指定的SqlSessionTemplate @MapperScan(basePackages = "com.wyait.manage.dao", sqlSessionTemplateRef = "testSqlSessionTemplate") public class DataSourceConfig { /** * 創建datasource對象 * @return */ @Bean(name = "testDataSource") @ConfigurationProperties(prefix = "slave.datasource.test")// prefix值必須是application.properteis中對應屬性的前綴 @Primary public DataSource testDataSource() { return DataSourceBuilder.create().build(); } /** * 創建sql工程 * @param dataSource * @return * @throws Exception */ @Bean(name = "testSqlSessionFactory") @Primary public SqlSessionFactory testSqlSessionFactory(@Qualifier("testDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //對應mybatis.type-aliases-package配置 bean.setTypeAliasesPackage("com.wyait.manage.pojo"); //對應mybatis.mapper-locations配置 bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); //開啟駝峰映射 bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true); return bean.getObject(); } /** * 配置事務管理 * @param dataSource * @return */ @Bean(name = "testTransactionManager") @Primary public DataSourceTransactionManager testTransactionManager(@Qualifier("testDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } /** * sqlSession模版,用於配置自動掃描pojo實體類 * @param sqlSessionFactory * @return * @throws Exception */ @Bean(name = "testSqlSessionTemplate") @Primary public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }

第二個數據源test1,TestDataSourceConfig配置類

/** * @項目名稱:wyait-common * @包名:com.wyait.manage.config * @類描述:數據源配置 * @創建人:wyait * @創建時間:2018-02-27 13:33 * @version:V1.0 */ //@Configuration //指明了掃描dao層,並且給dao層注入指定的SqlSessionTemplate @MapperScan(basePackages = "com.wyait.manage.test1", sqlSessionTemplateRef = "test1SqlSessionTemplate") public class TestDataSourceConfig { /** * 創建datasource對象 * @return */ @Bean(name = "test1DataSource") @ConfigurationProperties(prefix = "slave.datasource.test1")// prefix值必須是application.properteis中對應屬性的前綴 public DataSource test1DataSource() { return DataSourceBuilder.create().build(); } /** * 創建sql工程 * @param dataSource * @return * @throws Exception */ @Bean(name = "test1SqlSessionFactory") public SqlSessionFactory test1SqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //對應mybatis.type-aliases-package配置 bean.setTypeAliasesPackage("com.wyait.manage.pojo"); bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); //開啟駝峰映射 bean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true); return bean.getObject(); } /** * 配置事務管理 * @param dataSource * @return */ @Bean(name = "test1TransactionManager") public DataSourceTransactionManager test1TransactionManager(@Qualifier("test1DataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } /** * sqlSession模版,用於配置自動掃描pojo實體類 * @param sqlSessionFactory * @return * @throws Exception */ @Bean(name = "test1SqlSessionTemplate") public SqlSessionTemplate test1SqlSessionTemplate(@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } }
  • //TODO 創建不同的數據表和對應的查詢方法進行測試;

界面效果

登錄界面

++關於登錄,其中圖片驗證碼、短信驗證碼等校驗的代碼注釋掉了,做了簡單的實現,大家可以根據各自的需要可以打開並重新實現。++

默認密碼:654321

主界面

動態菜單的實現

  1. 查找所有菜單;
  2. 循環中判斷該菜單下是否有子菜單,如果有,生成子菜單目錄; 
    【目前只實現了父子兩級目錄;原因是前端依賴的layui目前只有兩級目錄的效果;可自行擴展添加】
  3. 判斷當前頁面請求路徑href是否包含菜單中的page,包含就回顯選中。 
    詳見代碼實現!

由於主體顯示的區域,沒有采用iframe引用的方式,再進行功能操作的時候,當請求的href不再菜單的page中時,會出現頁面刷新,但是菜單無法回顯選中的問題;

解決方案: 
在進行頁面跳轉的時候,拼接一個callback參數,參數值為未跳轉前的頁面uri路徑值;代碼如下:

  • common.js:
/** * 獲取get請求參數 * @param name * @returns */ function GetQueryString(name){ var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)"); var search=window.location.search; if(search!=null && search!=""){ var r = search.substr(1).match(reg); if(r!=null){ return unescape(r[2]); } } return null; } /** * 獲取菜單uri * @returns */ function getCallback(){ var pathname = window.location.pathname; var param=GetQueryString("callback"); //console.log("pathname:"+pathname); //console.log("param:"+param); if(param!=null && param != ""){ return param; }else{ return pathname; } }
  • 菜單子頁面代碼示例:
    //獲取當前頁面請求的uri function update(id){ window.location.href="/demo/update.html?id="+id+"&callback="+getCallback(); }

    這樣頁面在請求到新頁面后,依然包含了菜單頁面的page uri,可以實現動態菜單中回顯選中的效果。 
    當然,如果項目中使用iframe引用,就不存在該問題!

用戶管理


修改用戶: 

角色管理


權限管理


layui.tree,目前layui針對tree的開發並不完善,復選框、回顯選中、獲取選中的id等都需要自己擴展實現,所以不建議使用; 
這里用了一個treegrid,針對獲取復選框選中的數據id,自己改了相關的tree.js源碼實現的。 
在權限修改功能中,考慮到回顯選中,還需要改動,就改用了ztree實現。

總結

技術實現有多種方案,我這里選擇了我之前沒用過的方案;里面也采用了多種寫法,踩了不少坑。這次的項目分享,只實現了簡單的用戶、角色、權限管理的功能;大家可以根據各自的業務需求,進行改動;

權限這一塊,比較成熟的有:Apache shiro和Spring security,這里使用簡單易用的shiro,感興趣的可以Google對比下。

關於layui的使用,用過之后才發現,layui的插件確實好用,比如:layer彈框、laypage分頁、laydate日期等,確實好用;但是layui作為前端框架,上手需要時間來學習它的API;

后續會根據大家的反饋進行更新!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM