理解Shiro身份认证授权原理

shiro安全框架的核心就是认证和授权,前面已谈到关于restful的改造,本文主要谈一下认证和授权过程,以及粗粒度和细粒度的授权等。
参考:https://blog.csdn.net/johnstrive/article/details/74741783

权限管理

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

身份认证

判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

认证关键对象

  • Subject:主体
    访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
  • Principal:身份信息
    是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
  • credential:凭证信息
    是只有主体自己知道的安全信息,如密码、证书等。

授权

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

授权关键对象

授权可简单理解为 whowhat(which) 进行 How 操作:

  • Who,即主体(Subject),主体需要访问系统中的资源。
  • What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
  • How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
    权限分为粗颗粒和细颗粒,粗颗粒权限是指对资源类型的权限,细颗粒权限是对资源实例的权限。

权限模型

对上节中的主体、资源、权限通过数据模型表示。

  • 主体(账号、密码)
  • 角色(角色名称)
  • 主体和角色关系(主体id、角色id)
  • 权限(权限名称、资源id)
  • 角色和权限关系(角色id、权限id)
  • 资源(资源id、访问地址)

如下图:


image.png

权限控制

基于角色的访问控制

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

image.png

图中的判断逻辑代码可以理解为:

if(主体.hasRole("总经理角色id")){
    查询工资
}

缺点:以角色进行访问控制粒度较粗,如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断主体的角色是否是总经理或部门经理”,系统可扩展性差。
修改代码如下:

if(主体.hasRole("总经理角色id") ||  主体.hasRole("部门经理角色id")){
    查询工资
}

基于资源的访问控制

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

image.png

上图中的判断逻辑代码可以理解为:

if(主体.hasPermission("wage:query")){
    查询工资
}

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

粗颗粒度和细颗粒度

什么是粗颗粒度和细颗粒度

对资源类型的管理称为粗颗粒度权限管理,即只控制到菜单、按钮、方法,粗粒度的例子比如:用户具有用户管理的权限,具有导出订单明细的权限。对资源实例的控制称为细颗粒度权限管理,即控制到数据级别的权限,比如:用户只允许修改本部门的员工信息,用户只允许导出自己创建的订单明细。

如何实现粗颗粒度和细颗粒度

对于粗颗粒度的权限管理可以很容易做系统架构级别的功能,即系统功能操作使用统一的粗颗粒度的权限管理
对于细颗粒度的权限管理不建议做成系统架构级别的功能,因为对数据级别的控制是系统的业务需求,随着业务需求的变更业务功能变化的可能性很大,建议对数据级别的权限控制在业务层个性化开发,比如:用户只允许修改自己创建的商品信息可以在service接口添加校验实现,service接口需要传入当前操作人的标识,与商品信息创建人标识对比,不一致则不允许修改商品信息。

基于url拦截

基于url拦截是企业中常用的权限管理方法,实现思路是:将系统操作的每个url配置在权限表中,将权限对应到角色,将角色分配给用户,用户访问系统功能通过Filter进行过虑,过虑器获取到用户访问的url,只要访问的url是用户分配角色中的url则放行继续访问
如下图:

image.png

Shiro中关键对象

  • Subject
    即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。 Subject在shiro中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
  • SecurityManager
    即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
    SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
  • Authenticator
    即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
  • Authorizer
    即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
  • realm
    即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
    注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
  • SessionManager
    即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
  • SessionDAO
    即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
  • CacheManager
    CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
  • Cryptography
    即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

Shiro认证流程

image.png

自定义Realm

认证时需要自己实现realm去获取特定的数据,如验证账号密码等。

public class CustomRealm1 extends AuthorizingRealm {

    @Override
    public String getName() {
        return "customRealm1";
    }

    //支持UsernamePasswordToken
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        //从token中 获取用户身份信息
        String username = (String) token.getPrincipal();
        //拿username从数据库中查询
        //....
        //如果查询不到则返回null
        if(!username.equals("zhang")){//这里模拟查询不到
            return null;
        }

        //获取从数据库查询出来的用户密码 
        String password = "123";//这里使用静态数据模拟。。

        //返回认证信息由父类AuthenticatingRealm进行认证
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                username, password, getName());

        return simpleAuthenticationInfo;
    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        // TODO Auto-generated method stub
        return null;
    }
}

注意,在SimpleAuthenticationInfo中第一个参数是Object类型,即你可以把User对象存入,在后面可以通过SecurityUtils.getSubject().getPrincipal()获取用户信息。

认证密码加密

散列算法

一般用于生成一段文本的摘要信息,散列算法不可逆,将内容可以生成摘要,无法将摘要转成原始内容。散列算法常用于对密码进行散列,常用的散列算法有MD5、SHA。

一般散列算法需要提供一个salt(盐)与原始内容生成摘要信息,这样做的目的是为了安全性,比如:111111的md5值是:96e79218965eb72c92a549dd5a330112,拿着“96e79218965eb72c92a549dd5a330112”去md5破解网站很容易进行破解,如果要是对111111和salt(盐,一个随机数)进行散列,这样虽然密码都是111111加不同的盐会生成不同的散列值。
代码如下:

//md5加密,不加盐
        String password_md5 = new Md5Hash("111111").toString();
        System.out.println("md5加密,不加盐="+password_md5);

        //md5加密,加盐,一次散列
        String password_md5_sale_1 = new Md5Hash("111111", "eteokues", 1).toString();
        System.out.println("password_md5_sale_1="+password_md5_sale_1);
        String password_md5_sale_2 = new Md5Hash("111111", "uiwueylm", 1).toString();
        System.out.println("password_md5_sale_2="+password_md5_sale_2);
        //两次散列相当于md5(md5())

        //使用SimpleHash
        String simpleHash = new SimpleHash("MD5", "111111", "eteokues",1).toString();
        System.out.println(simpleHash);

在realm中使用

@Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        //用户账号
        String username = (String) token.getPrincipal();
        //根据用户账号从数据库取出盐和加密后的值
        //..这里使用静态数据
        //如果根据账号没有找到用户信息则返回null,shiro抛出异常“账号不存在”

        //按照固定规则加密码结果 ,此密码 要在数据库存储,原始密码 是111111,盐是eteokues
        String password = "cb571f7bd7a6f73ab004a70322b963d5";
        //盐,随机数,此随机数也在数据库存储
        String salt = "eteokues";

        //返回认证信息
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                username, password, ByteSource.Util.bytes(salt),getName());


        return simpleAuthenticationInfo;
    }

Shiro授权

授权流程

image.png

授权方式

Shiro 支持三种方式的授权:

  • 编程式:通过写if/else 授权代码块完成:
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
  • 注解式:通过在执行的Java方法上放置相应的注解完成:
@RequiresRoles("admin")
public void hello() {
//有权限
}
  • JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>

Permission 鉴权方式

我们了解到了 Shiro 的Authorization有三种方式,作为细粒度化的 Authorization,Permission 同样也支持粗粒度的 Authorization 的三种方式即代码判断,注解,JSP页面校验。

  • 代码判断/注解
@RequiresRoles("admin")
@RequiresPermissions("admin:view:*")
@RequestMapping(value="/admin")
public String AuthorizationOne () {
    Subject admin =SecurityUtils.getSubject();
    System.out.println("角色 " + admin.getPrincipal());
    admin.isPermitted("admin:view:*");
    System.out.println("角色 " + admin.getPrincipal()+" 是否拥有 admin:view:* 权限:"+admin.isPermitted("admin:view:*"));
    return "admin";
}
  • JSP 页面校验
<!-- 只有 user:view:* 权限才能显示一下内容 -->
<shiro:hasPermission name="user:view:*">
    Only User has 'user:view:*' can access to those words
</shiro:hasPermission>
<br>
<!-- 只有 admin:view:* 权限才能显示一下内容 -->
<shiro:hasPermission name="admin:view:*">
    Only Admin has 'admin:view:*' can access to those words
</shiro:hasPermission>

Shiro Authorization 大致可以被概述为实现了角色授权和权限授权

  • 角色授权:粗粒度授权,为当前Subject 做角色判定或赋予
  • 权限授权:细粒度授权,为当前Role 做权限判定或赋予。
  • 在细粒度授权时要重分理解 资源标识符:操作:对象实例ID 规则的定义的应用的实际场景。

权限字符串规则

权限一般是以字符串的形式表示的,权限字符串的规则是:“资源标识符:操作:资源实例标识符”,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,, 表示操作的分割,* 表示任意资源/操作/实例。
如下:

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

资源-操作-实例

资源-操作-实例 是 Shiro 做细粒度鉴权 persmission时的一种规则。

  • 扩展
    默认支持通配符权限字符串,: 表示资源/操作/实例的分割;, 表示操作的分割,* 表示任意资源/操作/实例。
  • 单个权限
    • user:query、user:edit。
    • 冒号是一个特殊字符,它用来分隔权限字符串的下一部件:第一部分是权限被操作的领域,第二部分是被执行的操作。
    • 多个值:每个部件能够保护多个值。因此,除了授予用户 user:query和 user:edit 权限外,也可以简单地授予他们一个:user:query, edit。
    • 还可以用 * 号代替所有的值,如:user:* , 也可以写:*:query,表示某个用户在所有的领域都有 query 的权限。
    • 例子
      • 单个资源多个权限 user:query user:add 多值 user:query,add
      • 单个资源所有权限 user:query,add,update,delete user:*
      • 所有资源某个权限 *:view
  • 实例级访问控制
    • 规则: 资源标识符:操作:对象实例 ID
    • 这种情况通常会使用三个部件:域、操作、被付诸实施的实例。如:user:edit:manager
    • 也可以使用通配符来定义,如:user:edit:、user::、user::manager
    • 部分省略通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如:user:edit 等价于 user:edit :
      user 等价于 user:
      :*
    • 通配符只能从字符串的结尾处省略部件,也就是说 user:edit 并不等价于 user:*:edit
    • 例子
      • 单个实例的单个权限 printer:query:lp7200 printer:print:epsoncolor
        • 对资源printer的lp7200实例拥有query权限。
        • 对资源printer的epsoncolor实例拥有query权限。
      • 所有实例的单个权限 printer:print:*
        • 对资源printer的所有r实例拥有query权限。
      • 所有实例的所有权限 printer::
        • 对资源printer的1实例拥有所有权限。然后通过如下代码判断
          subject().checkPermissions(“printer:setting:1”, “printer:printe:2”);
      • 单个实例的所有权限 printer:*:lp7200
        • 对资源printer的lp7200实例拥有所有权限
      • 单个实例的多个权限 printer:query,print:lp7200
        • 对资源printer的lp7200实例拥有query,print权限

基于角色的授权

// 用户授权检测 基于角色授权
// 是否有某一个角色
System.out.println("用户是否拥有一个角色:" + subject.hasRole("role1"));
// 是否有多个角色
System.out.println("用户是否拥有多个角色:" + subject.hasAllRoles(Arrays.asList("role1", "role2")));

对应的check方法:

subject.checkRole("role1");
subject.checkRoles(Arrays.asList("role1", "role2"));

基于资源授权

// 基于资源授权
System.out.println("是否拥有某一个权限:" + subject.isPermitted("user:delete"));
System.out.println("是否拥有多个权限:" + subject.isPermittedAll("user:create:1",    "user:delete"));

对应的check方法:

subject.checkPermission("sys:user:delete");
subject.checkPermissions("user:create:1","user:delete");

自定义realm

与上边认证自定义realm一样,大部分情况是要从数据库获取权限数据,这里直接实现基于资源的授权。
在认证章节写的自定义realm类中完善doGetAuthorizationInfo方法,此方法需要完成:根据用户身份信息从数据库查询权限字符串,由shiro进行授权。

// 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        // 获取身份信息
        String username = (String) principals.getPrimaryPrincipal();
        // 根据身份信息从数据库中查询权限数据
        //....这里使用静态数据模拟
        List<String> permissions = new ArrayList<String>();
        permissions.add("user:create");
        permissions.add("user.delete");

        //将权限信息封闭为AuthorizationInfo

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for(String permission:permissions){
            simpleAuthorizationInfo.addStringPermission(permission);
        }

        return simpleAuthorizationInfo;
    }

自定义permission和RolePerminssion

通过addStringPermission 默认是用Permission的实现类封装的 当然也可以实现自定义的Permission。

public class MyPermission implements Permission {

    String permissionCode;
    public MyPermission(String name) {
        permissionCode=name;
    }

    @Override
    public boolean implies(Permission permission) {
        //自定义比较
        // TODO Auto-generated method stub
        return false;
    }
}

当我们调用subject.isPermitted("user:update")会调用将指令传达给SecurityManager,SecurityManager 再将指令传达给授权管理类Authorizer,Authorizer会通过reaml获得授权信息SimpleAuthorizationInfo如果我们返回的授权信息拥有角色 会调用RolePermissionResolver实现类的方法 将角色的权限追加到SimpleAuthorizationInfo(默认是没有实现的)。

public class MyRolePermissionResolver  implements RolePermissionResolver{

    @Override
    public Collection<Permission> resolvePermissionsInRole(String roleString) {
        // TODO Auto-generated method stub
         return Arrays.asList((Permission)new MyPermission("menu:*")); 
    }

这里面是根据角色查询权限

最终 遍历SimpleAuthorizationInfo的权限信息 (我们的权限信息都封装Permission接口实现类 调用implies方法进行比较 如果比较成功返回true 表示授权通过)自定义Permission的好处就是我们可以自定义匹配规则。

@RequiresPermissions 注解说明

@RequiresAuthentication

验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。

@RequiresUser

验证用户是否被记忆,user有两种含义:
一种是成功登录的(subject.isAuthenticated() 结果为true);
另外一种是被记忆的(subject.isRemembered()结果为true)。

@RequiresGuest

验证是否是一个guest的请求,与@RequiresUser完全相反。
换言之,RequiresUser == !RequiresGuest。
此时subject.getPrincipal() 结果为null.

@RequiresRoles

例如:@RequiresRoles("aRoleName");
void someMethod();
如果subject中有aRoleName角色才可以访问方法someMethod。如果没有这个权限则会抛出异常AuthorizationException

@RequiresPermissions

例如: @RequiresPermissions({"file:read", "write:aFile.txt"} )
void someMethod();
要求subject中必须同时含有file:read和write:aFile.txt的权限才能执行方法someMethod()。否则抛出异常AuthorizationException

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

推荐阅读更多精彩内容