Shiro是Apache的強大靈活的開源安全框架
能提供認證侨糟、授權(quán)碍扔、企業(yè)會話管理、安全加密秕重、緩存等功能不同。
與Spring Security的比較
Apache Shiro | Spring Security |
---|---|
簡單靈活 | 復(fù)雜、笨重 |
可脫離Spring | 必須依賴Spring |
粒度較粗 | 粒度更細 |
Shiro的幾個關(guān)鍵要素
-
Subject
主體(官方解釋,不明白為毛要命名為主體二拐,一眼看到這么個東西讓人很難理解)服鹅,其實很簡單,Subject就是應(yīng)用和Shiro管理器交流的橋梁百新,基本上所有對權(quán)限的操作都是通過Subject進行的企软,比如登錄,比如注銷饭望,Subject就可以看成是Shiro里的用戶仗哨。
-
SecurityManager
安全管理器,所有與安全相關(guān)的操作都會由SecurityManager來處理铅辞,而且厌漂,通過查看源碼可以看到,Subject的所有操作都是借助于SecurityManager來完成的斟珊,它是Shiro的核心桩卵。
-
Realm
域(這個概念也是比較抽象的),可以有一個或多個倍宾,Shiro中所有的安全驗證數(shù)據(jù)都是由Realm提供的雏节,而且Shiro不知道應(yīng)用的權(quán)限存儲以何種方式存儲,所以我們一般都需要實現(xiàn)自己的Realm高职;可以這樣看钩乍,Subject提供驗證數(shù)據(jù)入口,Realm提供驗證的數(shù)據(jù)源怔锌,而真正的驗證功能由Shiro的認證器來完成寥粹。
-
Authenticator
認證器,負責主體認證的埃元,即認證器都用來實現(xiàn)用戶在什么情況下算是認證通過了涝涤。
-
Authrizer
授權(quán)器,或者訪問控制器岛杀,用來對主體(Subject)進行授權(quán)阔拳,覺得主體有哪些操作的權(quán)限,能訪問應(yīng)用中的那些功能类嗤。
-
SessionManager
Session管理器糊肠,但是這個地方的Session與當初學習Servlet時接觸到的Session基本類似,但是這個Session是由Shiro自己去維護的遗锣,與Web環(huán)境無關(guān)货裹,可以應(yīng)用到Web環(huán)境中,也可以應(yīng)用到普通的JavaSE環(huán)境精偿。
-
SessionDAO
數(shù)據(jù)訪問對象弧圆,用于會話的CRUD赋兵,比如將Session存儲到Redis,或者數(shù)據(jù)庫搔预,或者內(nèi)存毡惜,都可以通過SessionDAO來實現(xiàn),可以使用默認的SessionDAO斯撮,也可以自定義實現(xiàn)经伙。
-
CacheManager
緩存控制器,用來管理用戶勿锅、角色帕膜、權(quán)限等的緩存。
-
Cryptography
密碼模塊溢十,Shiro提供了一些常見的加密組件用于密碼加密/解密垮刹。
Shiro內(nèi)置的過濾器
- anon,authBasic,authc,user,logout
- perms,roles,ssl,port
過濾器簡稱過濾器簡稱 | 對應(yīng)的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在前后臺分離架構(gòu)的項目中的應(yīng)用
Shiro在傳統(tǒng)web項目中的應(yīng)用與前后臺分離項目中的區(qū)別
傳統(tǒng)項目中,前后臺在一個工程里张弛,頁面的跳轉(zhuǎn)荒典,請求的訪問,一般都是由后臺來控制吞鸭,中間不需要做太多的轉(zhuǎn)換寺董。
而在前后臺分離項目中,前后臺在不同的工程里刻剥,也在不同的服務(wù)器上遮咖,頁面的跳轉(zhuǎn)由前端路由來控制(其實也沒啥頁面的跳轉(zhuǎn),隨著前端框架如雨后竹筍一般的冒出來造虏,前端應(yīng)用都往單頁面應(yīng)用的方向發(fā)展)御吞,后臺只負責提供數(shù)據(jù)以及安全驗證,對于頁面的東西后臺已經(jīng)不做關(guān)注漓藕。在這種情況下陶珠,在使用Shiro時就需要有一些自定義的東西了。
需要關(guān)注的幾個點
- 通過Redis存儲Session
- 由Shiro來跳轉(zhuǎn)的請求地址
- 配置不需要驗證的請求接口
具體實現(xiàn)
作為一個SpringBoot洗腦流享钞,不管是什么新東西揍诽,最先想到的就是通過SpringBoot來集成。這里通過SpringBoot嫩与,集成Shiro寝姿、Swagger(模擬前臺通過JSON請求后臺)、Redis(暫時只存儲Session)划滋,使用Swagger來模擬請求,測試Shiro的權(quán)限控制埃篓。
以下的集成相關(guān)東西处坪,都是建立于一個完整的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玄帕。無論文件、數(shù)據(jù)庫或網(wǎng)絡(luò)數(shù)據(jù)Kryo都可以隨時完成序列化裤纹。Kryo還可以執(zhí)行自動深拷貝(克隆)、淺拷貝(克录榛恪)擂找。這是對象到對象的直接拷貝浩销,非對象->字節(jié)->對象的拷貝撼嗓。在后面的文章會分析一下Redis各種序列化方式的效率粉捻。
配置Redis連接(為了方便測試杏头,使用Redis單機版即可)
spring: redis: database: 0 host: localhost password: # Redis服務(wù)器若設(shè)置密碼,此處必須配置 port: 6379 timeout: 10000 # 連接超時時間(毫秒) pool: max-active: 8 # 連接池最大連接數(shù)(使用負數(shù)表示沒有限制) max-idle: 8 # 連接池中的最大空閑連接 min-idle: 0 # 連接池中的最小空閑連接 max-wait: -1 # 連接池最大阻塞等待時間(使用負數(shù)表示沒有限制)
-
Swagger的集成
為了不重復(fù)造輪子醇王,使用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相關(guān)資源配置為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的集成解恰,不需要開發(fā)人員自己去編碼锋八,實現(xiàn)Shiro的SessionDAO接口浙于。
<!-- shiro與Redis整合的開源插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.0.0</version> </dependency>
還沒完护盈,Shiro的常規(guī)配置還需要通過JavaConfig的方式去配置(以SpringBoot自動配置的方式實現(xiàn)),廢話少說羞酗,下面代碼見真章腐宋。
shiro的相關(guān)攔截規(guī)則配置
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 # 未登錄時跳轉(zhuǎn)URL globalSessionTimeout: 30 # 登錄過期時長
自定義的Shiro屬性配置類
ShiroProperties.java
@Data @ConfigurationProperties(prefix = "security.shiro") public class ShiroProperties { /** * 登錄Url */ private String loginUrl; /** * 沒權(quán)限訪問時的轉(zhuǎn)發(fā)Url(做未登錄提示信息用) */ private String noAccessUrl; /** * Shiro請求攔截規(guī)則配置(Shiro的攔截器規(guī)則,常用的anon和authc) */ private Map<String, List<String>> filter; /** * Shiro Session 過期時間(分鐘) */ private Long globalSessionTimeout = 30L; }
為解決前后臺分離架構(gòu)的項目下檀轨,未登錄時訪問系統(tǒng)的跳轉(zhuǎn)及對應(yīng)的提示信息Shiro原有邏輯為未登錄則跳轉(zhuǎn)到登錄Url胸竞,在前后臺分離架構(gòu)下,此種方式顯然不能滿足要求参萄,只能修改authc默認過濾器處理流程卫枝,通過將請求轉(zhuǎn)發(fā)到一個新的Url,給出未登錄提示信息讹挎,由前臺去控制路由跳轉(zhuǎn)到登錄頁面
@Slf4j public class SelfDefinedFormAuthenticationFilter extends FormAuthenticationFilter { // 沒有權(quán)限訪問的提示信息跳轉(zhuǎn)URL private String noAccessUrl; public String getNoAccessUrl() { return noAccessUrl; } public SelfDefinedFormAuthenticationFilter setNoAccessUrl(String noAccessUrl) { this.noAccessUrl = noAccessUrl; return this; } // 重寫跳轉(zhuǎn)到登錄URL的邏輯校赤,改為轉(zhuǎn)發(fā)到未登錄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,提供登錄驗證數(shù)據(jù)及授權(quán)邏輯
@Slf4j @Component public class SelfDefinedShiroRealm extends AuthorizingRealm { /** * 授權(quán) * @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相關(guān)配置马篮。
@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默認登錄界面地址,前后端分離中登錄界面跳轉(zhuǎn)應(yīng)由前端路由控制怜奖,后臺僅返回json數(shù)據(jù) shiroFilterFactoryBean.setLoginUrl(shiroProperties.getLoginUrl()); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 憑證匹配器(密碼需要加密時浑测,可使用) * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 設(shè)置加密算法 Md5Hash hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 設(shè)置散列加密次數(shù) 如: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); // 自定義的緩存實現(xiàn) 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實現(xiàn) * 使用的是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層的實現(xiàn) redis實現(xiàn) * 使用的是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); // 登錄成功后歪玲,獲取菜單權(quán)限信息 if (subject.isAuthenticated()) { return "登錄成功"; } } catch (IncorrectCredentialsException e) { return "密碼錯誤"; } catch (LockedAccountException e) { return "登錄失敗迁央,該用戶已被凍結(jié)"; } 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
返回信息
“未登錄,請先登錄再訪問”
夭委,代表請求成功攔截到了幅狮,未登錄不能正常訪問系統(tǒng) -
訪問/user/login進行登錄,然后訪問/user/home
入?yún)?
{ "userName":"admin", "password":"123" }
出參:
"登錄成功"
然后訪問/user/home株灸,成功返回
"這是主頁"
-
注銷后在訪問/user/home
直接請求/user/logout崇摄,訪問/user/home,提示
“未登錄慌烧,請先登錄再訪問”
逐抑,表示成功注銷。
注: /user/noLogin使用的是
@RequestMapping("/noLogin")
屹蚊,是為了保證所有請求方式(GET/POST/PUT/DELETE等)的未登錄請求都能轉(zhuǎn)發(fā)到此接口厕氨,從而正確返回未登錄提示信息进每。 -
以上相關(guān)源碼,請訪問https://github.com/ArtIsLong/shiro-spring-boot-starter.git
關(guān)注我的微信公眾號:FramePower
我會不定期發(fā)布相關(guān)技術(shù)積累命斧,歡迎對技術(shù)有追求田晚、志同道合的朋友加入,一起學習成長国葬!