Apache Shiro是一個功能強大且易於使用的Java安全框架,可執行身份驗證,受權,加密和會話管理。藉助Shiro易於理解的API,您能夠快速輕鬆地保護任何應用程序 - 從最小的移動應用程序到最大的Web和企業應用程序。網上找到大部分文章都是之前SpringMVC下的整合方式,不少人都不知道shiro提供了官方的starter能夠方便地跟SpringBoot整合。前端
請看shiro官網關於springboot整合shiro的連接:Integrating Apache Shiro into Spring-Boot Applicationsjava
整合準備
這篇文檔的介紹也至關簡單。咱們只須要按照文檔說明,而後在spring容器中注入一個咱們自定義的Realm,shiro經過這個realm就能夠知道如何獲取用戶信息來處理鑑權(Authentication),如何獲取用戶角色、權限信息來處理受權(Authorization)。若是是web應用程序的話須要引入shiro-spring-boot-web-starter,單獨的應用程序的話則引入shiro-spring-boot-starter。node
依賴python
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.4.0-RC2</version> </dependency>
用戶實體
首先建立一個用戶的實體,用來作認證c++
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作資源權限的判斷 }
這裏了爲了方便,就不去數據庫讀取了,方便測試咱們把,權限信息,角色信息,認證信息都靜態模擬下。程序員
Resourcesweb
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; } }
Roleredis
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 , 即安全數據源。spring
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 ,有機會在補充這個。