java高级开发

理解Shiro身份认证授权原理

2019-01-30  本文已影响2人  老鼠AI大米_Java全栈

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

权限管理

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

身份认证

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

认证关键对象

授权

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

授权关键对象

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

权限模型

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

如下图:


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中关键对象

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 支持三种方式的授权:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
@RequiresRoles("admin")
public void hello() {
//有权限
}
<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";
}
<!-- 只有 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 大致可以被概述为实现了角色授权和权限授权

权限字符串规则

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

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

资源-操作-实例

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

基于角色的授权

// 用户授权检测 基于角色授权
// 是否有某一个角色
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

上一篇 下一篇

猜你喜欢

热点阅读