Shiro是Apache的強大靈活的開源安全框架
能提供認證、授權、企業會話管理、安全加密、緩存等功能。
與Spring Security的比較
Apache Shiro | Spring Security |
---|---|
簡單靈活 | 復雜、笨重 |
可脫離Spring | 必須依賴Spring |
粒度較粗 | 粒度更細 |
Shiro的幾個關鍵要素
-
Subject
主體(官方解釋,不明白為毛要命名為主體,一眼看到這么個東西讓人很難理解),其實很簡單,Subject就是應用和Shiro管理器交流的橋梁,基本上所有對權限的操作都是通過Subject進行的,比如登錄,比如注銷,Subject就可以看成是Shiro里的用戶。
-
SecurityManager
安全管理器,所有與安全相關的操作都會由SecurityManager來處理,而且,通過查看源碼可以看到,Subject的所有操作都是借助于SecurityManager來完成的,它是Shiro的核心。
-
Realm
域(這個概念也是比較抽象的),可以有一個或多個,Shiro中所有的安全驗證數據都是由Realm提供的,而且Shiro不知道應用的權限存儲以何種方式存儲,所以我們一般都需要實現自己的Realm;可以這樣看,Subject提供驗證數據入口,Realm提供驗證的數據源,而真正的驗證功能由Shiro的認證器來完成。
-
Authenticator
認證器,負責主體認證的,即認證器都用來實現用戶在什么情況下算是認證通過了。
-
Authrizer
授權器,或者訪問控制器,用來對主體(Subject)進行授權,覺得主體有哪些操作的權限,能訪問應用中的那些功能。
-
SessionManager
Session管理器,但是這個地方的Session與當初學習Servlet時接觸到的Session基本類似,但是這個Session是由Shiro自己去維護的,與Web環境無關,可以應用到Web環境中,也可以應用到普通的JavaSE環境。
-
SessionDAO
數據訪問對象,用于會話的CRUD,比如將Session存儲到Redis,或者數據庫,或者內存,都可以通過SessionDAO來實現,可以使用默認的SessionDAO,也可以自定義實現。
-
CacheManager
緩存控制器,用來管理用戶、角色、權限等的緩存。
-
Cryptography
密碼模塊,Shiro提供了一些常見的加密組件用于密碼加密/解密。
Shiro內置的過濾器
- anon,authBasic,authc,user,logout
- perms,roles,ssl,port
過濾器簡稱過濾器簡稱 | 對應的java類 |
---|---|
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 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter |
Shiro在前后臺分離架構的項目中的應用
Shiro在傳統web項目中的應用與前后臺分離項目中的區別
傳統項目中,前后臺在一個工程里,頁面的跳轉,請求的訪問,一般都是由后臺來控制,中間不需要做太多的轉換。
而在前后臺分離項目中,前后臺在不同的工程里,也在不同的服務器上,頁面的跳轉由前端路由來控制(其實也沒啥頁面的跳轉,隨著前端框架如雨后竹筍一般的冒出來,前端應用都往單頁面應用的方向發展),后臺只負責提供數據以及安全驗證,對于頁面的東西后臺已經不做關注。在這種情況下,在使用Shiro時就需要有一些自定義的東西了。
需要關注的幾個點
- 通過Redis存儲Session
- 由Shiro來跳轉的請求地址
- 配置不需要驗證的請求接口
具體實現
作為一個SpringBoot洗腦流,不管是什么新東西,最先想到的就是通過SpringBoot來集成。這里通過SpringBoot,集成Shiro、Swagger(模擬前臺通過JSON請求后臺)、Redis(暫時只存儲Session),使用Swagger來模擬請求,測試Shiro的權限控制。
以下的集成相關東西,都是建立于一個完整的SpringBoot Demo。
-
集成Redis
引入Redis依賴
<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
引入第三方Redis序列化工具
<!-- 高效的序列化庫kyro --> <dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo-shaded</artifactId> <version>4.0.0</version> </dependency>
注: Kryo是一個快速高效的Java序列化框架,旨在提供快速、高效和易用的API。無論文件、數據庫或網絡數據Kryo都可以隨時完成序列化。Kryo還可以執行自動深拷貝(克隆)、淺拷貝(克隆)。這是對象到對象的直接拷貝,非對象->字節->對象的拷貝。在后面的文章會分析一下Redis各種序列化方式的效率。
配置Redis連接(為了方便測試,使用Redis單機版即可)
spring: redis: database: 0 host: localhost password: # Redis服務器若設置密碼,此處必須配置 port: 6379 timeout: 10000 # 連接超時時間(毫秒) pool: max-active: 8 # 連接池最大連接數(使用負數表示沒有限制) max-idle: 8 # 連接池中的最大空閑連接 min-idle: 0 # 連接池中的最小空閑連接 max-wait: -1 # 連接池最大阻塞等待時間(使用負數表示沒有限制)
-
Swagger的集成
為了不重復造輪子,使用swagger-spring-boot-starter(一個大牛自己針對Swagger封裝的一個SpringBoot的Starter自動配置模塊)即可。
<!-- swagger API集成 --> <dependency> <groupId>com.spring4all</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>1.7.1.RELEASE</version> </dependency>
在使用Shiro之后,由于默認情況下,資源都會被Shiro攔截,所以需要對Swagger的資源手動做加載,并使用
@EnableSwagger2Doc
打開Swagger自動配置,并且在下面shiro攔截器配置時,將swagger相關資源配置為anno。@Configuration @EnableSwagger2Doc public class SwaggerConfiguration extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/js/**").addResourceLocations("classpath:/js/"); registry.addResourceHandler("swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); } }
配置Swagger
swagger: title: 測試Demo description: 測試Demo version: 1.0.RELEASE license: Apache License, Version 2.0 license-url: https://www.apache.org/licenses/LICENSE-2.0.html terms-of-service-url: https://github.com/dyc87112/spring-boot-starter-swagger base-package: com.example base-path: /** exclude-path: /error, /ops/**
-
Shiro集成
引入Shiro官方提供的與Spring類項目集成的依賴包
<!-- shiro begin --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <!-- shiro ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency>
除了上面這兩個依賴包之外,以便于以后項目做集群,使用Redis存儲Shiro的安全驗證信息,所以在Github上翻了翻,找到了下面shiro-redis包,它很好的完成了Redis與Shiro的集成,不需要開發人員自己去編碼,實現Shiro的SessionDAO接口。
<!-- shiro與Redis整合的開源插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.0.0</version> </dependency>
還沒完,Shiro的常規配置還需要通過JavaConfig的方式去配置(以SpringBoot自動配置的方式實現),廢話少說,下面代碼見真章。
shiro的相關攔截規則配置
security: shiro: filter: anon: # 不需要Shiro攔截的請求URL - /api/v1/** # swagger接口文檔 - /swagger-ui.html - /webjars/** - /swagger-resources/** - /user/login # 登錄接口 - /user/noLogin # 未登錄提示信息接口 authc: # 需要Shiro攔截的請求URL - /** loginUrl: /user/login # 登錄接口 noAccessUrl: /user/noLogin # 未登錄時跳轉URL globalSessionTimeout: 30 # 登錄過期時長
自定義的Shiro屬性配置類
ShiroProperties.java
@Data @ConfigurationProperties(prefix = "security.shiro") public class ShiroProperties { /** * 登錄Url */ private String loginUrl; /** * 沒權限訪問時的轉發Url(做未登錄提示信息用) */ private String noAccessUrl; /** * Shiro請求攔截規則配置(Shiro的攔截器規則,常用的anon和authc) */ private Map<String, List<String>> filter; /** * Shiro Session 過期時間(分鐘) */ private Long globalSessionTimeout = 30L; }
為解決前后臺分離架構的項目下,未登錄時訪問系統的跳轉及對應的提示信息Shiro原有邏輯為未登錄則跳轉到登錄Url,在前后臺分離架構下,此種方式顯然不能滿足要求,只能修改authc默認過濾器處理流程,通過將請求轉發到一個新的Url,給出未登錄提示信息,由前臺去控制路由跳轉到登錄頁面
@Slf4j public class SelfDefinedFormAuthenticationFilter extends FormAuthenticationFilter { // 沒有權限訪問的提示信息跳轉URL private String noAccessUrl; public String getNoAccessUrl() { return noAccessUrl; } public SelfDefinedFormAuthenticationFilter setNoAccessUrl(String noAccessUrl) { this.noAccessUrl = noAccessUrl; return this; } // 重寫跳轉到登錄URL的邏輯,改為轉發到未登錄URL @Override protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException { String noAccessUrl = getNoAccessUrl(); try { request.getRequestDispatcher(noAccessUrl).forward(request, response); } catch (ServletException e) { e.getMessage(); } } }
自定義Realm,提供登錄驗證數據及授權邏輯
@Slf4j @Component public class SelfDefinedShiroRealm extends AuthorizingRealm { /** * 授權 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); return authorizationInfo; } /** * 認證 * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); log.info(username); SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo( new User(username, "123"), username, getName() ); return authorizationInfo; } }
新建配置類,配置Shiro相關配置。
@Configuration @EnableConfigurationProperties(ShiroProperties.class) public class ShiroConfiguration { @Autowired private RedisProperties redisProperties; @Autowired private ShiroProperties shiroProperties; @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //獲取filters Map<String, Filter> filters = shiroFilterFactoryBean.getFilters(); //將自定義 的FormAuthenticationFilter注入shiroFilter中 filters.put("authc", new SelfDefinedFormAuthenticationFilter(). setNoAccessUrl(shiroProperties.getNoAccessUrl())); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); //注意過濾器配置順序 不能顛倒 Map<String, List<String>> filterMap = shiroProperties.getFilter(); filterMap.forEach((filter, urls) -> { urls.forEach(url -> { filterChainDefinitionMap.put(url, filter); }); }); // 配置shiro默認登錄界面地址,前后端分離中登錄界面跳轉應由前端路由控制,后臺僅返回json數據 shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl()); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 憑證匹配器(密碼需要加密時,可使用) * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 設置加密算法 Md5Hash hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 設置散列加密次數 如:2=md5(md5(aaa)) hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } @Bean public SecurityManager securityManager( AuthorizingRealm authorizingRealm, SessionManager sessionManager, RedisCacheManager redisCacheManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(authorizingRealm); // 自定義的Session管理 securityManager.setSessionManager(sessionManager); // 自定義的緩存實現 securityManager.setCacheManager(redisCacheManager); return securityManager; } /** * 自定義的SessionManager * @param redisSessionDAO * @return */ @Bean public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) { SelfDefinedSessionManager sessionManager = new SelfDefinedSessionManager(); sessionManager.setSessionDAO(redisSessionDAO); sessionManager.setGlobalSessionTimeout(shiroProperties.getGlobalSessionTimeout() * 60 * 1000); return sessionManager; } /** * 配置shiro redisManager * 使用的是shiro-redis開源插件 * @return */ @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(redisProperties.getHost()); redisManager.setPort(redisProperties.getPort()); redisManager.setTimeout(redisProperties.getTimeout()); if (!ObjectUtils.isEmpty(redisProperties.getPassword())) { redisManager.setPassword(redisProperties.getPassword()); } return redisManager; } /** * cacheManager 緩存 redis實現 * 使用的是shiro-redis開源插件 * @param redisManager * @return */ @Bean public RedisCacheManager redisCacheManager(RedisManager redisManager) { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager); redisCacheManager.setValueSerializer(new StringSerializer()); return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao層的實現 redis實現 * 使用的是shiro-redis開源插件 * @param redisManager * @return */ @Bean public RedisSessionDAO redisSessionDAO(RedisManager redisManager) { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager); return redisSessionDAO; } /** * 開啟shiro aop注解支持 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
-
編寫簡單的Controller,測試一下
UserController.java
@Autowired private RedisSessionDAO redisSessionDAO; @ApiOperation("登錄") @PostMapping("/login") public Object login(@RequestBody User user) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword()); try { // 登錄 subject.login(token); // 登錄成功后,獲取菜單權限信息 if (subject.isAuthenticated()) { return "登錄成功"; } } catch (IncorrectCredentialsException e) { return "密碼錯誤"; } catch (LockedAccountException e) { return "登錄失敗,該用戶已被凍結"; } catch (AuthenticationException e) { return "該用戶不存在"; } catch (Exception e) { return e.getMessage(); } return "登錄失敗"; } @ApiOperation("注銷") @PostMapping("/logout") public Object logout() { Subject subject = SecurityUtils.getSubject(); redisSessionDAO.delete(subject.getSession()); return "注銷成功"; } @ApiOperation("未登錄提示信息接口") @RequestMapping("/noLogin") public Object noLogin() { return "未登錄,請先登錄再訪問"; } @ApiOperation("需登錄才能訪問") @PostMapping("/home") public Object home() { return "這是主頁"; }
訪問http://localhost:8080/shiro/swagger-ui.html頁面,通過Swagger測試請求的攔截。
-
未登錄訪問/user/home
返回信息
“未登錄,請先登錄再訪問”
,代表請求成功攔截到了,未登錄不能正常訪問系統 -
訪問/user/login進行登錄,然后訪問/user/home
入參:
{ "userName":"admin", "password":"123" }
出參:
"登錄成功"
然后訪問/user/home,成功返回
"這是主頁"
-
注銷后在訪問/user/home
直接請求/user/logout,訪問/user/home,提示
“未登錄,請先登錄再訪問”
,表示成功注銷。
注: /user/noLogin使用的是
@RequestMapping("/noLogin")
,是為了保證所有請求方式(GET/POST/PUT/DELETE等)的未登錄請求都能轉發到此接口,從而正確返回未登錄提示信息。 -
以上相關源碼,請訪問https://github.com/ArtIsLong/shiro-spring-boot-starter.git
關注我的微信公眾號:FramePower
我會不定期發布相關技術積累,歡迎對技術有追求、志同道合的朋友加入,一起學習成長!