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的整合,參考博客:
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/配置的目錄下,再加一層或一層以上的目錄。如圖:
image

比如:保存圖片時,一般會根據年月日進行分目錄,實際圖片保存在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/

  1. 將下載的layui解壓後,複製到項目的static/目錄下:
    image

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

==注意:
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設置
    image
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 創建不同的數據表和對應的查詢方法進行測試;

界面效果

登錄界面

image

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

默認密碼:654321

主界面

image

動態菜單的實現

  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引用,就不存在該問題!

用戶管理

image
修改用戶:
image

角色管理

image
image

權限管理

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

總結

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

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

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

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

20180422版本更新內容

  1. 優化更新用戶時,記錄操作用戶id;
  2. 優化用戶列表默認排序;
  3. 優化開通用戶後,再次添加用戶,上次操作數據未清除問題;
  4. 優化多設備同時登陸時,有效時間內驗證碼衝突問題;
  5. 優化登錄失敗時停止短信驗證碼倒計時功能;
  6. 優化Controller層返回值不準確等問題。

20180426版本更新內容

  1. 編輯用戶自己成功後,執行退出,重新登錄信息生效;
  2. 禁止用戶刪除自己;
  3. 優化用戶列表操作信息提示;
  4. 角色管理列表,通過添加參數callback,實現菜單回顯選中;

20180503版本更新內容

  1. 新增用戶表version版本字段;
  2. 更新用戶操作,通過version字段來保證數據一致;
  3. 新增通過攔截器實現動態更新用戶信息(同步更新在線用戶信息);
  4. 新增登錄成功後默認頁面home.html;
  5. 頁面操作細節優化。

spring boot + shiro 動態更新用戶信息

鏈接入口--> spring boot + shiro 動態更新用戶信息:http://blog.51cto.com/wyait/2112200

20180606版本更新內容

  1. 新增shiro權限註解;
  2. 請求亂碼問題解決;
  3. 統一異常處理;
  4. 頁面操作細節優化。

springboot + shiro 權限註解、統一異常處理、請求亂碼解決

鏈接入口--> springboot + shiro 權限註解、統一異常處理、請求亂碼解決 :http://blog.51cto.com/wyait/2125708

TODO

  • 後臺方法級別權限控制,通過shiro配置可實現;具體用戶管理的操作根據業務實際的需求可做調整;

以上更新,項目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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章