Shiro

作者: 拼搏男孩 | 来源:发表于2020-04-15 00:06 被阅读0次

    Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。

    image.png

    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 服务器);

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

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

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

    1、添加依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.qianfeng</groupId>
        <artifactId>413Shiro</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-core</artifactId>
                <version>1.4.0</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-api</artifactId>
                <version>1.7.25</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-simple</artifactId>
                <version>1.7.25</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.16</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.15</version>
            </dependency>
        </dependencies>
    </project>
    

    shiro的依赖非常多,由于我们只测试基本功能,所以这里只需要引入shiro-core这个依赖,与spring整合的时候还需要shiro-spring这个依赖,slf4j这两个依赖提供了日志相关的类,由于我还要连接数据库进行操作,所以引入了druid与mysql这两个依赖。

    2、身份验证

    TestShiro.java

    package com.qainfeng;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.config.IniSecurityManagerFactory;
    import org.apache.shiro.mgt.DefaultSecurityManager;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.realm.jdbc.JdbcRealm;
    import org.apache.shiro.realm.text.IniRealm;
    import org.apache.shiro.subject.Subject;
    import org.junit.Test;
    
    public class TestShiro {
        @Test
        public void shiroTest(){
            //创建一个默认的安全管理器
            DefaultSecurityManager manager = new DefaultSecurityManager();
            //读取配置文件创建一个配置域
            IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
            //设置域
            manager.setRealm(iniRealm);
            //将新创建的安全管理器设置为安全管理器
            SecurityUtils.setSecurityManager(manager);
            //获得subject(主体)
            Subject subject = SecurityUtils.getSubject();
            //根据前台发送过来的用户名与密码创建一个token
            UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
            try {
                //subject登录
                subject.login(token);
            } catch (UnknownAccountException e){
                //如果出现了这个异常
                System.out.println("未知账户");
                e.printStackTrace();
            } catch (IncorrectCredentialsException e){
                System.out.println("密码错误");
                e.printStackTrace();
            }
            catch (AuthenticationException e) {
                //这个异常类是上面两个异常类的父类,此外还有很多异常子类
                System.out.println("其他错误");
                e.printStackTrace();
            }
            subject.logout();
        }
        @Test
        public void shiroTest3(){
            DefaultSecurityManager manager = new DefaultSecurityManager();
            JdbcRealm jdbcRealm = new JdbcRealm();
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setUrl("jdbc:mysql://localhost:3306/market?useSSL=true&serverTimezone=UTC&characterEncoding=UTF-8");
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUsername("root");
            dataSource.setPassword("huwenlong");
            jdbcRealm.setDataSource(dataSource);
            jdbcRealm.setAuthenticationQuery("select password from users where user_name = ?");
            manager.setRealm(jdbcRealm);
            SecurityUtils.setSecurityManager(manager);
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken("汪淼","wangmiao123");
            subject.login(token);
            System.out.println("success");
        }
        @Test
        public void shiroTest4(){
            /*DefaultSecurityManager manager = new DefaultSecurityManager();
            IniRealm iniRealm = new IniRealm("classpath:shiro2.ini");
            manager.setRealm(iniRealm);*/
            IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro2.ini");
    
            // 通过工厂创建SecurityManager
            SecurityManager manager = factory.getInstance();
            SecurityUtils.setSecurityManager(manager);
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken("汪淼","wangmiao123");
            subject.login(token);
            System.out.println("success");
        }
    }
    

    有两种方式认证身份,一种方式是使用配置文件,直接在配置文件中写好用户名与密码,另外一种方式是连接数据库。

    shiro.ini

    [users]
    zhangsan=123456,admin,guest
    lisi=654321,guest
    [roles]
    admin=select,delete,update,insert
    guest=select,update,insert
    

    这个文件中配置了用户、角色、权限信息

    [main]
    dataSource=com.alibaba.druid.pool.DruidDataSource
    dataSource.driverClassName=com.mysql.cj.jdbc.Driver
    dataSource.url=jdbc:mysql://localhost:3306/market?useSSL=true&serverTimezone=UTC&characterEncoding=UTF-8
    dataSource.username=root
    dataSource.password=huwenlong
    
    jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
    jdbcRealm.dataSource=$dataSource
    jdbcRealm.authenticationQuery=select password from users where user_name = ?
    
    securityManager.realms=$jdbcRealm
    

    在第一种方式中,由于IniSecurityManagerFactory这个类已经被废弃了,所以我就使用DefaultSecurityManager来取代,不过这种做法不能使用ini文件来配置JdbcRealm,还是要采用传统的方式。连接数据库也有两种方式,第一种方式使用纯Java代码,第二种方式使用ini文件,两种方式的写法有很大的区别,这里需要注意。还有就是一个JdbcReal实例的默认认证查询是"select password from users where username=?",如果你的数据库表名与字段名与默认的不一样可以修改这条语句。

    3、授权

    package com.qainfeng;
    
    import com.alibaba.druid.pool.DruidDataSource;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.config.IniSecurityManagerFactory;
    import org.apache.shiro.mgt.DefaultSecurityManager;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.realm.jdbc.JdbcRealm;
    import org.apache.shiro.realm.text.IniRealm;
    import org.apache.shiro.subject.Subject;
    import org.junit.Test;
    
    public class TestShiro {
        @Test
        public void shiroTest2(){
            DefaultSecurityManager manager = new DefaultSecurityManager();
            IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
            manager.setRealm(iniRealm);
            SecurityUtils.setSecurityManager(manager);
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken("lisi","654321");
            subject.login(token);
            System.out.println(subject.hasRole("guest"));
            System.out.println(subject.hasRole("admin"));
            System.out.println(subject.isPermittedAll("select"));
            System.out.println(subject.isPermittedAll("select","delete","update","insert"));
        }
    }
    

    4、加密

    加密与解密主要使用Base64与MD5

    package com.qainfeng;
    
    import org.apache.shiro.codec.Base64;
    import org.apache.shiro.crypto.hash.Md5Hash;
    import org.junit.Test;
    
    public class TestEncryption {
        @Test
        public void encryptionBase64(){
            //Base64编码可逆,编码后的长度与原字符串的长度是正比,可用于对图片进行编码
            String str = "i am huwenlong,i love java";
            byte[] encode = Base64.encode(str.getBytes());
            System.out.println(new String(encode));
        }
        @Test
        public void encrytionMD5(){
            //MD5不可逆,长度固定32位
            String str = "i am huwenlong,i love java";
            String s = new Md5Hash(str, "hello", 3).toString();
            System.out.println(s);
        }
    }
    

    5、自定义Realm

    shiro的iniRealm与jdbcRealm中有一些默认的设置,如果我们想创建一个与默认设置不一样的项目就需要自定义Realm,自定义Realm需要继承AuthorizingRealm并重写两个方法,一个方法用于认证,一个方法用于授权

    MyRealm.java

    package com.qainfeng.shiro;
    
    import com.qainfeng.entity.Employee;
    import com.qainfeng.entity.Permission;
    import com.qainfeng.entity.Roles;
    import com.qainfeng.service.EmployeeService;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    import java.util.List;
    
    /**
     * @author huwen
     */
    public class MyRealm extends AuthorizingRealm {
        private final EmployeeService service = new EmployeeService();
    
        /**
         *
         * @param principalCollection principal的集合,可以理解为各种用户身份的集合,比如用户名、邮箱、手机号等
         * @return 返回的是授权信息,包括角色与权限
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //SimpleAuthorizationInfo是AuthorizationInfo的子类
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            String empName = getAvailablePrincipal(principalCollection).toString();
            List<Roles> roles = service.getAllRolesByEmpName(empName);
            for (Roles role : roles) {
                info.addRole(role.getRoleName());
            }
            List<Permission> permissions = service.getAllPermissionsByEmpName(empName);
            for (Permission permission : permissions) {
                info.addStringPermission(permission.getPermName());
            }
            return info;
        }
    
        /**
         * 这个方法用于认证
         * @param authenticationToken 用户名与密码
         * @return 认证信息
         * @throws AuthenticationException 可能引发的异常
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            AuthenticationInfo info = null;
            //将authenticationToken强转为usernamePasswordToken,向下转型能够成功因为我们知道用户输入的是用户名与密码
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            //从token中获取用户名与密码传给service,最后交给dao从数据库中查询
            String username = token.getUsername();
            //使用字节数组存储密码更为安全,因为字节数组是可变的,字符串是不可变的存在常量池中
            char[] password = token.getPassword();
            String pass = new String(password);
            Employee emp = service.getEmpByNameAndPassword(username, pass);
            //如果查询到的数据不为空,就构造一个SimpleAuthenticationInfo对象,将用户名与密码放在里里面
            if(emp!=null && emp.getEmpId()!=0){
                //getName()获取到的是当前Realm的标识,因为可以自定义多个Realm,不同的Realm需要区分
                info = new SimpleAuthenticationInfo(username,pass,getName());
            }
            return info;
        }
    }
    

    在这个类中我们创建了EmployeeService类型的对象,然后调用dao从数据库中获取数据。

    Employee.java

    package com.qainfeng.controller;
    
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.config.IniSecurityManagerFactory;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.subject.Subject;
    
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @WebServlet("/empServlet")
    public class EmployeeServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doPost(req, resp);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            req.setCharacterEncoding("UTF-8");
            String empName = req.getParameter("empName");
            String password = req.getParameter("password");
            IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro3.ini");
            SecurityManager manager = factory.getInstance();
            SecurityUtils.setSecurityManager(manager);
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(empName,password);
            try {
                subject.login(token);
            } catch (AuthenticationException e) {
                e.printStackTrace();
            }
            if (subject.isAuthenticated()){
                resp.sendRedirect("main.jsp");
            }else {
                resp.getOutputStream().write("fail".getBytes());
            }
        }
    }
    

    这个servlet的作用是接收前台提交的登录表单数据然后登录。

    shiro3.ini

    [main]
    myRealm=com.qainfeng.shiro.MyRealm
    securityManager.realm=$myRealm
    authc=org.apache.shiro.web.filter.authc.FormAuthenticationFilter
    authc.loginUrl=/index.html
    [urls]
    /index.html=anon
    /main.jsp=authc
    /manager.jsp=authc,roles[manager]
    /guest.jsp=authc,roles[guest]
    /select.jsp=perms[select]
    /delete.jsp=perms[delete]
    

    这个ini文件指定了自定义的Realm。

    web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <listener>
            <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
        </listener>
    
        <filter>
            <filter-name>ShiroFilter</filter-name>
            <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
        </filter>
    
        <filter-mapping>
            <filter-name>ShiroFilter</filter-name>
            <url-pattern>/*</url-pattern>
            <dispatcher>REQUEST</dispatcher>
            <dispatcher>FORWARD</dispatcher>
            <dispatcher>INCLUDE</dispatcher>
            <dispatcher>ERROR</dispatcher>
        </filter-mapping>
    </web-app>
    

    这个web.xml文件定义了一个监听器和过滤器,用于web项目的过滤。

    main.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
    <html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>这是主页</h1>
        <shiro:authenticated>登录成功</shiro:authenticated> <br>
        <shiro:hasRole name="manager">我是经理</shiro:hasRole> <br>
        <shiro:hasRole name="guest">我是顾客</shiro:hasRole> <br>
    
        <shiro:user>
            欢迎用户 点击 <a href="index.html">此处</a>登录
        </shiro:user> <br>
    
        <shiro:hasPermission name="select">我可以查询</shiro:hasPermission> <br>
        <shiro:hasPermission name="delete">我可以删除</shiro:hasPermission> <br>
    </body>
    </html>
    

    这个jsp页面使用的shiro标签,这是一个新的标签,专门用于shiro与jsp页面结合配置权限。

    相关文章

      网友评论

          本文标题:Shiro

          本文链接:https://www.haomeiwen.com/subject/nyowmhtx.html