简介

  • Java安全框架
  • Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
  • 三个核心组件:Subject、SecurityManager和Realms。
  • Subject:当前操作用户,代表了当前用户的安全操作。所有Subject都绑定到SEcurityManager,与Subject的所有交互都会委托给SecurityManager,可以把Subject认为是一个门面,SecurityManager才是实际的执行者。
  • SecurityManager:安全管理器,Shiro框架核心,管理所有用户的安全操作。即所有与安全有关的操作都会与SecurityManager交互;它管理着所有Subject,负责与其他组件进行交互,相当于SpringMvc的DispatcherServlet前端控制器。
  • Realm:Shiro从Realm(领域)获取安全数据(用户、角色、权限),就是说SecurityManager要验证用户身份,需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从REalm得到用户相应的权限进行验证;可以把Realm看成DataSource,即安全数据源。

    Shiro和Spring Security比较

  • Shiro比Spring更容易使用,实现和最重要的理解

  • Spring Security更加知名的唯一原因是因为品牌名称

  • “Spring”以简单而闻名,但讽刺的是很多人发现安装Spring Security很难

  • 然而,Spring Security却有更好的社区支持

  • Apache Shiro在Spring Security处理密码学方面有一个额外的模块

  • Spring-security 对spring 结合较好,如果项目用的springmvc ,使用起来很方便。但是如果项目中没有用到spring,那就不要考虑它了。

  • Shiro 功能强大、且 简单、灵活。是Apache下的项目,比较可靠,且不跟任何的框架或者容器绑定,可以独立运行

实战

  • 依赖

    <dependency>  
        <groupId>org.apache.shiro</groupId>  
        <artifactId>shiro-core</artifactId>  
        <version>1.2.2</version>  
    </dependency>
    
  • 包含SecurityManager等最基础的对象。
  • shiro.ini:准备用户库

    [users]
    zhang=123
    wang=123
    
  • 测试

    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.subject.Subject;
    import org.apache.shiro.util.Factory;
    import org.apache.shiro.mgt.SecurityManager;
    import org.junit.Assert;
    import org.junit.Test;
    
    /**
     * @Author haien
     * @Description 运用shiro测试登录登出
     * @Date 2019/2/15
     **/
    public class LoginLogoutTest {
        @Test
        public void testHelloWorld(){
            //1、获取SecurityManager工厂,并使用ini配置文件初始化SecurityManager
            Factory<org.apache.shiro.mgt.SecurityManager> factory=
                    new IniSecurityManagerFactory("classpath:config/shiro.ini");
            //2、得到SecurityManager实例,并绑定SecurityUtils
            SecurityManager securityManager=factory.getInstance();
            SecurityUtils.setSecurityManager(securityManager);
            //3、得到Subject,创建用户名、密码身份验证Token(即用户身份凭证)
            Subject subject=SecurityUtils.getSubject();
            UsernamePasswordToken token=new UsernamePasswordToken("zhang","123"); //可能不止用户名、密码,如登录时允许用户名、邮箱、手机选一登录
    
            try {
                //4、登录,即身份验证
                subject.login(token);
            }catch (AuthenticationException e){
                //5、身份验证失败
            }
    
            Assert.assertEquals(true,subject.isAuthenticated()); //断言用户是否已登录
    
            //6、退出
            subject.logout();
        }
    }
    
  • 其中,验证失败抛出AuthenticationException或其子类,如,DisabledAccountException(禁用的账号)、LockedAccountException(锁定的账号)、UnknownAccountException(错误的账号)、、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等。
  • 对于页面错误消息的显示,最好使用用户名/密码错误,而不是用户名错误、密码错误,防止一些恶意用户没法扫描账号库。

身份验证流程

  1. 首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecurityManager()设置;
  2. SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
  3. Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
  5. Authenticator会把相应的表单参数封装类AuthenticationToken传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了,如果返回一个身份信息封装类AuthenticationInfo则身份验证成功。

PS:此处可以配置多个Realm(上面的ini配置方式就是使用org.apache.shiro.realm.text.IniRealm),将按照相应的顺序及策略进行访问。

  • Realm:获取用户库

    public interface Realm {
        //返回一个唯一的Realm名字
        String getName(); 
    
        //判断此Realm是否支持此Token
        boolean supports(AuthenticationToken var1);
    
        //根据Token获取认证信息
        AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) 
            throws AuthenticationException;
    }
    
  • 单Realm配置:自定义Realm实现类

    public class MyRealm1 implements Realm {
    
        /**
         * @Author haien
         * @Description 获得唯一的Realm名字
         * @Date 2019/2/15
         * @Param []
         * @return java.lang.String
         **/
        @Override
        public String getName() {
            return "myrealm1";
        }
    
        /**
         * @Author haien
         * @Description 判断此Realm是否支持此Token
         * @Date 2019/2/15
         * @Param [authenticationToken]
         * @return boolean
         **/
        @Override
        public boolean supports(AuthenticationToken token) {
            //仅支持UsernamePasswordToken
            return token instanceof UsernamePasswordToken;
        }
    
        /**
         * @Author haien
         * @Description 根据Token获得Token信息
         * @Date 2019/2/15
         * @Param [authenticationToken表单参数封装类]
         * @return org.apache.shiro.authc.AuthenticationInfo身份信息封装类
         **/
        @Override
        public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken)
                throws AuthenticationException {
            //表单参数,来自login方法
            String username=(String)authenticationToken.getPrincipal();
            String password=new String((char[])authenticationToken.getCredentials());
            //String password=(String)authenticationToken.getCredentials();
            //若用户名错误
            if(!"zhang".equals(username)){ //zhang来自用户库
                throw new UnknownAccountException();
            }
            //若密码错误
            if(!"123".equals(password)){ //123来自用户库
                throw new IncorrectCredentialsException();
            }
            //身份验证成功,返回一个AuthenticationInfo实现
            return new SimpleAuthenticationInfo(username,password,getName());
        }
    }
    
  • 配置ini文件指定自定义Realm实现

    ;第三方用户库:指定自定义Realm(可以在这个Realm里面定义用户库位置)
    myRealm1=com.haien.shiroHelloWorld.realm.MyRealm1 //相当于调用public无参构造创建对象
    ;指定securityManage的realms实现(通过¥name来引入上面的Realm定义)
    securityManager.realms=$myRealm1 //相当于调用setter方法设置对象引用
    
  • 多Realm配置。此处我们使用显式指示顺序的方式指定Realm的顺序,若删去第4行,SecurityManager则会按照Realm声明的顺序进行验证(即无需设置realms属性,会自动发现);而显式指定后,没有被指定到的Realm将会被忽略。

    1 myRealm1=com.haien.shiroHelloWorld.realm.MyRealm1
    2 myRealm2=com.haien.shiroHelloWorld.realm.MyRealm2
    3 ;多Realm配置
    4 securityManager.realms=$myRealm1,$myRealm2
    

    Shiro默认提供的Realm

  • 一般继承AuthorizingRealm即可,本身有权限验证doGetAuthorizationInfo(),又继承了用户验证doGetAuthenticationInfo,也继承了缓存功能。其中主要默认实现如下:
    • org.apache.shiro.realm.text.IniRealm:[users]部分指定用户名/密码及其角色;[roles]部分指定角色即权限信息;
    • org.apache.shiro.realm.text.PropertiesRealm: user.username=password,role1,role2指定用户名/密码及其角色;role.role1=permission1,permission2指定角色及权限信息;
    • org.apache.shiro.realm.jdbc.JdbcRealm:通过sql查询相应的信息,如“select password from users where username = ?”获取用户密码,“select password, password_salt from users where username = ?”获取用户密码及盐;“select role_name from user_roles where username = ?”获取用户角色;“select permission from roles_permissions where role_name = ?”获取角色对应的权限信息;也可以调用相应的api进行自定义sql;
  • 特别地,值需要用户验证而不需要授权,则继承AuthenticatingRealm即可。
  • 而若实现Realm的话,一般是重写getAuthenticationInfo()实现用户验证,没有授权给功能。

JDBC Realm

  • 先到数据库shiro下建表:users,user_roles,roles_permissions,并添加一个用户:username-zhang,password-123.
  • ini配置

    ;jdbc用户库
    jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm //实例名=全限定类名,创建一个实例
    dataSource=com.alibaba.druid.pool.DruidDataSource
    dataSource.driverClassName=com.mysql.jdbc.Driver //实例名.属性=值,自动调用setter进行赋值
    dataSource.url=jdbc:mysql://127.0.0.1:3306/shiro
    dataSource.username=root
    dataSource.password=123456
    jdbcRealm.dataSource=$dataSource
    securityManager.realms=$jdbcRealm
    

代码实例