美文网首页shiro程序小寨分布式
Shiro在前后台分离架构项目中的应用

Shiro在前后台分离架构项目中的应用

作者: 梦中一点心雨 | 来源:发表于2018-12-18 14:26 被阅读22次

    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测试请求的拦截。

      1. 未登录访问/user/home

        返回信息“未登录,请先登录再访问”,代表请求成功拦截到了,未登录不能正常访问系统

      2. 访问/user/login进行登录,然后访问/user/home

        入参:

        {
            "userName":"admin",
            "password":"123"
        }
        

        出参:

        "登录成功"
        

        然后访问/user/home,成功返回"这是主页"

      3. 注销后在访问/user/home

        直接请求/user/logout,访问/user/home,提示“未登录,请先登录再访问”,表示成功注销。

      注: /user/noLogin使用的是@RequestMapping("/noLogin"),是为了保证所有请求方式(GET/POST/PUT/DELETE等)的未登录请求都能转发到此接口,从而正确返回未登录提示信息。

    以上相关源码,请访问https://github.com/ArtIsLong/shiro-spring-boot-starter.git

    相关文章

      网友评论

        本文标题:Shiro在前后台分离架构项目中的应用

        本文链接:https://www.haomeiwen.com/subject/jqqlkqtx.html