Shiro-权限管理框架

一. 权限的管理

1.什么是权限管理

基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。权限管理包括用户身份认证和授权两部分,简称认证 和授权。对于需要访问控制的资源,用户首先经过身份认证,认证通过后用户具有该资源的访问权限后才可访问。

2.认证

认证:判断一个用户是否为合法用户的处理过程。认证的过程中可以抽象出来几个关键对象

subject:主体

访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;

principal:身份信息

身份信息是主体进行身份认证的标识,标识必须具有唯一性,如手机号、邮箱等,一个主体可以有多个身份,但是必须只有一个主身份;

credential: 凭证信息

凭证信息是只有主体自己才知道的安全信息,如密码等

3.授权

授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限才可访问系统的资源,对于某些资源没有权限时无法访问的。授权抽象的关键对象:

  • who,即主体(subject),主体需要访问系统中的资源
  • what,即资源(Resource),如系统中的页面等。资源包括资源类型资源实例。比如商品信息为资源类型,类型为t1的商品为资源实例
  • how,权限、许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义。权限分为粗颗粒和细颗粒,粗颗粒权限指对资源类型的权限,细颗粒权限是对资源实例的权限。

4.权限模型

上图通常被称为权限管理的通用模型,不过企业开发中根据系统自身的特点还会对上图进行修改,但是用户、角色、权限、用户角色关系、角色权限关系是需要先理解的。

5.权限控制的方案

基于角色的权限控制

RBAC基于角色的访问控制(
Role-Based Access Control )是以角色为中心进行方位控制。比如:主体的角色为总经理可以查询企业运营报表,查询员工工资等

缺点:以角色进行访问控制粒度较粗,如果上图查询工资信息需要的角色变化为总计里和部门经理,此时就需要修改判断逻辑,系统扩展性差。

基于资源的权限控制

RBAC基于资源的访问控制(
Resource-Based Access Control )是以资源为中心进行访问控制,比如:主体必须具有查询工资权限才可以查询员工工资权限等,访问流程:

if(主体.hasPermission(“查询工资权限标识”)){
查询工资 }

系统设计时定义好查询工资的权限标识,即查询工资所需要的角色变化为总经理和部门经理也只需要将“查询工资信息权限”添加到“部门经理角色”的权限列表中,判断逻辑不用修改,系统可扩展性强。

Shiro介绍

Shiro是Apache旗下的一个开源框架,将软件系统的安全认证相关的功能抽取出来,实现用户身份认证、权限授权、加密、会话管理等功能。

使用Shiro可以非常快速的完成认证、授权等功能的开发,降低系统成本。Shiro使用方法,shiro可以运行在Web应用和非web应用,集群分布式应用中越来越多的用户开始使用Shiro。Java领域中Spring
security(原名Acegi)也是一个开源的权限管理框架,但是Spring security 依赖于Spring运行,而Shiro就相对独立,最重要的是Shiro使用简单、灵活。所以越来越多的用户选择Shiro

Shiro框架核心

SpringBoot下使用Shiro

1.添加依赖

我们可以去Shiro的官网下载相应的依赖,将所需依赖放入pom.xml文件中,所需依赖如下(不包括SpringBoot自带的依赖):

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- shiro连接数据库时所需依赖-->
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--ehcache所需依赖-->
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.6.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>

2.配置Shiro的核心过滤器

@Configuration
public class ShiroFilterConf {
    //shiro过滤器的配置
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String,String>  map = new HashMap<>();
        map.put("xxx","anon");//anon是匿名过滤器的简写
        map.put("xxx","authc");//authc是认证过滤器的简写
      //设置登录入口
        shiroFilterFactoryBean.setLoginUrl("/userLogin.jsp");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    //Shiro的核心对象 安全管理器
    @Bean
    public SecurityManager getSecurityManager(Realm realm,CacheManager cacheManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setCacheManager(cacheManager);
        securityManager.setRealm(realm);
        return  securityManager;
    }

   //自定义Realm
    @Bean
    public Realm getRealm(CredentialsMatcher credentialsMatcher){
        //自定义的Realm
        MyRealm myRealm = new MyRealm();
        myRealm.setCredentialsMatcher(credentialsMatcher);
        return myRealm;
    }
    //选择HashedCredentialsMatcher 为凭证匹配器
    @Bean
    public CredentialsMatcher getCredentialsMatcher(){
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //设置加密的方式,不区分大小写
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //设置散列的次数
        hashedCredentialsMatcher.setHashIterations(1024);
        return hashedCredentialsMatcher;
    }


    @Bean
    public CacheManager getCacheManager(){
        EhCacheManager cacheManager = new EhCacheManager();
        return  cacheManager;
    }

}

3.自定义Realm

Shiro默认使用自带的IniRealm,IniRealm从ini配置文件中读取信息,而实际需求肯定是从数据库中读取用户信息,所以需要自定义Realm

shiro提供的realm体系如下

最基础的是Realm接口,CachingRealm负责缓存处理,AuthenticatingRealm负责认证,AuthorizingRealm继承
AuthenticatingRealm 后又负责授权,通常自定的Realm继承AuthorizingRealm即可

public class MyRealm extends AuthorizingRealm {

    @Autowired
    UserService userService;

    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("---------授权-------------");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 通过主体 查  角色   通过角色 查  权限
         String primaryPrincipal = (String)principalCollection.getPrimaryPrincipal();
        //xxxx查询角色、查询权限
        authorizationInfo.addRole("角色");
        authorizationInfo.addStringPermission("权限");
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("---------验证-------------");
        String phone = (String) authenticationToken.getPrincipal();
        User user = userService.queryOne(phone);
        AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(phone,user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
        return authenticationInfo;
    }
}

4.使用

@RequestMapping("/loginUser")
    public String loginShiro(User user) {
        //其他业务代码。。

        Subject subject = SecurityUtils.getSubject();//创建主体
        AuthenticationToken token = new UsernamePasswordToken(user.getPhone(),user.getPassword());//创建身份令牌
        
            try {
                //调用登陆方法
                subject.login(token);
                return "xxx";
            } catch (UnknownAccountException e) {
                e.printStackTrace();
                return "xxx";
            } catch (IncorrectCredentialsException e){
                e.printStackTrace();
                return "xxx";
            }
    }

5.常见异常

  • UnknownAccountException:账号不存在异常
  • IncorrectCredentialsException:密码错误异常
  • DisabledAccountExceptio:账号被禁用
  • LockedAccountException:账号被锁定
  • ExcessiveAttemptsException:登陆次数过多
  • ExpiredCredentialsException:凭证过期

Shiro的认证分析

我们先来看Shiro默认情况下的认证流程

  1. 创建token(UsernamePasswordToken)令牌,token中有用户提交的认证信息即账号和密码
  2. 主体执行登录方法subject.login(token),通过调用securityManager的login方法最终由authenticate(token)进行验证;

通过追踪shiro的源码,我们来看一下执行流程

在subject.login(token)处打断点,debug进入

DelegatingSubject

DelegatingSubject调用了SecurityManager的login方法,我们跟过去看

DefaultSecurityManager

在默认的SecurityManager中调用
authenticate (token),我们经过一系列追踪

ModularRealmAuthenticator

ModularRealmAuthenticator调用自身方法

此时调用Realm中的方法来获取用户信息,我们跟进去

AuthenticatingRealm
抽象方法

AuthenticatingRealm调用自身doGetAuthenticationInfo方法,而doGetAuthenticationInfo方法是抽象方法,我们猜测继续追踪肯定会是其实现类的该方法上

MyRealm

果然如我们所料,我们来到了我们之前自定的Realm上。可以发现doGetAuthenticationInfo方法只是将查询到的用户信息封装为AuthenticationInfo后就返回了,并没有对用户信息进行比对,我们继续追踪

AuthenticatingRealm

看方法名和参数,可以推测此方法通过前台传入的token和后台查询的info进行了比对,我们进去看

比对

果然,该方法通过equals方法,将凭证信息(
credential 密码)进行比对。到此为止,整个认证流程就十分清晰了。

Shiro的授权

我们自定的Realm中实现了两个方法doGetAuthorizationInfo和doGetAuthenticationInfo(这两个方法真是双胞胎,考验眼神),其中doGetAuthenticationInfo我们经过分析已经知道是用来认证的时候查询用户信息的,而doGetAuthorizationInfo就是用来获取授权的信息的。具体的执行流程,同学们有兴趣的可以参考上面认证的流程,去源码中看看,我们在这里看看授权时常用的API操作。

权限字符串规则

权限字符串的规则是:“资源标识符:操作:资源实例标识符”,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。例如:

  • 用户创建权限:user:create,或user:create:*
  • 用户修改实例001的权限:user:update:001
  • 用户实例001的所有权限:user:*:001

Java环境下基于角色的授权API

//判断当前主体是否包含此角色
boolean b = subject.hasRole("super");
List<String> list = Arrays.asList("super", "admin");
//判断当前主体是否包含某个角色
boolean[] booleans = subject.hasRoles(list);
//判断当前主体是否包含全部的角色
boolean b = subject.hasAllRoles(list);

Java环境下基于资源的授权API

boolean b = subject.isPermitted("admin:delete");
String[] strs={"admin:delete", "admin:add"};
boolean[] permitted = subject.isPermitted(strs);
boolean permittedAll = subject.isPermittedAll(strs);

在MyRealm中授权

在ShiroFilterConf 中我们授权时是通过过滤器进行授权的

map.put("/css/**","anon");
map.put("/main/**","authc");
过滤器

anon,authcBasic,auchc,user是认证过滤器,

perms,roles,ssl,rest,port是授权过滤器

anon:例子/admins/**=anon 没有参数,表示可以匿名使用,不需要登陆。

authc:例如/admins/user/**=authc表示需要认证(登录)才能使用。

roles:例子/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[“admin,guest”],每个参数通过才算通过,相当于hasAllRoles()方法。

perms:例子/admins/user/**=perms[user:add:*],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms[“user:add:*,user:modify:*”],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。

rest:例子/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。

port:例子/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。

authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证

ssl:例子/admins/user/**=ssl没有参数,表示安全的url请求,协议为https

user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查

Shiro常用标签

使用这些标签,在jsp页面上进行权限控制

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:principal></shiro:principal>  //用户的身份信息
<shiro:authenticated></shiro:authenticated> //认证成功  执行标签体的内容
<shiro:notAuthenticated></shiro:notAuthenticated> //未认证  执行标签体内容
//基于角色的权限管理
<shiro:hasRole name="super"></shiro:hasRole>
<shiro:hasAnyRoles name="admin,super"></shiro:hasAnyRoles>
//基于资源的权限管理
<shiro:hasPermission name="user:delete"></shiro:hasPermission>