連接入口:php
其餘時間的版本更新,詳見本文末尾或git項目更新日誌!css
使用redis記錄驗證碼;html
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.gitjava
spring boot + mybatis的整合,參考博客:
https://blog.51cto.com/wyait/1969626mysql
靜態資源路徑是指系統能夠直接訪問的路徑,且路徑下的全部文件都可被用戶直接讀取。nginx
在Springboot中默認的靜態資源路徑有:classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,從這裏能夠看出這裏的靜態資源路徑都是在classpath中(也就是在項目路徑下指定的這幾個文件夾)git
試想這樣一種狀況:一個網站有文件上傳文件的功能,若是被上傳的文件放在上述的那些文件夾中會有怎樣的後果?github
網站數據與程序代碼不能有效分離; 當項目被打包成一個.jar文件部署時,再將上傳的文件放到這個.jar文件中是有多麼低的效率; 網站數據的備份將會很痛苦。
此時可能最佳的解決辦法是將靜態資源路徑設置到磁盤的某個目錄。與應用程序分離。web
在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官網:http://www.layui.com
layui下載地址:https://github.com/sentsin/layui/
將下載的layui解壓後,複製到項目的static/目錄下:
==注意:
html頁面中的標籤必需要加上對應的閉合標籤或標籤內加上"/",好比:<meta></meta> 或 <meta/>等;
在引入static/目錄下的css和js等文件時,路徑中不須要加"/static/",默認加載的是static/目錄下的文件;==
Apache Shiro是一個功能強大、靈活的,開源的安全框架。它能夠乾淨利落地處理身份驗證、受權、企業會話管理和加密。
Apache Shiro的首要目標是易於使用和理解。安全一般很複雜,甚至讓人感到很痛苦,可是Shiro卻不是這樣子的。一個好的安全框架應該屏蔽複雜性,向外暴露簡單、直觀的API,來簡化開發人員實現應用程序安全所花費的時間和精力。
Shiro能作什麼呢?
等等——都集成到一個有凝聚力的易於使用的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
/** * @項目名稱: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定義說明:
一、一個URL能夠配置多個Filter,使用逗號分隔;
二、當設置多個過濾器時,所有驗證經過,才視爲經過;
三、部分過濾器可指定參數,如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 : 配置記住我或認證經過能夠訪問
/** * @項目名稱: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緩存 --> <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> <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>
<!--thymeleaf依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
#關閉thymeleaf緩存 spring.thymeleaf.cache=false
參數 | 介紹 |
---|---|
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實現的;
/** * 設置記住我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; }
//注入Cookie記住我管理器 securityManager.setRememberMeManager(rememberMeManager());
<input type="checkbox" name="rememberMe" lay-skin="primary" title="記住我"/>
//新增rememberMe參數 @RequestParam(value="rememberMe",required = false)boolean rememberMe ... ... // 一、 封裝用戶名、密碼、是否記住我到token令牌對象 [支持記住我] AuthenticationToken token = new UsernamePasswordToken( user.getMobile(), DigestUtils.md5Hex(user.getPassword()),rememberMe);
shiro功能之密碼錯誤次數限制
針對用戶在登陸時用戶名和密碼輸入錯誤進行次數限制,並鎖定;
Shiro中用戶名密碼的驗證交給了CredentialsMatcher;
在CredentialsMatcher裏面校驗用戶密碼,使用ehcache記錄登陸失敗次數就能夠實現。
在驗證用戶名密碼以前先驗證登陸失敗次數,若是超過5次就拋出嘗試過多的異常,不然驗證用戶名密碼,驗證成功把嘗試次數清零,不成功則直接退出。這裏依靠Ehcache自帶的timeToIdleSeconds來保證鎖定時間(賬號鎖定以後的最後一次嘗試間隔timeToIdleSeconds秒以後自動清除)。
/** * @項目名稱: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就會從緩存中移除,從而實現了若是登陸次數超出指定的值就鎖定。
<!-- 登陸記錄緩存 鎖定2分鐘 --> <cache name="passwordRetryCache" maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="0" overflowToDisk="false" statistics="false"> </cache>
/** * 憑證匹配器 (因爲咱們的密碼校驗交給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; }
//使用自定義的CredentialsMatcher進行密碼校驗和輸錯次數限制 shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
更改密碼加密規則,和自定義的HashedCredentialsMatcher匹配器加密規則保持一致;
// 第一個參數 ,登錄後,須要在session保存數據 // 第二個參數,查詢到密碼(加密規則要和自定義的HashedCredentialsMatcher中的HashAlgorithmName散列算法一致) // 第三個參數 ,realm名字 new SimpleAuthenticationInfo(user, DigestUtils.md5Hex(user.getPassword()), getName());
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)+"次帳號將鎖定"); } }
Cache<String, AtomicInteger> passwordRetryCache= ecm.getCache("passwordRetryCache"); //username是緩存key passwordRetryCache..remove(username);
html頁面使用thymeleaf模版;
<!--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
@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
<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官網:http://www.treejs.cn/v3/api.php
<!-- 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(); }
本項目中數據校驗,前臺統一使用自定義的正則校驗;後臺使用兩種校驗方式供你們選擇使用;
//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()
在postProcessBeanDefinitionRegistry()方法中主要註冊爲spring的bean對象;
參考博客: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
/** * @項目名稱: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); } }
++關於登陸,其中圖片驗證碼、短信驗證碼等校驗的代碼註釋掉了,作了簡單的實現,你們能夠根據各自的須要能夠打開並從新實現。++
默認密碼:654321
因爲主體顯示的區域,沒有采用iframe引用的方式,再進行功能操做的時候,當請求的href再也不菜單的page中時,會出現頁面刷新,可是菜單沒法回顯選中的問題;
解決方案:
在進行頁面跳轉的時候,拼接一個callback參數,參數值爲未跳轉前的頁面uri路徑值;代碼以下:
/** * 獲取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;
後續會根據你們的反饋進行更新!
連接入口--> spring boot + shiro 動態更新用戶信息:https://blog.51cto.com/wyait/2112200
連接入口--> springboot + shiro 權限註解、統一異常處理、請求亂碼解決 :https://blog.51cto.com/wyait/2125708
以上更新,項目wyait-manage、wyait-manage-1.2.0源碼同步更新。
新增功能:
項目源碼:(包含數據庫源碼)
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