SpringBoot 優雅的整合 Shiro


Apache Shiro是一個功能強大且易於使用的Java安全框架,可執行身份驗證,授權,加密和會話管理。借助Shiro易於理解的API,您可以快速輕松地保護任何應用程序 - 從最小的移動應用程序到最大的Web和企業應用程序。網上找到大部分文章都是以前SpringMVC下的整合方式,很多人都不知道shiro提供了官方的starter可以方便地跟SpringBoot整合。

請看shiro官網關於springboot整合shiro的鏈接:Integrating Apache Shiro into Spring-Boot Applications

整合准備
這篇文檔的介紹也相當簡單。我們只需要按照文檔說明,然后在spring容器中注入一個我們自定義的Realm,shiro通過這個realm就可以知道如何獲取用戶信息來處理鑒權(Authentication),如何獲取用戶角色、權限信息來處理授權(Authorization)。如果是web應用程序的話需要引入shiro-spring-boot-web-starter,單獨的應用程序的話則引入shiro-spring-boot-starter。

依賴

<dependency>     <groupId>org.apache.shiro</groupId>     <artifactId>shiro-spring-boot-web-starter</artifactId>     <version>1.4.0-RC2</version> </dependency>


用戶實體
首先創建一個用戶的實體,用來做認證

package com.maoxs.pojo; import lombok.Data; import java.io.Serializable; import java.util.Date; import java.util.HashSet; import java.util.Set; @Data public class User  implements Serializable {     private Long uid;       // 用戶id     private String uname;   // 登錄名,不可改     private String nick;    // 用戶昵稱,可改     private String pwd;     // 已加密的登錄密碼     private String salt;    // 加密鹽值     private Date created;   // 創建時間     private Date updated;   // 修改時間     private Set<String> roles = new HashSet<>();    //用戶所有角色值,用於shiro做角色權限的判斷     private Set<String> perms = new HashSet<>();    //用戶所有權限值,用於shiro做資源權限的判斷 } 

這里了為了方便,就不去數據庫讀取了,方便測試我們把,權限信息,角色信息,認證信息都靜態模擬下。


Resources

package com.maoxs.service; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.Set; @Service public class ResourcesService {     /**      * 模擬根據用戶id查詢返回用戶的所有權限      *      * @param uid      * @return      */     public Set<String> getResourcesByUserId(Long uid) {         Set<String> perms = new HashSet<>();         //三種編程語言代表三種角色:js程序員、java程序員、c++程序員         //docker的權限         perms.add("docker:run");         perms.add("docker:ps");         //maven的權限         perms.add("mvn:debug");         perms.add("mvn:test");         perms.add("mvn:install");         //node的權限         perms.add("npm:clean");         perms.add("npm:run");         perms.add("npm:test");         return perms;     } }

Role

package com.maoxs.service; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.Set; @Service public class RoleService {     /**      * 模擬根據用戶id查詢返回用戶的所有角色      *      * @param uid      * @return      */     public Set<String> getRolesByUserId(Long uid) {         Set<String> roles = new HashSet<>();         //這里用三個工具代表角色         roles.add("docker");         roles.add("maven");         roles.add("node");         return roles;     } }

User

package com.maoxs.service; import com.maoxs.pojo.User; import org.springframework.stereotype.Service; import java.util.Date; import java.util.Random; @Service public class UserService {     /**      * 模擬查詢返回用戶信息      *      * @param uname      * @return      */     public User findUserByName(String uname) {         User user = new User();         user.setUname(uname);         user.setNick(uname + "NICK");         user.setPwd("J/ms7qTJtqmysekuY8/v1TAS+VKqXdH5sB7ulXZOWho=");//密碼明文是123456         user.setSalt("wxKYXuTPST5SG0jMQzVPsg==");//加密密碼的鹽值         user.setUid(new Random().nextLong());//隨機分配一個id         user.setCreated(new Date());         return user;     } }


認證
Shiro 從從Realm獲取安全數據(如用戶、角色、權限),就是說SecurityManager要驗證用戶身份,那么它需要從Realm獲取相應的用戶進行比較以確定用戶身份是否合法;也需要從Realm得到用戶相應的角色/權限進行驗證用戶是否能進行操作;可以把Realm看成DataSource , 即安全數據源。

Realm

package com.maoxs.realm; import com.maoxs.cache.MySimpleByteSource; import com.maoxs.pojo.User; import com.maoxs.service.ResourcesService; import com.maoxs.service.RoleService; import com.maoxs.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.Sha256Hash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; /**  * 這個類是參照JDBCRealm寫的,主要是自定義了如何查詢用戶信息,如何查詢用戶的角色和權限,如何校驗密碼等邏輯  */ public class CustomRealm extends AuthorizingRealm {     @Autowired     private UserService userService;     @Autowired     private RoleService roleService;     @Autowired     private ResourcesService resourcesService;     //告訴shiro如何根據獲取到的用戶信息中的密碼和鹽值來校驗密碼     {         //設置用於匹配密碼的CredentialsMatcher         HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();         hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);         hashMatcher.setStoredCredentialsHexEncoded(false);         hashMatcher.setHashIterations(1024);         this.setCredentialsMatcher(hashMatcher);     }     //定義如何獲取用戶的角色和權限的邏輯,給shiro做權限判斷     @Override     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {         //null usernames are invalid         if (principals == null) {             throw new AuthorizationException("PrincipalCollection method argument cannot be null.");         }         User user = (User) getAvailablePrincipal(principals);         SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();         System.out.println("獲取角色信息:" + user.getRoles());         System.out.println("獲取權限信息:" + user.getPerms());         info.setRoles(user.getRoles());         info.setStringPermissions(user.getPerms());         return info;     }     //定義如何獲取用戶信息的業務邏輯,給shiro做登錄     @Override     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {         UsernamePasswordToken upToken = (UsernamePasswordToken) token;         String username = upToken.getUsername();         // Null username is invalid         if (username == null) {             throw new AccountException("請輸入用戶名");         }         User userDB = userService.findUserByName(username);         if (userDB == null) {             throw new UnknownAccountException("用戶不存在");         }         //查詢用戶的角色和權限存到SimpleAuthenticationInfo中,這樣在其它地方         //SecurityUtils.getSubject().getPrincipal()就能拿出用戶的所有信息,包括角色和權限         Set<String> roles = roleService.getRolesByUserId(userDB.getUid());         Set<String> perms = resourcesService.getResourcesByUserId(userDB.getUid());         userDB.getRoles().addAll(roles);         userDB.getPerms().addAll(perms);         SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userDB, userDB.getPwd(), getName());         if (userDB.getSalt() != null) {             info.setCredentialsSalt(ByteSource.Util.bytes(userDB.getSalt()));         }         return info;     } }



相關配置
然后呢在只需要吧這個Realm注冊到Spring容器中就可以啦

@Bean public CustomRealm customRealm() {    CustomRealm realm = new CustomRealm();    return realm;   }


為了保證實現了Shiro內部lifecycle函數的bean執行 也是shiro的生命周期,注入LifecycleBeanPostProcessor

@Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {     return new LifecycleBeanPostProcessor(); }


緊接着配置安全管理器,SecurityManager是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部組件實例,並通過它來提供安全管理的各種服務。

@Bean public DefaultWebSecurityManager securityManager() {     DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();     securityManager.setRealm(customRealm());     return securityManager; }


除此之外Shiro是一堆一堆的過濾鏈,所以要對shiro 的過濾進行設置,

@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() {     DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();     chainDefinition.addPathDefinition("favicon.ico", "anon");     chainDefinition.addPathDefinition("/login", "anon");     chainDefinition.addPathDefinition("/**", "user");     return chainDefinition; }



yml
這里要說明下由於我們引入的是shiro-spring-boot-web-starter,官方對配置進行了一系列的簡化,並加入了一些自動配置項,所以我們要在yml中加入

shiro:
  web:
    enabled: true   loginUrl: /login


除此之外呢還有這些屬性

鍵                                                   默認值     描述
shiro.enabled                                        true     啟用Shiro的Spring模塊 shiro.web.enabled       true     啟用Shiro的Spring Web模塊 shiro.annotations.enabled       true     為Shiro的注釋啟用Spring支持 shiro.sessionManager.deleteInvalidSessions      true     從會話存儲中刪除無效會話 shiro.sessionManager.sessionIdCookieEnabled     true     啟用會話ID到cookie,用於會話跟蹤 shiro.sessionManager.sessionIdUrlRewritingEnabled    true     啟用會話URL重寫支持 shiro.userNativeSessionManager     false   如果啟用,Shiro將管理HTTP會話而不是容器 shiro.sessionManager.cookie.name     JSESSIONID  會話cookie名稱 shiro.sessionManager.cookie.maxAge     -1     會話cookie最大年齡 shiro.sessionManager.cookie.domain     空值     會話cookie域 shiro.sessionManager.cookie.path     空值     會話cookie路徑 shiro.sessionManager.cookie.secure     false     會話cookie安全標志 shiro.rememberMeManager.cookie.name     rememberMe     RememberMe cookie名稱 shiro.rememberMeManager.cookie.maxAge     一年     RememberMe cookie最大年齡 shiro.rememberMeManager.cookie.domain     空值     RememberMe cookie域名 shiro.rememberMeManager.cookie.path     空值     RememberMe cookie路徑 shiro.rememberMeManager.cookie.secure     false     RememberMe cookie安全標志 shiro.loginUrl     /login.jsp     未經身份驗證的用戶重定向到登錄頁面時使用的登錄URL shiro.successUrl     /     用戶登錄后的默認登錄頁面(如果在當前會話中找不到替代) shiro.unauthorizedUrl     空值     頁面將用戶重定向到未授權的位置(403頁)

 

在Controller中添加登錄方法

@RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public Result login(@RequestParam("username") String userName, @RequestParam("password") String Password) throws Exception {     Subject currentUser = SecurityUtils.getSubject();     UsernamePasswordToken token = new UsernamePasswordToken(userName, Password);     token.setRememberMe(true);// 默認不記住密碼     try {         currentUser.login(token); //登錄         log.info("==========登錄成功=======");         return new Result(true, "登錄成功");     } catch (UnknownAccountException e) {         log.info("==========用戶名不存在=======");         return new Result(false, "用戶名不存在");     } catch (DisabledAccountException e) {         log.info("==========您的賬戶已經被凍結=======");         return new Result(false, "您的賬戶已經被凍結");     } catch (IncorrectCredentialsException e) {         log.info("==========密碼錯誤=======");         return new Result(false, "密碼錯誤");     } catch (ExcessiveAttemptsException e) {         log.info("==========您錯誤的次數太多了吧,封你半小時=======");         return new Result(false, "您錯誤的次數太多了吧,封你半小時");     } catch (RuntimeException e) {         log.info("==========運行異常=======");         return new Result(false, "運行異常");     } } @RequestMapping("/logout") public String logOut() {     Subject subject = SecurityUtils.getSubject();     subject.logout();     return "index"; }


這樣就實現了整合認證的流程,,如果token信息與數據庫表總username和password數據一致,則該用戶身份認證成功。

鑒權
只用注解控制鑒權授權
使用注解的優點是控制的粒度細,並且非常適合用來做基於資源的權限控制。

只用注解的話非常簡單。我們只需要使用url配置配置一下所以請求路徑都可以匿名訪問:

@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() {     DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();     //這里配置所有請求路徑都可以匿名訪問     chain.addPathDefinition("/**", "anon");     // 這另一種配置方式。但是還是用上面那種吧,容易理解一點。     // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");     return chain; }


然后在控制器類上使用shiro提供的種注解來做控制:

      注解                                        功能
@RequiresGuest                    只有游客可以訪問
@RequiresAuthentication     需要登錄才能訪問
@RequiresUser                      已登錄的用戶或“記住我”的用戶能訪問
@RequiresRoles                    已登錄的用戶需具有指定的角色才能訪問
@RequiresPermissions          已登錄的用戶需具有指定的權限才能訪問


示例

@RestController public class Test1Controller {     // 由於TestController類上沒有加@RequiresAuthentication注解,     // 不要求用戶登錄才能調用接口。所以hello()和a1()接口都是可以匿名訪問的     @GetMapping("/hello")     public String hello() {         return "hello spring boot";     }     // 游客可訪問,這個有點坑,游客的意思是指:subject.getPrincipal()==null     // 所以用戶在未登錄時subject.getPrincipal()==null,接口可訪問     // 而用戶登錄后subject.getPrincipal()!=null,接口不可訪問     @RequiresGuest     @GetMapping("/guest")     public String guest() {         return "@RequiresGuest";     }     // 已登錄用戶才能訪問,這個注解比@RequiresUser更嚴格     // 如果用戶未登錄調用該接口,會拋出UnauthenticatedException     @RequiresAuthentication     @GetMapping("/authn")     public String authn() {         return "@RequiresAuthentication";     }     // 已登錄用戶或“記住我”的用戶可以訪問     // 如果用戶未登錄或不是“記住我”的用戶調用該接口,UnauthenticatedException     @RequiresUser     @GetMapping("/user")     public String user() {         return "@RequiresUser";     }     // 要求登錄的用戶具有mvn:build權限才能訪問     // 由於UserService模擬返回的用戶信息中有該權限,所以這個接口可以訪問     // 如果沒有登錄,UnauthenticatedException     @RequiresPermissions("mvn:install")     @GetMapping("/mvnInstall")     public String mvnInstall() {         return "mvn:install";     }     // 要求登錄的用戶具有mvn:build權限才能訪問     // 由於UserService模擬返回的用戶信息中【沒有】該權限,所以這個接口【不可以】訪問     // 如果沒有登錄,UnauthenticatedException     // 如果登錄了,但是沒有這個權限,會報錯UnauthorizedException     @RequiresPermissions("gradleBuild")     @GetMapping("/gradleBuild")     public String gradleBuild() {         return "gradleBuild";     }     // 要求登錄的用戶具有js角色才能訪問     // 由於UserService模擬返回的用戶信息中有該角色,所以這個接口可訪問     // 如果沒有登錄,UnauthenticatedException     @RequiresRoles("docker")     @GetMapping("/docker")     public String docker() {         return "docker programmer";     }     // 要求登錄的用戶具有js角色才能訪問     // 由於UserService模擬返回的用戶信息中有該角色,所以這個接口可訪問     // 如果沒有登錄,UnauthenticatedException     // 如果登錄了,但是沒有該角色,會拋出UnauthorizedException     @RequiresRoles("python")     @GetMapping("/python")     public String python() {         return "python programmer";     } }


注意 解決spring aop和注解配置一起使用的bug。如果您在使用shiro注解配置的同時,引入了spring aop的starter,會有一個奇怪的問題,導致shiro注解的請求,不能被映射,需加入以下配置:

/** * setUsePrefix(false)用於解決一個奇怪的bug。在引入spring aop的情況下。 * 在@Controller注解的類的方法中加入@RequiresRole等shiro注解,會導致該方法無法映射請求, * 導致返回404。加入這項配置能解決這個bug */ @Bean @DependsOn("lifecycleBeanPostProcessor") public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){         DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();         defaultAdvisorAutoProxyCreator.setUsePrefix(true);         return defaultAdvisorAutoProxyCreator; }


只用url配置控制鑒權授權
shiro提供和多個默認的過濾器,我們可以用這些過濾器來配置控制指定url的權限:

配置縮寫                            對應的過濾器                                    功能
anon                          AnonymousFilter                            指定url可以匿名訪問
authc                         FormAuthenticationFilter               指定url需要form表單登錄,默認會從請求中獲取username、password,rememberMe等參數並嘗試登錄,如果登錄不了就會跳轉到loginUrl配置的路徑。我們也可以用這個過濾器做默認的登錄邏輯,但是一般都是我們自己在控制器寫登錄邏輯的,自己寫的話出錯返回的信息都可以定制嘛。
authcBasic                 BasicHttpAuthenticationFilter        指定url需要basic登錄
logout                        LogoutFilter                                   登出過濾器,配置指定url就可以實現退出功能,非常方便
noSessionCreation    NoSessionCreationFilter                 禁止創建會話
perms                        PermissionsAuthorizationFilter      需要指定權限才能訪問
port                            PortFilter                                        需要指定端口才能訪問
rest                            HttpMethodPermissionFilter          將http請求方法轉化成相應的動詞來構造一個權限字符串,這個感覺意義不大,有興趣自己看源碼的注釋
roles                            RolesAuthorizationFilter               需要指定角色才能訪問
ssl                                SslFilter                                        需要https請求才能訪問
user                            UserFilter                                       需要已登錄或“記住我”的用戶才能訪問


在spring容器中使用ShiroFilterChainDefinition來控制所有url的鑒權和授權。優點是配置粒度大,對多個Controller做鑒權授權的控制。下面是栗子

@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() {     DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();     /**     * 這里小心踩坑!我在application.yml中設置的context-path: /api/v1     * 但經過實際測試,過濾器的過濾路徑,是context-path下的路徑,無需加上"/api/v1"前綴      */     //訪問控制     chain.addPathDefinition("/user/login", "anon");//可以匿名訪問     chain.addPathDefinition("/page/401", "anon");//可以匿名訪問     chain.addPathDefinition("/page/403", "anon");//可以匿名訪問     chain.addPathDefinition("/my/hello", "anon");//可以匿名訪問     chain.addPathDefinition("/my/changePwd", "authc");//需要登錄     chain.addPathDefinition("/my/user", "user");//已登錄或“記住我”的用戶可以訪問     chain.addPathDefinition("/my/mvnBuild", "authc,perms[mvn:install]");//需要mvn:build權限     chain.addPathDefinition("/my/npmClean", "authc,perms[npm:clean]");//需要npm:clean權限     chain.addPathDefinition("/my/docker", "authc,roles[docker]");//需要js角色     chain.addPathDefinition("/my/python", "authc,roles[python]");//需要python角色     // shiro 提供的登出過濾器,訪問指定的請求,就會執行登錄,默認跳轉路徑是"/",或者是"shiro.loginUrl"配置的內容     // 由於application-shiro.yml中配置了 shiro:loginUrl: /page/401,返回會返回對應的json內容     // 可以結合/user/login和/t1/js接口來測試這個/t4/logout接口是否有效     chain.addPathDefinition("/logout", "anon,logout");     //其它路徑均需要登錄     chain.addPathDefinition("/**", "authc");     return chain; }


二者結合,url配置控制鑒權,注解控制授權
就個人而言,我是非常喜歡注解方式的。但是兩種配置方式靈活結合,才是適應不同應用場景的最佳實踐。只用注解或只用url配置,會帶來一些比較累的工作。用url配置控制鑒權,實現粗粒度控制;用注解控制授權,實現細粒度控制。下面是示例:

/**  * 這里統一做鑒權,即判斷哪些請求路徑需要用戶登錄,哪些請求路徑不需要用戶登錄。  * 這里只做鑒權,不做權限控制,因為權限用注解來做。  * @return  */ @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() {     DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();     //哪些請求可以匿名訪問     chain.addPathDefinition("/user/login", "anon");     chain.addPathDefinition("/page/401", "anon");     chain.addPathDefinition("/page/403", "anon");     chain.addPathDefinition("/hello", "anon");     chain.addPathDefinition("/guest", "anon");     //除了以上的請求外,其它請求都需要登錄     chain.addPathDefinition("/**", "authc");     return chain; } 

 

@RestController public class Test5Controller {     // 由於ShiroConfig中配置了該路徑可以匿名訪問,所以這接口不需要登錄就能訪問     @GetMapping("/hello")     public String hello() {         return "hello spring boot";     }     // 如果ShiroConfig中沒有配置該路徑可以匿名訪問,所以直接被登錄過濾了。     // 如果配置了可以匿名訪問,那這里在沒有登錄的時候可以訪問,但是用戶登錄后就不能訪問     @RequiresGuest     @GetMapping("/guest")     public String guest() {         return "@RequiresGuest";     }     @RequiresAuthentication     @GetMapping("/authn")     public String authn() {         return "@RequiresAuthentication";     }     @RequiresUser     @GetMapping("/user")     public String user() {         return "@RequiresUser";     }     @RequiresPermissions("mvn:install")     @GetMapping("/mvnInstall")     public String mvnInstall() {         return "mvn:install";     }     @RequiresPermissions("gradleBuild")     @GetMapping("/gradleBuild")     public String gradleBuild() {         return "gradleBuild";     }     @RequiresRoles("python")     @GetMapping("/python")     public String python() {         return "python programmer";     } } 


記住我
記住我功能在各大網站是比較常見的,實現起來也是大同小異,主要就是利用cookie來實現,而shiro對記住我功能的實現也是比較簡單的,只需要幾步即可。

首先呢配置下Cookie的生成模版,配置下cookie的name,cookie的有效時間等等。

@Bean public SimpleCookie rememberMeCookie() {     //System.out.println("ShiroConfiguration.rememberMeCookie()");     //這個參數是cookie的名稱,對應前端的checkbox的name = rememberMe     SimpleCookie simpleCookie = new SimpleCookie("rememberMe");     //<!-- 記住我cookie生效時間30天 ,單位秒;-->     simpleCookie.setMaxAge(259200);     return simpleCookie; }


然后呢配置rememberMeManager

@Bean public CookieRememberMeManager rememberMeManager() {     //System.out.println("ShiroConfiguration.rememberMeManager()");     CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();     cookieRememberMeManager.setCookie(rememberMeCookie());     //rememberMe cookie加密的密鑰 建議每個項目都不一樣 默認AES算法 密鑰長度(128 256 512 位)     cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));     return cookieRememberMeManager; } 


rememberMeManager()方法是生成rememberMe管理器,而且要將這個rememberMe管理器設置到securityManager中。

@Bean public DefaultWebSecurityManager securityManager() {     DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();     securityManager.setRealm(customRealm(redisCacheManager));     securityManager.setRememberMeManager(rememberMeManager());     return securityManager; }


好了記住我功能就到這里了,不過要記住一點,如果使用了authc的過濾的url的是不能使用記住我功能的,切記,至於什么原因,很好理解。有一些操作你是不需要別人在記住我功能下完成的,這樣很不安全,所以shiro規定記住我功能最多得user級別的,不能到authc級別。

啟用緩存
Shiro提供了類似Spring的Cache抽象,即Shiro本身不實現Cache,但是對Cache進行了又抽象,方便更換不同的底層Cache實現。對應前端的一個頁面訪問請求會同時出現很多的權限查詢操作,這對於權限信息變化不是很頻繁的場景,每次前端頁面訪問都進行大量的權限數據庫查詢是非常不經濟的。因此,非常有必要對權限數據使用緩存方案。

由於Spring和Shiro都各自維護了自己的Cache抽象,為防止Realm注入的service里緩存注解和事務注解失效,所以定義自己的CacheManager處理緩存。

整合Redis
CacheManager代碼如下。

package com.maoxs.cache; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.util.Destroyable; import org.springframework.data.redis.cache.RedisCacheManager; import java.util.Collection; import java.util.Set; public class ShiroRedisCacheManager implements CacheManager, Destroyable {     private RedisCacheManager cacheManager;     public RedisCacheManager getCacheManager() {         return cacheManager;     }     public void setCacheManager(RedisCacheManager cacheManager) {         this.cacheManager = cacheManager;     }     //為了個性化配置redis存儲時的key,我們選擇了加前綴的方式,所以寫了一個帶名字及redis操作的構造函數的Cache類     public <K, V> Cache<K, V> getCache(String name) throws CacheException {         if (name == null) {             return null;         }         return new ShiroRedisCache<K, V>(name, getCacheManager());     }     @Override     public void destroy() throws Exception {         cacheManager = null;     }     /**     * <p> 自定義緩存 將數據存入到redis中 </p>     *     * @param <K>     * @param <V>     * @author xxx     * @date 2018年2月1日     * @time 22:32:11     */     @Slf4j     class ShiroRedisCache<K, V> implements org.apache.shiro.cache.Cache<K, V> {         private RedisCacheManager cacheManager;         private org.springframework.cache.Cache cache;         //    private RedisCache cache2;         public ShiroRedisCache(String name, RedisCacheManager cacheManager) {             if (name == null || cacheManager == null) {                 throw new IllegalArgumentException("cacheManager or CacheName cannot be null.");             }             this.cacheManager = cacheManager;             //這里首先是從父類中獲取這個cache,如果沒有會創建一個redisCache,初始化這個redisCache的時候             //會設置它的過期時間如果沒有配置過這個緩存的,那么默認的緩存時間是為0的,如果配置了,就會把配置的時間賦予給這個RedisCache             //如果從緩存的過期時間為0,就表示這個RedisCache不存在了,這個redisCache實現了spring中的cache             this.cache = cacheManager.getCache(name);         }         @Override         public V get(K key) throws CacheException {             log.info("從緩存中獲取key為{}的緩存信息", key);             if (key == null) {                 return null;             }             org.springframework.cache.Cache.ValueWrapper valueWrapper = cache.get(key);             if (valueWrapper == null) {                 return null;             }             return (V) valueWrapper.get();         }         @Override         public V put(K key, V value) throws CacheException {             log.info("創建新的緩存,信息為:{}={}", key, value);             cache.put(key, value);             return get(key);         }         @Override         public V remove(K key) throws CacheException {             log.info("干掉key為{}的緩存", key);             V v = get(key);             cache.evict(key);//干掉這個名字為key的緩存             return v;         }         @Override         public void clear() throws CacheException {             log.info("清空所有的緩存");             cache.clear();         }         @Override         public int size() {             return cacheManager.getCacheNames().size();         }         /**          * 獲取緩存中所的key值          */         @Override         public Set<K> keys() {             return (Set<K>) cacheManager.getCacheNames();         }         /**          * 獲取緩存中所有的values值          */         @Override         public Collection<V> values() {             return (Collection<V>) cache.get(cacheManager.getCacheNames()).get();         }         @Override         public String toString() {             return "ShiroSpringCache [cache=" + cache + "]";         }     } }


然后呢就是把這個CacheManager注入到securityManager中

@Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {     RedisTemplate<Object, Object> template = new RedisTemplate<>();     template.setConnectionFactory(connectionFactory);     //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用JDK的序列化方式)     Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);     ObjectMapper mapper = new ObjectMapper();     mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);     mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);     serializer.setObjectMapper(mapper);     template.setValueSerializer(serializer);     //使用StringRedisSerializer來序列化和反序列化redis的key值     template.setKeySerializer(new StringRedisSerializer());     template.afterPropertiesSet();     return template; } /** * Spring緩存管理器配置 * * @param redisTemplate * @return */ @Bean public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {     CollectionSerializer<Serializable> collectionSerializer = CollectionSerializer.getInstance();     RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());     RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()         .entryTtl(Duration.ofHours(1))         .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(collectionSerializer));     return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } /** * shiro緩存管理器的配置 * * @param redisCacheManager * @return */ @Bean public ShiroRedisCacheManager shiroRedisCacheManager(RedisCacheManager redisCacheManager) {     ShiroRedisCacheManager cacheManager = new ShiroRedisCacheManager();     cacheManager.setCacheManager(redisCacheManager);     //name是key的前綴,可以設置任何值,無影響,可以設置帶項目特色的值     return cacheManager; }


相對應的Realm和securityManager也要稍做更改

@Bean public CustomRealm customRealm(RedisCacheManager redisCacheManager) {     CustomRealm realm = new CustomRealm();     realm.setCachingEnabled(true);     //設置認證密碼算法及迭代復雜度     //realm.setCredentialsMatcher(credentialsMatcher());     //認證     realm.setCacheManager(shiroRedisCacheManager(redisCacheManager));     realm.setAuthenticationCachingEnabled(true);     //授權     realm.setAuthorizationCachingEnabled(true);     //這里主要是緩存key的名字     realm.setAuthenticationCacheName("fulinauthen");     realm.setAuthenticationCacheName("fulinauthor");     return realm; } @Bean public DefaultWebSecurityManager securityManager(RedisCacheManager redisCacheManager) {     DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();     securityManager.setRealm(customRealm(redisCacheManager));     securityManager.setCacheManager(shiroRedisCacheManager(redisCacheManager));     securityManager.setRememberMeManager(rememberMeManager());     return securityManager; } 


這樣的話每次認證的時候就會把權限信息放入redis中,就不用反復的去查詢數據庫了。

注意
Realm里注入的UserService等service,需要延遲注入,所以都要添加@Lazy注解(如果不加需要自己延遲注入),否則會導致該service里的@Cacheable緩存注解、@Transactional事務注解等失效。

整合的時候應該會有人遇到不能序列化的問題吧,原因是因為用了Shiro的SimpleAuthenticationInfo中的setCredentialsSalt注入的屬性ByteSource沒有實現序列化接口,此時呢只用把源碼一貼,實現下序列化接口即可

package com.maoxs.cache; import org.apache.shiro.codec.Base64; import org.apache.shiro.codec.CodecSupport; import org.apache.shiro.codec.Hex; import org.apache.shiro.util.ByteSource; import java.io.File; import java.io.InputStream; import java.io.Serializable; import java.util.Arrays; /**  * 解決ByteSource 序列化問題  */ public class MySimpleByteSource implements ByteSource, Serializable {     private byte[] bytes;     private String cachedHex;     private String cachedBase64;     public MySimpleByteSource() {     }     public MySimpleByteSource(byte[] bytes) {         this.bytes = bytes;     }     public MySimpleByteSource(char[] chars) {         this.bytes = CodecSupport.toBytes(chars);     }     public MySimpleByteSource(String string) {         this.bytes = CodecSupport.toBytes(string);     }     public MySimpleByteSource(ByteSource source) {         this.bytes = source.getBytes();     }     public MySimpleByteSource(File file) {         this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);     }     public MySimpleByteSource(InputStream stream) {         this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);     }     public static boolean isCompatible(Object o) {         return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;     }     public byte[] getBytes() {         return this.bytes;     }     public boolean isEmpty() {         return this.bytes == null || this.bytes.length == 0;     }     public String toHex() {         if (this.cachedHex == null) {             this.cachedHex = Hex.encodeToString(this.getBytes());         }         return this.cachedHex;     }     public String toBase64() {         if (this.cachedBase64 == null) {             this.cachedBase64 = Base64.encodeToString(this.getBytes());         }         return this.cachedBase64;     }     public String toString() {         return this.toBase64();     }     public int hashCode() {         return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;     }     public boolean equals(Object o) {         if (o == this) {             return true;         } else if (o instanceof ByteSource) {             ByteSource bs = (ByteSource) o;             return Arrays.equals(this.getBytes(), bs.getBytes());         } else {             return false;         }     }     private static final class BytesHelper extends CodecSupport {         private BytesHelper() {         }         public byte[] getBytes(File file) {             return this.toBytes(file);         }         public byte[] getBytes(InputStream stream) {             return this.toBytes(stream);         }     } }



然后在realm中改變使用

if (userDB.getSalt() != null) {
    info.setCredentialsSalt(new MySimpleByteSource(userDB.getSalt()));
}

整合Ehcache
整合ehcache就更簡單,套路都是一樣的只不過2.x和3.x 需要注入不同的CacheManager即可。這里需要注入下3.x的Ehcache是實現了Jcache,不過整合起來都是一樣的,詳情可以去看我之前的整合Spring抽象緩存的帖子。

官方提供了shiro-ehcache的整合包,不過這個整合包是針對Ehcache2.x的。

Redis存儲Session
關於共享session的問題大家都應該知道了,傳統的部署項目,兩個相同的項目部署到不同的服務器上,Nginx負載均衡后會導致用戶在A上登陸了,經過負載均衡后,在B上要重新登錄,因為A上有相關session信息,而B沒有。這種情況也稱為“有狀態”服務。而“無狀態”服務則是:在一個公共的地方存儲session,每次訪問都會統一到這個地方來拿。思路呢就是實現Shiro的Session接口,然后呢自己控制,這里我們實現AbstractSessionDAO。

package com.maoxs.cache; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; import org.springframework.data.redis.core.RedisTemplate; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.concurrent.TimeUnit; @Slf4j public class ShiroRedisSessionDao extends AbstractSessionDAO {     private RedisTemplate redisTemplate;     public ShiroRedisSessionDao(RedisTemplate redisTemplate) {         this.redisTemplate = redisTemplate;     }     @Override     public void update(Session session) throws UnknownSessionException {         log.info("更新seesion,id=[{}]", session.getId().toString());         try {             redisTemplate.opsForValue().set(session.getId().toString(), session, 30, TimeUnit.MINUTES);         } catch (Exception e) {             log.error(e.getMessage(), e);         }     }     @Override     public void delete(Session session) {         log.info("刪除seesion,id=[{}]", session.getId().toString());         try {             String key = session.getId().toString();             redisTemplate.delete(key);         } catch (Exception e) {             log.info(e.getMessage(), e);         }     }     @Override     public Collection<Session> getActiveSessions() {         log.info("獲取存活的session");         return Collections.emptySet();     }     @Override     protected Serializable doCreate(Session session) {         Serializable sessionId = generateSessionId(session);         assignSessionId(session, sessionId);         log.info("創建seesion,id=[{}]", session.getId().toString());         try {             redisTemplate.opsForValue().set(session.getId().toString(), session, 30, TimeUnit.MINUTES);         } catch (Exception e) {             log.error(e.getMessage(), e);         }         return sessionId;     }     @Override     protected Session doReadSession(Serializable sessionId) {         log.info("獲取seesion,id=[{}]", sessionId.toString());         Session readSession = null;         try {             readSession = (Session) redisTemplate.opsForValue().get(sessionId.toString());         } catch (Exception e) {             log.error(e.getMessage());         }         return readSession;     } } 最后吧你寫好的SessionDao注入到shiro的securityManager中即可 /** * 配置sessionmanager,由redis存儲數據 */ @Bean(name = "sessionManager") public DefaultWebSessionManager sessionManager(RedisTemplate redisTemplate) {     DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();     CollectionSerializer<Serializable> collectionSerializer = CollectionSerializer.getInstance();     redisTemplate.setDefaultSerializer(collectionSerializer);     //redisTemplate默認采用的其實是valueSerializer,就算是采用其他ops也一樣,這是一個坑。     redisTemplate.setValueSerializer(collectionSerializer);     ShiroRedisSessionDao redisSessionDao = new ShiroRedisSessionDao(redisTemplate);     //這個name的作用也不大,只是有特色的cookie的名稱。     sessionManager.setSessionDAO(redisSessionDao);     sessionManager.setDeleteInvalidSessions(true);     SimpleCookie cookie = new SimpleCookie();     cookie.setName("starrkCookie");     sessionManager.setSessionIdCookie(cookie);     sessionManager.setSessionIdCookieEnabled(true);     return sessionManager; } @Bean public DefaultWebSecurityManager securityManager(RedisTemplate redisTemplate, RedisCacheManager redisCacheManager) {     DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();     securityManager.setRealm(customRealm(redisCacheManager));     securityManager.setCacheManager(shiroRedisCacheManager(redisCacheManager));     securityManager.setRememberMeManager(rememberMeManager());     securityManager.setSessionManager(sessionManager(redisTemplate));     return securityManager; }



這樣每次讀取Session就會從Redis中取讀取了,當然還有謝謝開源的插件解決方案,比如crazycake ,有機會在補充這個。


免責聲明!

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



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