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

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依赖

    
    
        org.springframework.boot
        spring-boot-starter-data-redis
    
    

    引入第三方Redis序列化工具

    
    
        com.esotericsoftware
        kryo-shaded
        4.0.0
    
    

    注: 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自动配置模块)即可。

    
    
        com.spring4all
        swagger-spring-boot-starter
        1.7.1.RELEASE
    
    

    在使用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类项目集成的依赖包

    
    
        org.apache.shiro
        shiro-spring
        ${shiro.version}
    
    
    
        org.apache.shiro
        shiro-ehcache
        ${shiro.version}
    
    

    除了上面这两个依赖包之外,以便于以后项目做集群,使用Redis存储Shiro的安全验证信息,所以在Github上翻了翻,找到了下面shiro-redis包,它很好的完成了Redis与Shiro的集成,不需要开发人员自己去编码,实现Shiro的SessionDAO接口。

    
    
        org.crazycake
        shiro-redis
        3.0.0
    
    

    还没完,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> 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 filters = shiroFilterFactoryBean.getFilters();
            //将自定义 的FormAuthenticationFilter注入shiroFilter中
            filters.put("authc", new SelfDefinedFormAuthenticationFilter().
                    setNoAccessUrl(shiroProperties.getNoAccessUrl()));
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            Map filterChainDefinitionMap = new LinkedHashMap();
            //注意过滤器配置顺序 不能颠倒
            Map> 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


关注我的微信公众号:FramePower
我会不定期发布相关技术积累,欢迎对技术有追求、志同道合的朋友加入,一起学习成长!


Shiro在前后台分离架构项目中的应用_第1张图片
微信公众号

你可能感兴趣的:(Shiro在前后台分离架构项目中的应用)