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

文章目录
  1. 1. 与Spring Security的比较
  2. 2. Shiro的几个关键要素
  3. 3. Shiro内置的过滤器
  4. 4. Shiro在前后台分离架构的项目中的应用
    1. 4.1. Shiro在传统web项目中的应用与前后台分离项目中的区别
    2. 4.2. 需要关注的几个点
    3. 4.3. 具体实现

Shiro是Apache的强大灵活的开源安全框架

能提供认证、授权、企业会话管理、安全加密、缓存等功能。

与Spring Security的比较

Apache ShiroSpring 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类
anonorg.apache.shiro.web.filter.authc.AnonymousFilter
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
portorg.apache.shiro.web.filter.authz.PortFilter
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter
sslorg.apache.shiro.web.filter.authz.SslFilter
userorg.apache.shiro.web.filter.authc.UserFilter
logoutorg.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依赖

    1
    2
    3
    4
    5
    <!-- Redis -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    引入第三方Redis序列化工具

    1
    2
    3
    4
    5
    6
    <!-- 高效的序列化库kyro -->
    <dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo-shaded</artifactId>
    <version>4.0.0</version>
    </dependency>

    注: Kryo是一个快速高效的Java序列化框架,旨在提供快速、高效和易用的API。无论文件、数据库或网络数据Kryo都可以随时完成序列化。Kryo还可以执行自动深拷贝(克隆)、浅拷贝(克隆)。这是对象到对象的直接拷贝,非对象->字节->对象的拷贝。在后面的文章会分析一下Redis各种序列化方式的效率。

    配置Redis连接(为了方便测试,使用Redis单机版即可)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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自动配置模块)即可。

    1
    2
    3
    4
    5
    6
    <!-- 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。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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类项目集成的依赖包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 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接口。

    1
    2
    3
    4
    5
    6
    <!-- shiro与Redis整合的开源插件 -->
    <dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.0.0</version>
    </dependency>

    还没完,Shiro的常规配置还需要通过JavaConfig的方式去配置(以SpringBoot自动配置的方式实现),废话少说,下面代码见真章。

    shiro的相关拦截规则配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @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,给出未登录提示信息,由前台去控制路由跳转到登录页面

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @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,提供登录验证数据及授权逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    @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相关配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    @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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    @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

      入参:

      1
      2
      3
      4
      {
      "userName":"admin",
      "password":"123"
      }

      出参:

      1
      "登录成功"

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


微信公众号

如果文章对你有帮助,欢迎点击上方按钮打赏作者