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
我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!