11.Shiro 对 Spring 的支持 - 图1

11.Shiro 对 Spring 的支持

Shiro 的第一个版本发布于 2004 年, Spring 项目起源于 2002 年,在 Shiro 最初的版本中没有与 Spring 相关的内容。后来,随着 Spring 的流行,从 2010 年开始, Shiro 开始提供对 Spring 的支持,推出了一个独立的 jar 包,名为 shiro-spring。从 2018 年开始, Shiro 在 v1.4 中开始增强对 SpringBoot 的支持。

在本章中,我们先解释整合的步骤,然后再对运行机制和源码进行分析,内容结构如下:

  • Shiro 与 SpringBoot 的整合步骤
  • 运行机制和源码分析

11.1 Shiro 与 SpringBoot 的整合步骤

11.1.1 添加项目依赖

首先,需要在 SpringBoot 项目的 pom.xml 文件中添加 Shiro 的相关依赖。以下是 Maven 的依赖配置:

  1. <dependencies>
  2. <!-- SpringBoot 相关的依赖,这里省略 -->
  3. <!-- Shiro 相关的依赖 -->
  4. <!-- 注意:由于 shiro-spring-boot-starter 与 SpringBoot 之间存在版本依赖,这里还是采用 Shiro 1.12 版本的配置方式-->
  5. <dependency>
  6. <groupId>org.apache.shiro</groupId>
  7. <artifactId>shiro-spring</artifactId>
  8. <classifier>jakarta</classifier>
  9. <version>${shiro.version}</version>
  10. <exclusions>
  11. <exclusion>
  12. <groupId>org.apache.shiro</groupId>
  13. <artifactId>shiro-core</artifactId>
  14. </exclusion>
  15. <exclusion>
  16. <groupId>org.apache.shiro</groupId>
  17. <artifactId>shiro-web</artifactId>
  18. </exclusion>
  19. </exclusions>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.apache.shiro</groupId>
  23. <artifactId>shiro-core</artifactId>
  24. <classifier>jakarta</classifier>
  25. <version>${shiro.version}</version>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.apache.shiro</groupId>
  29. <artifactId>shiro-web</artifactId>
  30. <classifier>jakarta</classifier>
  31. <version>${shiro.version}</version>
  32. <exclusions>
  33. <exclusion>
  34. <groupId>org.apache.shiro</groupId>
  35. <artifactId>shiro-core</artifactId>
  36. </exclusion>
  37. </exclusions>
  38. </dependency>
  39. <dependency>
  40. <groupId>org.apache.shiro</groupId>
  41. <artifactId>shiro-ehcache</artifactId>
  42. <version>${shiro.version}</version>
  43. </dependency>
  44. </dependencies>

这是常见的 Maven 配置文件,不作解释。有一个点需要注意: Shiro 的版本与 SpringBoot 的版本之间存在兼容性问题,如果读者使用了最新的 SpringBoot 版本,或者使用了 shiro-spring-boot-starter ,需要自己修改配置并测试兼容性。

11.1.2 编写 ShiroConfig.java 文件

在 SpringBoot 中,Shiro 的配置通常使用 Java 配置类来实现,示例代码如下:

  1. @Configuration
  2. public class ShiroConfig {
  3. //...
  4. //这里配置一堆 Shiro 相关的 Bean
  5. }

这是常见的 Spring 配置类,不作解释。

11.1.3 实现自定义 Realm

如前所述,Realm 是 Shiro 的核心组件之一,用于从数据源中获取用户的认证和授权信息,以下是自定义 NiceFishMySQLRealm 示例:

  1. package com.nicefish.rbac.shiro.realm;
  2. import com.nicefish.rbac.jpa.entity.UserEntity;
  3. import com.nicefish.rbac.service.IUserService;
  4. import com.nicefish.rbac.shiro.util.NiceFishSecurityUtils;
  5. import org.apache.shiro.authc.*;
  6. import org.apache.shiro.authz.AuthorizationInfo;
  7. import org.apache.shiro.authz.SimpleAuthorizationInfo;
  8. import org.apache.shiro.realm.AuthorizingRealm;
  9. import org.apache.shiro.subject.PrincipalCollection;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import java.util.Set;
  14. public class NiceFishMySQLRealm extends AuthorizingRealm {
  15. private static final Logger logger = LoggerFactory.getLogger(NiceFishMySQLRealm.class);
  16. @Autowired
  17. private IUserService userService;
  18. @Override
  19. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  20. UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
  21. String username = usernamePasswordToken.getUsername();
  22. String password = usernamePasswordToken.getPassword()!=null?new String(usernamePasswordToken.getPassword()):"";
  23. UserEntity userEntity;
  24. try {
  25. userEntity = userService.checkUser(username, password);
  26. logger.debug("UserName>"+username);
  27. logger.debug("Password>"+password);
  28. }catch (Exception e) {
  29. logger.error(username + "登录失败{}", e.getMessage());
  30. throw new AuthenticationException(e.getMessage(), e);
  31. }
  32. SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userEntity, password, getName());
  33. return info;
  34. }
  35. @Override
  36. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  37. Integer userId= NiceFishSecurityUtils.getUserId();
  38. Set<String> permStrs=this.userService.getPermStringsByUserId(userId);
  39. logger.debug(permStrs.toString());
  40. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
  41. info.setStringPermissions(permStrs);
  42. return info;
  43. }
  44. }

在 ShiroConfig.java 中配置 NiceFishMySQLRealm ,暴露给 Spring 容器:

  1. @Bean
  2. public NiceFishMySQLRealm nicefishRbacRealm() {
  3. NiceFishMySQLRealm niceFishMySQLRealm = new NiceFishMySQLRealm();
  4. niceFishMySQLRealm.setCachingEnabled(true);
  5. niceFishMySQLRealm.setAuthenticationCachingEnabled(true);
  6. niceFishMySQLRealm.setAuthenticationCacheName("authenticationCache");
  7. niceFishMySQLRealm.setAuthorizationCachingEnabled(true);
  8. niceFishMySQLRealm.setAuthorizationCacheName("authorizationCache");
  9. return niceFishMySQLRealm;
  10. }

11.1.4 配置过滤器

在 ShiroConfig.java 中配置过滤器示例代码如下:

  1. @Bean
  2. public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
  3. ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  4. shiroFilterFactoryBean.setSecurityManager(securityManager);
  5. shiroFilterFactoryBean.setLoginUrl(loginUrl);
  6. shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
  7. Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
  8. filters.put("captchaValidateFilter", captchaValidateFilter());
  9. shiroFilterFactoryBean.setFilters(filters);
  10. //所有静态资源交给Nginx管理,这里只配置与 shiro 相关的过滤器。
  11. LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
  12. filterChainDefinitionMap.put("/nicefish/cms/post/write-post", "captchaValidateFilter");
  13. filterChainDefinitionMap.put("/nicefish/cms/post/update-post", "captchaValidateFilter");
  14. filterChainDefinitionMap.put("/nicefish/cms/comment/write-comment", "captchaValidateFilter");
  15. filterChainDefinitionMap.put("/nicefish/auth/user/register", "anon,captchaValidateFilter");
  16. filterChainDefinitionMap.put("/nicefish/auth/shiro/login", "anon,captchaValidateFilter");
  17. filterChainDefinitionMap.put("/**", "anon");
  18. shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  19. return shiroFilterFactoryBean;
  20. }

ShiroFilterFactoryBean 是 Shiro 与 Spring 集成的核心类之一,它的主要功能是把自定义的 Filter 插入到 Spring 的过滤器链中,从而拦截到符合配置项的请求,转发给 Shiro 处理。

11.2 运行机制和源码分析

接下来,我们来解析运行机制和源代码,重点讨论以下 4 个最关键的主题:

  • ShiroConfig.java 配置文件与 Bean 的生命周期管理
  • SpringShiroFilter 在运行时过滤请求
  • 注解扫描与方法拦截
  • Session 的处理

11.2.1 ShiroConfig.java 配置文件与 Bean 的生命周期管理

在早期的版本中,开发者必须使用 XML 文件来配置 Shiro 。从 Spring 4.x 版本(2013 年左右)开始,使用 Java 代码编写配置文件逐渐成为主流,取代了大量的 XML 配置。这一趋势在 SpringBoot 推出之后(2014 年)得到了广泛普及。

Shiro 从 1.3 版本(2016 年)开始正式支持使用 Java 文件进行配置,而不再依赖 XML 或 .ini 文件配置。随着 Shiro 1.4 版本的发布,Shiro 进一步加强了对 SpringBoot 的支持,提供了 shiro-spring-boot-starter ,并改进了 @Configuration 注解的支持,使得在 SpringBoot 环境中配置 Shiro 更加方便,示例代码如下:

  1. @Configuration
  2. public class ShiroConfig {
  3. //...
  4. @Bean
  5. //...
  6. //...
  7. }

@Configuration 是 Spring 提供的注解,当 Spring 容器启动时,会扫描此注解,如果在其中发现了 @Bean 进行装饰的实例, Spring 就会自动管理这些实例的生命周期。

对于 Shiro 来说,需要把以下组件暴露给 Spring 容器进行管理: Realm、SecurityManager、ShiroFilterFactoryBean、CookieRememberMeManager、SessionDAO、EventBus、EhCacheManager 。

11.2.2 SpringShiroFilter 在运行时过滤请求

按照职责划分, Shiro 在整个系统中只处理与权限相关的请求。所以 Shiro 定义了自己的 Filter 用来过滤请求,类名是 SpringShiroFilter 。同时,为了配置方便, Shiro 定义了一个工厂类叫做 ShiroFilterFactoryBean ,这个类会辅助创建 SpringShiroFilter 的实例,在上面增加一些自定义的配置,代码示例如下:

  1. @Bean
  2. public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
  3. ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  4. shiroFilterFactoryBean.setSecurityManager(securityManager);
  5. shiroFilterFactoryBean.setLoginUrl(loginUrl);
  6. shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
  7. Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
  8. filters.put("captchaValidateFilter", captchaValidateFilter());
  9. shiroFilterFactoryBean.setFilters(filters);
  10. //所有静态资源交给Nginx管理,这里只配置与 shiro 相关的过滤器。
  11. LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
  12. filterChainDefinitionMap.put("/nicefish/cms/post/write-post", "captchaValidateFilter");
  13. filterChainDefinitionMap.put("/nicefish/cms/post/update-post", "captchaValidateFilter");
  14. filterChainDefinitionMap.put("/nicefish/cms/comment/write-comment", "captchaValidateFilter");
  15. filterChainDefinitionMap.put("/nicefish/auth/user/register", "anon,captchaValidateFilter");
  16. filterChainDefinitionMap.put("/nicefish/auth/shiro/login", "anon,captchaValidateFilter");
  17. filterChainDefinitionMap.put("/**", "anon");
  18. shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  19. return shiroFilterFactoryBean;
  20. }

当 Spring 容器启动时,会解析这个 Bean ,获得过滤器的实例,并且加入到过滤器链(Filter Chain)中去。在运行时,当请求到来的时候,被匹配到的请求就会转发给 Shiro 处理。

11.2.3 注解扫描与方法拦截

当匹配到的请求转发给 Shiro 之后,方法调用链就进入了 Shiro 框架内部,接下来,我们来看 Shiro 是如何进行封装,并与 Spring 进行对接的。

Shiro 自己实现了一套轻量级的 AOP 机制,这一套机制没有 Spring 那么复杂,也不是为了取代 Spring 。在 Shiro 的 AOP 机制中,主要有两个核心的处理流程:注解扫描、方法拦截。这样实现的目的是:

  • 方便与 Spring 集成:当 Spring 启动时, AuthorizationAttributeSourceAdvisor 这个类会扫描所有权限注解,对于扫描到的方法, Spring 会生成代理对象,并将 ShiroMethodInterceptor 加入到拦截器链中。当带有权限注解的方法被调用时,代理对象会先调用 MethodInterceptor 。在 MethodInterceptor 内部会通过 SecurityManager 调用相应的权限检查逻辑,如果检查通过,则继续执行方法,否则抛出异常。
  • 可以脱离 Spring 框架独立运行,也可以与其它 AOP 框架集成。

我们先来看 Shiro 是如何与 Spring 配合扫描权限注解的。

在 shiro-spring-XXX-jakarta.jar 中,有一个关键的配置类 ShiroAnnotationProcessorConfiguration ,它是 Shiro 与 Spring 整合的关键桥梁。ShiroAnnotationProcessorConfiguration 的代码非常少,完整列举如下:

  1. package org.apache.shiro.spring.config;
  2. import org.apache.shiro.mgt.SecurityManager;
  3. import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
  4. import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.context.annotation.DependsOn;
  8. /**
  9. * @since 1.4.0
  10. */
  11. @Configuration
  12. public class ShiroAnnotationProcessorConfiguration extends AbstractShiroAnnotationProcessorConfiguration{
  13. @Bean
  14. @DependsOn("lifecycleBeanPostProcessor")
  15. protected DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
  16. return super.defaultAdvisorAutoProxyCreator();
  17. }
  18. @Bean
  19. protected AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
  20. return super.authorizationAttributeSourceAdvisor(securityManager);
  21. }
  22. }
  • @Configuration 表示这是一个配置类,Spring 容器启动时,会自动扫描并解析这个配置类中的内容,然后自动管理其中 @Bean 的生命周期。
  • 用 @Bean 注解定义了两个 Bean ,显然,这两个 Bean 已经交给 Spring 容器管理了。
  • 第一个 Bean 是 DefaultAdvisorAutoProxyCreator ,这是 Spring 框架中的一个类,它的作用是自动创建代理类。 @DependsOn(“lifecycleBeanPostProcessor”):这个注解表示这个 Bean 的创建依赖于 lifecycleBeanPostProcessor,因为 lifecycleBeanPostProcessor 管理了 Shiro 中一些重要 Bean 的生命周期。
  • 第二个 Bean 是 AuthorizationAttributeSourceAdvisor,这是 Shiro 自己实现的一个 AOP 顾问类,这个类非常关键,它负责在运行时检查被调用的方法上是否带有权限注解。

AuthorizationAttributeSourceAdvisor 的关键源代码如下:

  1. public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {
  2. private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
  3. new Class[] {
  4. RequiresPermissions.class, RequiresRoles.class,
  5. RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
  6. };
  7. //这里很关键,在构造函数中直接 new 了一个顾问,并调用 setAdvice 设置给 Spring。
  8. public AuthorizationAttributeSourceAdvisor() {
  9. setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
  10. }
  11. //...
  12. //这里扫描指定的 Class 上是否存在权限注解。
  13. private boolean isAuthzAnnotationPresent(Class<?> targetClazz) {
  14. for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
  15. Annotation a = AnnotationUtils.findAnnotation(targetClazz, annClass);
  16. if ( a != null ) {
  17. return true;
  18. }
  19. }
  20. return false;
  21. }
  22. //...
  23. }

整体上说, Shiro 自己实现了一个方法切点顾问类(Method Pointcut Advisor),通过 @Configuration 和 @Bean 这两个注解,把它暴露给 Spring 去管理,而在顾问类的构造方法中,直接设置了一个方法拦截器,也就是名字很长的 AopAllianceAnnotationsAuthorizingMethodInterceptor。在运行时,这个方法拦截器将会拦截所有带有权限注解的方法,先进行权限校验。

那么, AopAllianceAnnotationsAuthorizingMethodInterceptor 具体又做了什么呢?我们来看它的关键源代码(已省略无关代码):

  1. public class AopAllianceAnnotationsAuthorizingMethodInterceptor
  2. extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor {
  3. public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
  4. List<AuthorizingAnnotationMethodInterceptor> interceptors =
  5. new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
  6. //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
  7. //raw JDK resolution process.
  8. AnnotationResolver resolver = new SpringAnnotationResolver();
  9. //we can re-use the same resolver instance - it does not retain state:
  10. //注意这里的拦截器, 把 Shiro 实现的5种注解拦截器全部加入到拦截器中去。
  11. interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
  12. interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
  13. interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
  14. interceptors.add(new UserAnnotationMethodInterceptor(resolver));
  15. interceptors.add(new GuestAnnotationMethodInterceptor(resolver));
  16. setMethodInterceptors(interceptors);
  17. }
  18. }

权限注解拦截器相关的继承结构如下:

11.Shiro 对 Spring 的支持 - 图2

我们来分析 PermissionAnnotationMethodInterceptor 的实现,其它拦截器的实现逻辑类似,在 PermissionAnnotationMethodInterceptor 内部,会调用工具类 PermissionAnnotationHandler来负责真正的权限检测功能,其中的关键代码如下(已省略无关代码):

  1. public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {
  2. //...
  3. public void assertAuthorized(Annotation a) throws AuthorizationException {
  4. if (!(a instanceof RequiresPermissions)) return;
  5. RequiresPermissions rpAnnotation = (RequiresPermissions) a;
  6. String[] perms = getAnnotationValue(a);
  7. Subject subject = getSubject();
  8. if (perms.length == 1) {
  9. //检查权限
  10. subject.checkPermission(perms[0]);
  11. return;
  12. }
  13. if (Logical.AND.equals(rpAnnotation.logical())) {
  14. //检查权限
  15. getSubject().checkPermissions(perms);
  16. return;
  17. }
  18. if (Logical.OR.equals(rpAnnotation.logical())) {
  19. // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
  20. boolean hasAtLeastOnePermission = false;
  21. for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true;
  22. // Cause the exception if none of the role match, note that the exception message will be a bit misleading
  23. if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]);
  24. }
  25. }
  26. }

也就是说:只要我们在某个方法加上了权限注解, Spring 在启动的时候就会自动创建代理类。然后在运行时,当这个方法被调用的时候,Shiro 中的对应的权限拦截器就会首先被执行,这就是权限注解的运行机制。

11.2.4 Session 的处理

在 ShiroConfig 中,一般还会配置 SessionManager 的实例,并且通过 @Bean 注解暴露给 Spring ,让 Spring 容器去管理它的生命周期,示例代码如下:

  1. @Bean
  2. public DefaultWebSessionManager sessionManager() {
  3. DefaultWebSessionManager defaultWebSessionMgr = new DefaultWebSessionManager();
  4. //启用 EhCache 缓存,Shiro 默认不启用
  5. //启用 EhCache 缓存之后,需要在持久化的 Session 和缓存中的 Session 之间进行数据同步。
  6. //EhCache 实例配置位于 classpath:ehcache-shiro.xml 文件中,session 默认缓存在 "shiro-activeSessionCache" 实例中。
  7. //认证、授权、Session,全部使用同一个 EhCache 运行时对象。
  8. defaultWebSessionMgr.setCacheManager(ehCacheManager());
  9. defaultWebSessionMgr.setDeleteInvalidSessions(true);
  10. defaultWebSessionMgr.setGlobalSessionTimeout(timeout);
  11. defaultWebSessionMgr.setSessionIdUrlRewritingEnabled(false);
  12. //启用定时调度器,用来清理 Session ,Shiro 默认采用内置的 ExecutorServiceSessionValidationScheduler 进行调度。
  13. defaultWebSessionMgr.setSessionValidationSchedulerEnabled(true);
  14. defaultWebSessionMgr.setSessionValidationInterval(validationInterval);
  15. defaultWebSessionMgr.setSessionDAO(sessionDAO());
  16. defaultWebSessionMgr.setSessionFactory(sessionFactory());
  17. return defaultWebSessionMgr;
  18. }

在之前的内容中,我们已经知道, SessionManager 的实例最终会被设置给 SecurityManager ,这个动作是由 Spring 的依赖注入机制自动完成的。关于 SessionManager 与 SecurityManager 之间的关系,请翻阅“第三章 身份验证与授权”,这里不再解释。

11.3 本章小结

本章详细分析了 Shiro 与 SpringBoot 整合时的运行机制。

资源链接

版权声明

本书基于 CC BY-NC-ND 4.0 许可协议发布,自由转载-非商用-非衍生-保持署名。

版权归大漠穷秋所有 © 2024 ,侵权必究。