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

    <!-- 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


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


微信公众号
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容