Shiro初探

Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。

Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但并非必须如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员确保其应用程序安全的工作。其基本功能点如下图所示:

Authentication身份认证/登录,验证用户是不是拥有相应的身份;

Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web SupportWeb支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrencyshiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing提供测试支持;

Run As允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

 

Shiro的架构有三个主要概念:和SubjectSecurityManagerRealms。下图是这些组件如何交互的详细概述如下图:

 

可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

Subject主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

从这个意义上讲,Realm本质上是一个特定于安全性的DAO:它封装了数据源的连接细节,并根据需要使相关数据可用于Shiro。配置Shiro时,必须至少指定一个Realm用于身份验证和/或授权。所述SecurityManager可与多个境界被配置,但至少有一个是必需的。

Shiro提供了开箱即用的领域,可以连接到许多安全数据源(也称为目录),如LDAP,关系数据库(JDBC),文本配置源(如INI和属性文件等)。如果默认域不符合您的需要,您可以插入自己的Realm实现来表示自定义数据源。

与其他内部组件一样,Shiro SecurityManager管理如何使用Realms获取要表示为Subject实例的安全性和身份数据。

 

详细的架构图如下:


  

Subject主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的核心;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDAODAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography密码模块,Shiro提供了一些常见的加密组件用于如密码加密/解密的。

应用程序SecurityManager执行安全操作并管理所有应用程序用户的状态。在Shiro的默认SecurityManager实现中,这包括:

  • Authentication

  • Authorization

  • Session Management

  • Cache Management

  • Realm coordination

  • Event propagation

  • “Remember Me” Services

  • Subject creation

  • Logout and more.

为了简化配置并支持灵活的配置/可插拔性,Shiro的实现在设计上都是高度模块化的——实际上模块化到SecurityManager实现(及其类层次结构)根本不做什么。相反,SecurityManager实现主要充当轻量级的“容器”组件,将几乎所有行为委托给嵌套/封装的组件。这种“包装器”设计反映在上面详细的架构图中。

当组件实际执行逻辑时,SecurityManager实现知道如何以及何时协调组件以获得正确的行为。

        Shiro认证:

认证流程:

创建SecurityManager -->Subject提交认证 -->SecurityMaager认证 -->Authenticator认证-->Realm验证

代码演示环境:

java JDK 1.8

spring boot 2.0.4

IDEA

MySql 5.6

maven 3.5.4 

添加maven依赖:

   <!-- shiro-spring -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>

创建一个测试类AUthTest:

public class AUthTest {
    SimpleAccountRealm simpleAccountRealm=new SimpleAccountRealm();
    @Before
    public void add()
    {
        simpleAccountRealm.addAccount("zhangsan","123456");//在认证的时候从realm取数据
    }

    @Test
    public void TestAuth()
    {
        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);//将simpleAccountRealm设置到我们的环境当中来
        //2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

 

shiro授权

授权流程:

创建SecurityManager -->Subject授权 -->SecurityManger授权 -->Authorize授权-->Realm获取角色权限数据

 

认证的过程和授权的过程是类似的,我们依然用AuthTest这个类来测试:

public class AUthTest {
    SimpleAccountRealm simpleAccountRealm=new SimpleAccountRealm();
    @Before
    public void add()
    {
       // simpleAccountRealm.addAccount("zhangsan","123456","admin");//再添加用户的时候我们给用户赋予一个adminq权限
        simpleAccountRealm.addAccount("zhangsan","123456","admin","user");//再添加用户的时候我们给用户赋予一个adminq权限
    }

    @Test
    public void TestAuth()
    {
        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);//将simpleAccountRealm设置到我们的环境当中来
        //2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
        subject.checkRole("admin");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
        //需要在认证完成之后检查权限
        subject.checkRole("admin");
        subject.checkRoles("admin","user");
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

Shiro内置Realm:

IniRealm  通过ini文件来设置用户和权限

JdbcRealm   需要连接数据库来取得角色和权限数据

 

IniRealm:

在Resources目录下面创建 user.ini文件

[users]
zhangsan=123456,admin
[roles]
admin=user:delete,user:update

我们依然是创建一个测试类IniRealmTest:

public class IniRealmTest
{
    @Test
    public void TestAuth()
    {
        IniRealm iniRealm =new IniRealm("classpath:user.ini");
        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(iniRealm);
        //2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
        subject.checkRole("admin");
        subject.checkPermissions("user:update","user:dele2te");//Subject does not have permission [user:dele2te]
    //    subject.checkPermission("user:dele2te");//Subject does not have permission [user:dele2te]
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

JdbcRealm:

需要连接数据库,我们先在maven中引入:

      <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>

创建一个测试类JdbcRealmTest

public class JdbcRealmTest
{
    DruidDataSource dataSource=new DruidDataSource();
    {
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test1");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
    }
    @Test
    public void TestAuth()
    {
        //jdbcrealm需要访问数据库
        JdbcRealm jdbcRealm=new JdbcRealm();
        jdbcRealm.setDataSource(dataSource);

        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);
        //2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法

        //    subject.checkPermission("user:dele2te");//Subject does not have permission [user:dele2te]
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

运行这个测试方法,我们需要在test1数据库中创建users表。大家可能会疑惑,我们这里没有写任何sql 那么realm是怎么工作的,点击JdbcRealm对象我们会看到,它会有默认的sql    这里截取了部分JdbcRealm类的内容

public class JdbcRealm extends AuthorizingRealm {
  
    protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
 
    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
...

通常情况下我们数据库的表和JdbcRealm默认访问的表不一致,这个时候我们可以使用

jdbcRealm.setAuthenticationQuery("my sql"); 传入我们自己的sql语句

public class JdbcRealmTest
{
    DruidDataSource dataSource=new DruidDataSource();
    {
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test1");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
    }
    @Test
    public void TestAuth()
    {
        //jdbcrealm需要访问数据库
        JdbcRealm jdbcRealm=new JdbcRealm();
        jdbcRealm.setDataSource(dataSource);
        jdbcRealm.setPermissionsLookupEnabled(true);//注意默认情况下是不会去查询我们的权限数据的,这里需要设置为true
        jdbcRealm.setAuthenticationQuery("select * from user where name=? limit 1");//提供我们自己的sql
        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);
        //2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","1");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
        //    subject.checkPermission("user:dele2te");//Subject does not have permission [user:dele2te]
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

同样我们可以自定义我们的角色和权限的查询sql

jdbcRealm.setUserRolesQuery("select role_name from user_roles where username = ?");
        jdbcRealm.setPermissionsQuery("select permission from roles_permissions where role_name = ?");

自定义Realm:

仿照JdbcRealm对象,要实现自定义的Realm我们需要继承AuthorizingRealm

新建一个类CustomRealm

public class CustomRealm extends AuthorizingRealm {

    Map<String,String> userMap=new HashMap<>(16) ;
    {
        userMap.put("zhangsan","123456");
        super.setName("CustomRealm");//设置我们访问的realm名称(这里取什么名称无所谓)
    }
    //这个是用来做授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username=(String)principals.getPrimaryPrincipal();//从我们的认证信息中获得用户名
        System.out.println(username);
        //从数据库和缓存中获取角色数据
        Set<String> roles=GetRolesByUserName(username);
        //通过用户名来获得权限数据
        Set<String> premission=GetPremissionsByUserName(username);
        //将我们去到的角色数据和权限数据返回
        SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(premission);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }
    //这个是用来做认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.从主体传过来的认证信息中获得用户名
        String userName = (String) token.getPrincipal();
        //2.通过用户名去到数据库中获取凭证
        String password=GetPasswordByUserName(userName);
        if(password==null) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo("zhangsan","123456",getName());//最后一个参数是Realm
        return simpleAuthenticationInfo;
    }

    /**
     * 这里是模拟数据库访问
     * @param userName
     * @return
     */
    private String GetPasswordByUserName(String userName)
    {
        return userMap.get(userName);
    }

    /**
     * 获得角色数据
     * @param username
     * @return
     */
    private Set<String> GetRolesByUserName(String username) {
        Set<String> sets=new HashSet<>();
        sets.add("admin");
        sets.add("user");
        return sets;
    }

    /**
     * 模拟数据库
     * @param username
     * @return
     */
    private Set<String> GetPremissionsByUserName(String username)
    {
        Set<String> sets=new HashSet<>();
        sets.add("user:delete");
        sets.add("user:update");
        return sets;
    }
}

新建一个测试类CustomRealmTest

public class CustomRealmTest
{
    @Test
    public void TestAuth()
    {
        CustomRealm customRealm=new CustomRealm();
        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(customRealm);
        //2.主体提交认证请求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
        subject.checkPermission("user:delete");//Subject does not have permission [user:dele2te]
        subject.checkRole("admin");
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

Shiro加密

我们可以在自定义Realm基础上使用特定的加密算法(如MD5),以及加盐操作来保证我们密码的安全

我们依然用CustomRealm  与 CustomRealmTest做演示

public class CustomRealm extends AuthorizingRealm {

    Map<String,String> userMap=new HashMap<>(16) ;
    {
        userMap.put("zhangsan","e8b6ba86bafab35cfdf900a5402ba046");
        super.setName("CustomRealm");//设置我们访问的realm名称(这里取什么名称无所谓)
    }
    //这个是用来做授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username=(String)principals.getPrimaryPrincipal();//从我们的认证信息中获得用户名
        System.out.println(username);
        //从数据库和缓存中获取角色数据
        Set<String> roles=GetRolesByUserName(username);
        //通过用户名来获得权限数据
        Set<String> premission=GetPremissionsByUserName(username);
        //将我们去到的角色数据和权限数据返回
        SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(premission);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }
    //这个是用来做认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.从主体传过来的认证信息中获得用户名
        String userName = (String) token.getPrincipal();
        //2.通过用户名去到数据库中获取凭证
        String password=GetPasswordByUserName(userName);
        if(password==null) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo("zhangsan",password,getName());//最后一个参数是Realm
        //加盐
        simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("zhang"));
        return simpleAuthenticationInfo;
    }

    /**
     * 这里是模拟数据库访问
     * @param userName
     * @return
     */
    private String GetPasswordByUserName(String userName)
    {
        return userMap.get(userName);
    }

    /**
     * 获得角色数据
     * @param username
     * @return
     */
    private Set<String> GetRolesByUserName(String username) {
        Set<String> sets=new HashSet<>();
        sets.add("admin");
        sets.add("user");
        return sets;
    }

    /**
     * 模拟数据库
     * @param username
     * @return
     */
    private Set<String> GetPremissionsByUserName(String username)
    {
        Set<String> sets=new HashSet<>();
        sets.add("user:delete");
        sets.add("user:update");
        return sets;
    }

    public static void main(String[] args)
    {
        Md5Hash md5Hash=new Md5Hash("123456","zhang",2);
        System.out.println(md5Hash.toString());
    }
}
public class CustomRealmTest
{
    @Test
    public void TestAuth()
    {
        CustomRealm customRealm=new CustomRealm();
        //1.构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();
        defaultSecurityManager.setRealm(customRealm);
        //2.主体提交认证请求
        HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//传一个加密名称
        hashedCredentialsMatcher.setHashIterations(2);//设置加密次数,这边的值得和加密的值一致
        customRealm.setCredentialsMatcher(hashedCredentialsMatcher);

        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject =SecurityUtils.getSubject(); //注意这里的subject引入的包是shiro的
        UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken("zhangsan","123456");
        subject.login(usernamePasswordToken);
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
        subject.checkPermission("user:delete");//Subject does not have permission [user:dele2te]
       subject.checkRole("admin");
        subject.logout();
        System.out.println("isAuthenticated:"+subject.isAuthenticated());//shiro提供的是否认证的方法
    }
}

 

Shiro官方文档:http://shiro.apache.org/reference.html

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值