Ch2-Shiro 用户登录验证

96
AllenChyou
2017.05.19 10:16* 字数 2515

银行开户的身份验证

怎么验证一个人就是他本人呢?比如我去银行里开一个账户,办一张卡,那就需要我本人,带着自己的身份证去到银行柜台排队办理;这个过程中,柜员同志会拿着我的身份证,看着我的脸来辨别是不是我本人,如果和证件照的样子长得相差不是非常大的话,那么就可以通过了;
现实生活中,身份证上的名字就是我的唯一标识,当然真正不重复的应该是身份证号吧;证件照本身就是我的验证信息,我需要提供一张和身份证上的照片一样的脸,才能通过验证;所以要是有一个和我长得一样的人拿着我的身份证去银行,那么实际上他也就是我了,也可以开一个账户,拥有了创建账户的权限;

取钱的身份验证

我已经在银行开好了户,里面也有人民币的时候,我去银行取现金的话,需要什么证明呢?假设是ATM机的状况的话,我需要的就是一张之前办好的银行卡和六位数字的取款密码就可以了,至于是不是长得和我一样这没有关系,照样可以取现金;这两样验证信息说明,第一,唯一标示就是银行卡,有了银行卡就可以尝试操作账户,第二,六位密码就是验证信息,需要输入准确的密码口令才能对应操作成功.这两个条件符合,任何人都可以取走我银行账户里的钱

Shiro的身份验证

怎么在软件环境下,证明,对应的用户就是这个用户本人呢?让程序员去看用户的脸对应身份证很明显是不现实的,一般的做法类似银行取现,提供唯一的身份标识,加上这个标识对应的验证信息,就可以算是这个身份验证通过,具体来讲就是账户的用户名和密码;
principals : 主体用来标识自己的信息,比如用户名,ID,身份证号,手机号码等
credentials : 验证信息,证书,凭证,这是跟唯一标识相对应的验证信息
主体就是上一回说到的 Subject ,用户;
Realm就是验证主体的数据源,具体的验证规则放在这里

Demo环境准备

依赖

<dependencies>  
    <dependency>  
        <groupId>junit</groupId>  
        <artifactId>junit</artifactId>  
        <version>4.9</version>  
    </dependency>  
    <dependency>  
        <groupId>commons-logging</groupId>  
        <artifactId>commons-logging</artifactId>  
        <version>1.1.3</version>  
    </dependency>  
    <dependency>  
        <groupId>org.apache.shiro</groupId>  
        <artifactId>shiro-core</artifactId>  
        <version>1.2.2</version>  
    </dependency>  
</dependencies> 

登录 退出

我们需要一些身份验证的信息和标识-shiro.ini

[users]
zhang=123
wang=123

写一个测试来演示一下登录和退出的流程,以及 Shiro 在这个过程中的作用
LoginLogoutTest

@Test  
public void testHelloworld() {  
    //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager  
    Factory<org.apache.shiro.mgt.SecurityManager> factory =  
            new IniSecurityManagerFactory("classpath:shiro.ini");  
    //2、得到SecurityManager实例 并绑定给SecurityUtils  
    org.apache.shiro.mgt.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();  
} 

首先知道自己手上有什么,用户名和密码,那就可以构造一个Token出来,这个令牌的的意思就是验证信息的组合;
测试里面需要构造一个验证的环境,需要注入验证信息的数据来,也需要安全管理器这个核心对象来进行操作的准备;
我们从ini文件中加载一个管理器的工厂,几经辗转之后得出一个subject对象,使用这个主体来进行登录和退出操作;

除了构造环境之外,主要的操作就是手机用户的身份信息,然后subject对象来进行调用登录退出操作如果验证不通过,就会抛出对应的异常;

真实环境中,用户的验证信息是不可能存储在文件中,而且是明文密码的形式的,一般需要关系数据库建表,然后加密存储
Token这个令牌的意义,实际上是见着放行;这个用户名密码可以作为一种令牌,实际上还有很多种类的 token 存在;
北京的紫禁城威武雄壮, 禁卫森严,但是每天的王公大臣,太监宫女,买菜御厨,送水进贡的进进出出也是不计其数;这里电视剧情节中的太监腰牌实际上就是令牌的一类吧,不管是谁,拿着腰牌就可以随意进出皇宫,正常的进出时间的话;当然还有非常时间,比如夜里和重大事件发生,禁军收到指令,任何人不得进出,也就是身份验证的规则发生了改变,一般的token无法验证通过了,只有皇上的金腰牌,高级的token可以验证通过,或者是多少爵位以上的亲王大臣才能进出,这帮人就不需要腰牌,他们的脸就是Token,俗称刷脸;
无论什么Token,最后能不能通过验证,还是看规则的设定

身份认证的内部流程

流程

这个流程还是很清楚的,subject会把lugin的委托请求转发给管理器,来进行实际的验证,管理器把这个任务分流给了认证器,认证器就自己一个个规则这么验证过来,如果中间没有出错或者异常抛出,那么就算是验证通过了;
这个规则Realm的策略和具体情况,可以是环境配置的视乎默认的那个,就像之前的ini文件配置的规则,也可以插入从其他数据源拿来的实现,一大串;

Realm 域-安全数据,认证规则

Realm 就是认证信息的数据源,系统本身需要一个准绳和规则来确定什么是合法的,什么是需要拒绝的,这个Realm 就是这个规则的抽象层;至于这个Realm是从哪里来的,具体怎么存储,灵活替换的也无所谓啦
之前的Ini文件中配置就是一种数据源,具体的实现类就是org.apache.shiro.realm.text.IniRealm 本身实现了Realm的接口 org.apache.shiro.realm.Realm

String getName(); //返回一个唯一的Realm名字  
boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token  
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)  
 throws AuthenticationException;  //根据Token获取认证信息  

自定义单Realm实现

MyRealm1

public class MyRealm1 implements Realm {  
    @Override  
    public String getName() {  
        return "myrealm1";  
    }  
    @Override  
    public boolean supports(AuthenticationToken token) {  
        //仅支持UsernamePasswordToken类型的Token  
        return token instanceof UsernamePasswordToken;   
    }  
    @Override  
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
        String username = (String)token.getPrincipal();  //得到用户名  
        String password = new String((char[])token.getCredentials()); //得到密码  
        if(!"zhang".equals(username)) {  
            throw new UnknownAccountException(); //如果用户名错误  
        }  
        if(!"123".equals(password)) {  
            throw new IncorrectCredentialsException(); //如果密码错误  
        }  
        //如果身份认证验证成功,返回一个AuthenticationInfo实现;  
        return new SimpleAuthenticationInfo(username, password, getName());  
    }  
} 

配置文件ini也需要对应修改
通过美元符号引用定义

#声明一个realm  
myRealm1=ch2.realm.MyRealm1  
#指定securityManager的realms实现  
securityManager.realms=$myRealm1   

多个Realm 的配置

shiro-multi-realm.ini

#声明一个realm  
myRealm1=com.github.zhangkaitao.shiro.chapter2.realm.MyRealm1  
myRealm2=com.github.zhangkaitao.shiro.chapter2.realm.MyRealm2  
#指定securityManager的realms实现  
securityManager.realms=$myRealm1,$myRealm2 

多个realm作为一串的验证,会按配置的顺序执行,没有在列表中出现的realm,即使在上面定义了亦不会被执行

shiro 的Realm体系

继承图

Realm 是根本的接口,层层实现之后,次要根本的就是AuthorizingRealm, 自定义的话继承这个类就好了
从不同的数据源拿到的信息,都可以生成Realm,下面来到JDBC的数据源

JDBC Realm的使用

数据库和依赖更新,使用mysql数据库和driud连接池

<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>5.1.39</version>  
</dependency>  
<dependency>  
    <groupId>com.alibaba</groupId>  
    <artifactId>druid</artifactId>  
    <version>0.2.23</version>  
</dependency>

接下来就是sql建表,分别是用户表users-存储用户名密码,user_roles存储用户角色,roles_permissions存储角色权限;
shiro.sql

drop schema if exists shiro;
create schema shiro;
use shiro;

create table users (
  id bigint auto_increment,
  username varchar(100),
  password varchar(100),
  password_salt varchar(100),
  constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_users_username on users(username);

create table user_roles(
  id bigint auto_increment,
  username varchar(100),
  role_name varchar(100),
  constraint pk_user_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_user_roles on user_roles(username, role_name);

create table roles_permissions(
  id bigint auto_increment,
  role_name varchar(100),
  permission varchar(100),
  constraint pk_roles_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create unique index idx_roles_permissions on roles_permissions(role_name, permission);

insert into users(username,password)values('zhang','123');

类似的配置文件 shiro-jdbc-realm.ini

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm  
dataSource=com.alibaba.druid.pool.DruidDataSource  
dataSource.driverClassName=com.mysql.jdbc.Driver  
dataSource.url=jdbc:mysql://localhost:3306/shiro  
dataSource.username=root  
#dataSource.password=  
jdbcRealm.dataSource=$dataSource  
securityManager.realms=$jdbcRealm 
  • 变量名=全限定类名会自动创建对象实例
  • 变量名.属性=会自动调用相应的setter进行赋值
  • $变量名是上面定义的对象引用

Authenticator 和 Authentication Strategy

身份验证器和身份验证的策略是一种包含关系,验证器就是验证身份是不是通过的模块,那么策略就是指定一下我在验证的时候使用的方式是什么?是一个realm通过了就算过呢?还是所有的过了才算过,还是其中几个等等,这就是策略
Authenticator 的作用的是验证用户的身份, 也是 Shiro 里面身份验证的入口点; 如果验证成功的话,就会返回返回验证信息对象 AuthenticationInfo, 验证失败就会抛出异常对象

public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)  throws AuthenticationException;

默认內建的认证策略实现

  • FirstSuccessfulStrategy : 只要有一个realm认证成功就好,而且返回的是第一个认证成功的信息
  • AtLeastOneSuccessfulStrategy : 也是只要有一个认证OK就可以通过,但是返回的是所有的认证信息
  • AllSuccessfulStrategy : 所有的realm都认证成功才算成功, 而且返回所偶有的信息

默认的认证器中采用的策略是AtLeastOneSuccessfulStrategy;

新的测试中,需要自己定义三个realm,然后修改管理器和认证器的配置
shiro-authenticator-all-successful.ini

#指定securityManager的authenticator实现
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator

#指定securityManager.authenticator的authenticationStrategy
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy

myRealm1=ch2.realm.MyRealm1
myRealm2=ch2.realm.MyRealm2
myRealm3=ch2.realm.MyRealm3
securityManager.realms=$myRealm1,$myRealm3

优化修改一下代码中的登录逻辑,把管理器的重复配置优化抽象出来


    private void login(String configFile) {
        //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
        Factory<org.apache.shiro.mgt.SecurityManager> factory =
                new IniSecurityManagerFactory(configFile);

        //2、得到SecurityManager实例 并绑定给SecurityUtils
        org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        //3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");

        subject.login(token);
    }

然后就是进行测试

@Test
    public void testAllSuccessfulStrategyWithSuccess() {
        login("classpath:shiro-authenticator-all-success.ini");
        Subject subject = SecurityUtils.getSubject();

        //得到一个身份集合,其包含了Realm验证成功的身份信息
        PrincipalCollection principalCollection = subject.getPrincipals();
        Assert.assertEquals(2, principalCollection.asList().size());
    }

除了默认的三个认证策略之外,还可以自定义策略,来看下需要搞定的额API

//在所有Realm验证之前调用  
AuthenticationInfo beforeAllAttempts(  
Collection<? extends Realm> realms, AuthenticationToken token)   
throws AuthenticationException;  
//在每个Realm之前调用  
AuthenticationInfo beforeAttempt(  
Realm realm, AuthenticationToken token, AuthenticationInfo aggregate)   
throws AuthenticationException;  
//在每个Realm之后调用  
AuthenticationInfo afterAttempt(  
Realm realm, AuthenticationToken token,   
AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)  
throws AuthenticationException;  
//在所有Realm之后调用  
AuthenticationInfo afterAllAttempts(  
AuthenticationToken token, AuthenticationInfo aggregate)   
throws AuthenticationException;

从这个策略的API来看,关于策略的流程定义主要就是两个切入点和两个结果 :
第一个切入点是全部的realm开始验证之前,和之后这两个时间点,第二个切入点就是每一个realm开始验证之前和之后这两个时间点;需要返回的结果也是两个,一个是正常分支,成功验证返回验证信息,具体是返回一个还是多个那看具体的策略怎么认定,另一个异常分支抛出认证异常,退出认证流程;

其实这个策略很像Junit和TestNG中单元测试或者端测试的测试策略,当一个测试集合开始的时候,首先执行的是beforeClass标记的方法,然后每一个测试case的前后又有beforeMethod和afterMethod,最后是afterClass标记的方法执行;联系这里的策略来看,测试流程也就是定义一个测试策略,只是这里的策略退出机制是返回或者抛出异常结束,而测试流程是捕获异常继续执行的,并且使用assert的检查来代替正常返回

通过实现接口org.apache.shiro.authc.pam.AuthenticationStrategy 或者直接集成抽象类 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy 来实现自己的策略,比如至少两个之类的

Shiro
Web note ad 1