後臺管理系統
版本更新
後續版本更新內容
鏈接入口:
- springboot + shiro之登錄人數限制、登錄判斷重定向、session時間設置:http://blog.51cto.com/wyait/2107423
- springboot + shiro 動態更新用戶信息:http://blog.51cto.com/wyait/2112200
- springboot + shiro 權限註解、統一異常處理、請求亂碼解決 :http://blog.51cto.com/wyait/2125708
- shiro併發登陸人數控制(超出登錄用戶最大配置數量,清理用戶)功能;
- 解決父子頁面判斷用戶未登錄之後,重定向到頁面中嵌套顯示登錄界面問題;
- 解決ajax請求,判斷用戶未登錄之後,重定向到登錄頁面問題;
- 解決完成了功能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的整合,參考博客:
http://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會在靜態資源配置中依次找到匹配的目錄,然後加載靜態資源;
自定義靜態資源配置方法
自定義靜態資源配置方法,參考博客:http://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/
-
將下載的layui解壓後,複製到項目的static/目錄下:
- 在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緩存
- 導入依賴;
<!--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>
- 引入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>
- 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實現的;
- 在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());
- 前端頁面新增rememberMe複選框
<input type="checkbox" name="rememberMe" lay-skin="primary" title="記住我"/>
- 登錄方法更改
//新增rememberMe參數
@RequestParam(value="rememberMe",required = false)boolean rememberMe
... ...
// 1、 封裝用戶名、密碼、是否記住我到token令牌對象 [支持記住我]
AuthenticationToken token = new UsernamePasswordToken(
user.getMobile(), DigestUtils.md5Hex(user.getPassword()),rememberMe);
- 頁面cookie設置
shiro功能之密碼錯誤次數限制
針對用戶在登錄時用戶名和密碼輸入錯誤進行次數限制,並鎖定;
Shiro中用戶名密碼的驗證交給了CredentialsMatcher;
在CredentialsMatcher裏面校驗用戶密碼,使用ehcache記錄登錄失敗次數就可以實現。
在驗證用戶名密碼之前先驗證登錄失敗次數,如果超過5次就拋出嘗試過多的異常,否則驗證用戶名密碼,驗證成功把嘗試次數清零,不成功則直接退出。這裏依靠Ehcache自帶的timeToIdleSeconds來保證鎖定時間(帳號鎖定之後的最後一次嘗試間隔timeToIdleSeconds秒之後自動清除)。
- 自定義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就會從緩存中移除,從而實現瞭如果登錄次數超出指定的值就鎖定。
- ehcache中新增密碼重試次數緩存passwordRetryCache
<!-- 登錄記錄緩存 鎖定2分鐘 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="false">
</cache>
- 在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;
}
- 設置ShiroRealm密碼匹配使用自定義的HashedCredentialsMatcher實現類
//使用自定義的CredentialsMatcher進行密碼校驗和輸錯次數限制
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
- 更改ShiroRealm類doGetAuthenticationInfo登錄認證方法
更改密碼加密規則,和自定義的HashedCredentialsMatcher匹配器加密規則保持一致;
// 第一個參數 ,登陸後,需要在session保存數據
// 第二個參數,查詢到密碼(加密規則要和自定義的HashedCredentialsMatcher中的HashAlgorithmName散列算法一致)
// 第三個參數 ,realm名字
new SimpleAuthenticationInfo(user, DigestUtils.md5Hex(user.getPassword()),
getName());
- 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)+"次賬號將鎖定");
}
}
- 後臺新增用戶解鎖操作;清除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()
- 在setEnvironment()方法中主要是讀取了application.properties的配置;
-
在postProcessBeanDefinitionRegistry()方法中主要註冊爲spring的bean對象;
- 在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
主界面
動態菜單的實現
- 查找所有菜單;
- 循環中判斷該菜單下是否有子菜單,如果有,生成子菜單目錄;
【目前只實現了父子兩級目錄;原因是前端依賴的layui目前只有兩級目錄的效果;可自行擴展添加】 - 判斷當前頁面請求路徑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;
後續會根據大家的反饋進行更新!
20180422版本更新內容
- 優化更新用戶時,記錄操作用戶id;
- 優化用戶列表默認排序;
- 優化開通用戶後,再次添加用戶,上次操作數據未清除問題;
- 優化多設備同時登陸時,有效時間內驗證碼衝突問題;
- 優化登錄失敗時停止短信驗證碼倒計時功能;
- 優化Controller層返回值不準確等問題。
20180426版本更新內容
- 編輯用戶自己成功後,執行退出,重新登錄信息生效;
- 禁止用戶刪除自己;
- 優化用戶列表操作信息提示;
- 角色管理列表,通過添加參數callback,實現菜單回顯選中;
20180503版本更新內容
- 新增用戶表version版本字段;
- 更新用戶操作,通過version字段來保證數據一致;
- 新增通過攔截器實現動態更新用戶信息(同步更新在線用戶信息);
- 新增登錄成功後默認頁面home.html;
- 頁面操作細節優化。
spring boot + shiro 動態更新用戶信息
鏈接入口--> spring boot + shiro 動態更新用戶信息:http://blog.51cto.com/wyait/2112200
20180606版本更新內容
- 新增shiro權限註解;
- 請求亂碼問題解決;
- 統一異常處理;
- 頁面操作細節優化。
springboot + shiro 權限註解、統一異常處理、請求亂碼解決
鏈接入口--> springboot + shiro 權限註解、統一異常處理、請求亂碼解決 :http://blog.51cto.com/wyait/2125708
TODO
- 後臺方法級別權限控制,通過shiro配置可實現;具體用戶管理的操作根據業務實際的需求可做調整;
以上更新,項目wyait-manage、wyait-manage-1.2.0源碼同步更新。
新增功能:
- springboot + shiro之登錄人數限制、登錄判斷重定向、session時間設置:http://blog.51cto.com/wyait/2107423
- springboot + shiro 動態更新用戶信息:http://blog.51cto.com/wyait/2112200
- springboot + shiro 權限註解、統一異常處理、請求亂碼解決 :http://blog.51cto.com/wyait/2125708
項目源碼:(包含數據庫源碼)
github源碼: https://github.com/wyait/manage.git
碼雲:https://gitee.com/wyait/manage.git
wyait-common工具項目,源碼地址 :
github:https://github.com/wyait/project.git
碼雲:https://gitee.com/wyait/project.git