安全权限验证框架Shiro初探

一、Shiro简介

Shiro是Apache下的一个安全权限管理框架,对比与Spring Security,它更加的小巧轻便,对于简单的权限控制完全可以应付。

Shiro中各个模块的作用分别是:

  • Authentication: 表示身份的认证,通常就是用户的登录过程。
  • Authorization: 表示给相应的用户授权,判断当前用户是否具有做某件事的权限。
  • Session Manager: 会话管理,当用户登录后再没有退出之前,所有的信息都会保存在这个session中,值得一提的是,这个session并非是JavaWeb中Servlet的session,而是Shiro自己提供的session,如此,该session在非Web环境也是可以使用的。
  • Cryptography: 加密模块负责将数据加密存储到数据库,验证的时候用加密的密文对用户的身份进行认证。
  • Web Support: Shiro可以非常容易地集成到Web项目中。
  • Caching: 缓存使得用户登录后其基本信息不用再次通过查询得到,而是直接从自身的缓存获取,提高了验证的效率。
  • Concurrency: 支持多线程的并发验证模式。
  • Testing: 提供测试支持。
  • Run As: 在得到用户的许可下,另一个用户能以这个用户的身份进行访问。
  • Remember Me: 用户选择该项后,下次登录就不用验证直接得到身份的认证。

Shiro中一些实体术语的解释:

  • Subject: 主体,代表当前需要得到身份认证的用户。
  • SecurityManager: 安全管理器,负责所有的安全认证过程,是整个Shiro的核心。
  • Realm: 数据域,表示Shiro从哪里获取数据来进行身份认证和授权。
  • Authenticator: 认证器,主要负责对主体的认证工作。
  • Authrizer: 授权器,判断用户是否具有某种权限。

二、一个最简单的身份认证过程

首先,我们需要自己创建一个maven的工程,引入必须的jar包:

        
            junit
            junit
            4.9
        
        
            org.apache.shiro
            shiro-core
            1.2.2

然后我们创建一个单元测试类来模拟登录过程中Shiro对用户的身份认证过程:

public class LoginTest {
    private SimpleAccountRealm sar = new SimpleAccountRealm();
    @Before
    public void setUser(){
        this.sar.addAccount("zhangsan", "123321");
    }
    @Test
    public void testLogin(){
        // 创建SecurityManager
        DefaultSecurityManager dsm = new DefaultSecurityManager();
        // 连接到realm
        dsm.setRealm(sar);
        SecurityUtils.setSecurityManager(dsm);
        // 构造需要认证的主体及信息
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123321");
        // 进行认证
        subject.login(token);
        System.out.println("是否认证成功?" + subject.isAuthenticated());
        subject.logout();
        System.out.println("是否认证成功?" + subject.isAuthenticated());
    }
}

在如上的简单示例中,SimpleAccountRealm就是存放着用户真正密码的数据源,将由DefaultSecurityManager来对用户输入的密码进行验证,判断是否通过身份的认证。其执行的结果如下:

是否认证成功?true
是否认证成功?false

三、增加对用户角色的授权验证

在上面的例子中,我们只是对用户的身份进行了认证,然而一个用户可能拥有多种角色,不同用户也有着不同的角色,我们此时就需要对用户是否拥有某种角色来进行授权判断:

public class LoginAndCheckRoleTest {
    private SimpleAccountRealm sar = new SimpleAccountRealm();
    @Before
    public void setUser(){
        this.sar.addAccount("zhangsan", "123321", "admin", "manager");
    }
    @Test
    public void testLogin(){
        // 创建SecurityManager
        DefaultSecurityManager dsm = new DefaultSecurityManager();
        // 连接到realm
        dsm.setRealm(sar);
        SecurityUtils.setSecurityManager(dsm);
        // 构造需要认证的主体及信息
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123321");
        // 进行认证
        subject.login(token);
        System.out.println("是否认证成功?" + subject.isAuthenticated());
        System.out.println("是否是Admin?" + subject.hasRole("admin"));
        subject.checkRole("manager");
        System.out.println("是否是Guest?" + subject.hasRole("guest"));
        subject.logout();
    }
}

在如上的例子中,我们的SimpleAccountRealm中不但存储了用户的密码等身份信息,还存放了角色等授权信息,在用户登录成功后,用户自动就获取了这些角色的授权。其执行的结果如下:

是否认证成功?true
是否是Admin?true
是否是Guest?false

四、使用持久化的外部数据源进行认证和授权

上面都是直接在程序中指定的用户的身份信息和授权信息,在真实的开发中很少用到,更多的是从外部数据库或者缓存中读取。

4.1 使用ini配置文件的外部数据源

首先我们在ini配置文件中存放好用户的身份和授权信息:

[users]
zhangsan=zhangsan999,admin
wangwu=likesea999
[roles]
admin=user:delete

该配置文件有一定的书写格式,[users]代表的是用户的密码和角色,[roles]代表某种角色拥有的权限信息。

public class IniRealmTest {
    @Test
    public void testLogin() {
        IniRealm ir = new IniRealm("classpath:shiro.ini");
        // 创建SecurityManager
        DefaultSecurityManager dsm = new DefaultSecurityManager();
        // 连接到realm
        dsm.setRealm(ir);
        SecurityUtils.setSecurityManager(dsm);
        // 构造需要认证的主体及信息
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
                "zhangsan999");
        // 进行认证
        subject.login(token);
        System.out.println("是否认证成功?" + subject.isAuthenticated());
        System.out.println("是否是Admin?" + subject.hasRole("admin"));
        System.out.println("是否允许删除用户?" + subject.isPermitted("user:delete"));
        System.out.println("是否允许增加用户?" + subject.isPermitted("user:add"));
        subject.logout();
    }
}

我们这次只是将原来的SimpleAccountRealm改成了IniRealm,并指定到具体的ini配置文件,其它的都没有修改。其执行结果如下:

是否认证成功?true
是否是Admin?true
是否允许删除用户?true
是否允许增加用户?false

4.2 使用Mysql的外部数据源

首先我们需要在mysql数据库中创建新的数据库shiro_test,然后在其下创建三张表plu_userplu_roleplu_permission分别存放用户的密码、角色和权限,具体的SQL语句和数据插入语句这里就略过了:

public class JdbcRealmTest {
    public static final String LOGIN_QUERY_SQL = "select password from plu_user where name = ?";
    public static final String ROLE_QUERY_SQL = "select role from plu_role where name = ?";
    public static final String PERMISSION_QUERY_SQL = "select permission from plu_permission where role = ?";
    private static DruidDataSource ds;
    @BeforeClass
    public static void initDataSource(){
        ds = new DruidDataSource();
        ds.setUrl("jdbc:mysql://localhost:3306/shiro_test?serverTimezone=GMT");
        ds.setUsername("root");
        ds.setPassword("root");
    }
    @Test
    public void testLogin() {
        JdbcRealm jr = new JdbcRealm();
        // 自定义指定查询用户登录、角色及权限的SQL语句
        jr.setAuthenticationQuery(LOGIN_QUERY_SQL);
        jr.setUserRolesQuery(ROLE_QUERY_SQL);
        jr.setPermissionsQuery(PERMISSION_QUERY_SQL);
        jr.setDataSource(ds);
        // 默认不会开启权限查询,需要手动开启
        jr.setPermissionsLookupEnabled(true);
        // 创建SecurityManager
        DefaultSecurityManager dsm = new DefaultSecurityManager();
        // 连接到realm
        dsm.setRealm(jr);
        SecurityUtils.setSecurityManager(dsm);
        // 构造需要认证的主体及信息
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
                "zhangsan999");
        // 进行认证
        subject.login(token);
        System.out.println("是否认证成功?" + subject.isAuthenticated());
        System.out.println("是否是Admin?" + subject.hasRole("admin"));
        System.out.println("是否允许删除用户?" + subject.isPermitted("user:delete"));
        System.out.println("是否允许增加用户?" + subject.isPermitted("user:add"));
        subject.logout();
    }
}

如上代码使用了JdbcRealm,默认情况下,Shiro会自动去数据库的usersuser_rolesroles_permissions表中查找身份、角色和权限数据,如果你按照这些表名建立了数据库表并插入了相应的数据,那么就不用如上面代码中一样再指定SQL查询语句了,Shiro都帮你做好了。

在实际的开发过程中,我们的密码、角色和权限表都不太可能取这样的名字,所以就需要如示例中一样自定义查询的SQL语句,值得注意的是,Shiro默认是不开启权限的查询功能的,我们需要在代码中手动地打开。如上代码执行的结果为:

是否认证成功?true
是否是Admin?true
是否允许删除用户?true
是否允许增加用户?false

五、自定义Realm的使用

Shiro提供的几个realm虽然很好用,但是我们或许会有自己的认证和授权逻辑,那么就需要自定义一个realm:

public class MyRealm extends AuthorizingRealm {
    // 用来模拟数据库中的用户和密码对
    private Map userMap = new HashMap<>(16);
    // 用来模拟缓存中的角色信息
    private Set roleSet = new HashSet<>();
    // 用来模拟缓存中的权限信息
    private Set permissionSet = new HashSet<>();
    // 普通类代码块,每次加载类的时候执行,早于类的构造函数执行
    {
        // 用户密码经过加盐加密后的值
        userMap.put("jansen", "e8c5405c5a2d5ee3b7912e2e78c3e189");
        roleSet.add("admin");
        roleSet.add("manager");
        permissionSet.add("user:delete");
        permissionSet.add("user:update");
    }
    // 自定义授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        Set roles = this.getRolesByUserName(userName);
        Set permissions = this.getPermissionsByUserName(userName);
        SimpleAuthorizationInfo sai = new SimpleAuthorizationInfo();
        sai.setStringPermissions(permissions);
        sai.setRoles(roles);
        return sai;
    }
    // 自定义认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {
        // 1:获取主体提交的认证信息中的用户名
        String userName = (String) token.getPrincipal();
        // 2:通过该用户名获取真实的密码
        String password = this.getPasswordByUserName(userName);
        if(password == null){
            System.out.println("查无此人!");
            return null;
        }
        SimpleAuthenticationInfo sai = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        // MD5加盐设置,此处盐为固定的字符串“shiro”
        sai.setCredentialsSalt(ByteSource.Util.bytes("shiro"));
        return sai;
    }
    // 模拟从数据库中获取用户的密码
    private String getPasswordByUserName(String userName){
        return userMap.get(userName);
    }
    // 模拟从缓存中获取角色信息
    private Set getRolesByUserName(String userName){
        return roleSet;
    } 
    // 模拟从缓存或者数据库中获取权限信息
    private Set getPermissionsByUserName(String userName){
        return permissionSet;
    }
}

然后我们就可以使用自定义的realm来进行测试了:

public class MyRealmTest {  
    @Test
    public void testLogin() {
        MyRealm mr = new MyRealm();
        // 如果要将用户的密码加密的话
        HashedCredentialsMatcher hcm = new HashedCredentialsMatcher();
        hcm.setHashAlgorithmName("md5");
        hcm.setHashIterations(1);
        mr.setCredentialsMatcher(hcm);
        // 创建SecurityManager
        DefaultSecurityManager dsm = new DefaultSecurityManager();
        // 连接到realm
        dsm.setRealm(mr);
        SecurityUtils.setSecurityManager(dsm);
        // 构造需要认证的主体及信息
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("jansen","123321");
        // 进行认证
        subject.login(token);
        System.out.println("是否认证成功?" + subject.isAuthenticated());
        System.out.println("是否是Admin?" + subject.hasRole("admin"));
        System.out.println("是否允许删除用户?" + subject.isPermitted("user:delete"));
        System.out.println("是否允许增加用户?" + subject.isPermitted("user:add"));
        subject.logout();
    }
  
    @Test
    public void generateMd5Code(){
        Md5Hash hashCode = new Md5Hash("123321","shiro");
        System.out.println(hashCode);// e8c5405c5a2d5ee3b7912e2e78c3e189
    }
}

我们此处在用户认证的时候加入了加盐MD5加密的逻辑,程序执行的结果为:

是否认证成功?true
是否是Admin?true
是否允许删除用户?true
是否允许增加用户?false

以上这些例子有助于你快速了解Shiro的简单使用,关于进一步和Spring的集成、Session使用等高级的主题将在以后逐步呈现。

你可能感兴趣的:(安全权限验证框架Shiro初探)