Shiro之实现认证

与其它java开源框架相似,将shiro的jar包加入项目就可使用shiro提供的功能了。shiro-core是核心包必须选用,还提供了与web整合的shiro-web、与spring整合的shiro-spring、与任务调度quartz整合的shiro-quartz等jar包。java

写在前面的话:本篇文章只是经过一个入门程序教你们学会使用Shiro实现认证功能(包括普通认证和通过散列算法对密码进行加密后的认证),没有将Shiro同Spring整合起来使用(后面的文章咱们会讲解Shiro整合web开发环境的项目中如何实现认证功能,见Shiro整合Web项目及整合后的开发),因此咱们采用的jar包不多:shiro-core.jar、junit测试jar包、commons-logging.jar。web

1.认证的概念

身份认证,即在应用中谁能证实他就是他本人。通常提供如他们的身份ID一些标识信息来代表他就是他本人,如提供身份证,用户名/密码来证实。
在shiro中,用户须要提供principals (身份)和credentials(证实)给shiro,从而应用能验证用户身份:算法

  • principals:身份,即主体的标识属性,能够是任何东西,如用户名、邮箱等,惟一便可。一个主体能够有多个principals,但只有一个Primary principals,通常是用户名/密码/手机号。
  • credentials:证实/凭证,即只有主体知道的安全值,如密码/数字证书等。

最多见的principals和credentials组合就是用户名/密码了。接下来先经过一个入门程序进行一个基本的身份认证。spring

另外两个相关的概念是以前提到的Subject及Realm,分别是主体及验证主体的数据源。数据库

2.Shiro认证流程

Shiro认证流程图以下:apache

文字描述认证流程:安全

  1. 首先调用Subject.login(token)进行登陆,其会自动委托给SecurityManager,调用以前必须经过SecurityUtils. setSecurityManager()设置。
  2. SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证。
  3. Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处能够自定义插入本身的实现。
  4. Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证。
  5. Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,若是没有返回/抛出异常表示身份验证失败了。此处能够配置多个Realm,将按照相应的顺序及策略进行访问。

经过Shiro认证流程图,咱们在用Shiro完成认证功能时就要彻底按照这个流程去写代码。导入上述所说的3个jar包后咱们就能够开始写Shiro认证的入门程序了。架构

3.Shiro认证入门程序

本入门程序是经过模仿用户的登陆与退出来完成认证。咱们将用户登陆时输入的信息与数据库中的信息进行比较,只有两者信息符合时就说明用户认证经过了。框架

3.1建立shiro-first.ini

在src包下建立名为shiro-first.ini文件,程序中咱们没有链接到数据库,因此采用后缀为ini的文件来模拟数据库中的数据。该文件中的数据以下:ide

在.ini文件中经过[users]指定了两个主体(Subject):用户名zhangsan和密码1111十一、用户名lisi和密码22222。这样在.ini文件中配置就至关于模拟了数据库中的两条用户信息。

3.2编写入门程序

咱们接下来就参照上述认证流程图来写认证的程序,以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AuthenticationTest
{
    //用户的登陆和退出
    @Test
    public void testLoginAndLogout()
    {
        //建立securityManager工厂
        Factory<SecurityManager> factory=new IniSecurityManagerFactory(
                "classpath:shiro-first.ini");

        //建立SecurityManager
        SecurityManager securityManager = factory.getInstance();

        //将securityManager设置到当前的运行环境中
        SecurityUtils.setSecurityManager(securityManager);

        //丛SecurityUtils里边的到一个subject
        Subject subject = SecurityUtils.getSubject();

        // 在认证提交前准备token(令牌)
        // 模拟用户输入的帐号和密码,未来是由用户输入进去从页面传送过来
        UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
                "111111");

        try {
            // 执行认证提交
            subject.login(token);
        } catch (AuthenticationException e) {
            // 认证失败
            e.printStackTrace();
        }

        // 是否定证经过
        boolean isAuthenticated = subject.isAuthenticated();

        System.out.println("是否定证经过:" + isAuthenticated);

        // 退出操做
        subject.logout();
    }
}

 

运行程序,输出是否定证经过:true。由于该用户输入的信息和数据库中的信息匹配,因此认证成功。代码讲解:

  1. 首先经过new IniSecurityManagerFactory并指定一个ini配置文件来建立一个SecurityManager工厂。
  2. 接着获取SecurityManager并绑定到SecurityUtils,这是一个全局设置,设置一次便可。
  3. 经过SecurityUtils获得Subject,其会自动绑定到当前线程;若是在web环境在请求结束时须要解除绑定;而后获取身份验证的Token(即用户输入的信息),如用户名/密码。
  4. 调用subject.login方法进行登陆,其会自动委托给SecurityManager.login方法进行登陆。
  5. 若是身份验证失败请捕获AuthenticationException或其子类异常,常见的如 :DisabledAccountException(禁用的账号)LockedAccountException(锁定的账号)UnknownAccountException(错误的账号)ExcessiveAttemptsException(登陆失败次数过多)IncorrectCredentialsException (错误的凭证)ExpiredCredentialsException(过时的凭证)等,具体请查看其继承关系;对于页面的错误消息展现,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描账号库。
  6. 最后能够调用subject.logout退出,其会自动委托给SecurityManager.logout方法退出。

从如上代码可总结出身份验证的步骤:

  1. 收集用户身份/凭证,即如用户名/密码。
  2. 调用Subject.login进行登陆,若是失败将获得相应的AuthenticationException异常,根据异常提示用户错误信息;不然登陆成功。
  3. 最后调用Subject.logout进行退出操做。

从上面的测试类中咱们发现的几个问题:一、用户名/密码硬编码在ini配置文件,之后须要改为如数据库存储,且密码须要加密存储。二、用户身份Token可能不只仅是用户名/密码,也可能还有其余的,如登陆时容许用户名/邮箱/手机号同时登陆。

上面咱们是直接访问数据库(.ini文件)的,而咱们真正用到Shiro时是经过Realm来访问数据库的,在这里也应该是经过Realm来访问.ini配置文件。因此下面咱们来说讲经过上面提到的Realm完成认证功能。

4.自定义Realm

实际开发中咱们经过realm从数据库中查询用户信息,因此realm的做用可想而知:根据token中的身份信息去查询数据库(入门程序咱们使用ini配置文件模拟数据库),若是查到用户则返回认证信息,若是查询不到就返回null。

在Shiro架构中,realm接口中的java代码以下:

1
2
3
4
5
6
String getName(); //返回一个惟一的Realm名字  

boolean supports(AuthenticationToken token); //判断此Realm是否支持此Token  

AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)  
throws AuthenticationException;  //根据Token获取认证信息

 

而每每咱们都是自定义Realm,因此咱们须要自定义一个CustomRealm.java文件并让它继承AuthorizingRealm抽象类,在sric包下建立一个realm包,在realm包中建立咱们的自定义Realm,java代码以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class CustomRealm extends AuthorizingRealm
{

    //设置realm的名称
    @Override
    public void setName(String name) {
        super.setName("customRealm");
    }

    //用于认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        //token是用户输入的
        //第一步:丛token中取出身份信息
        String userCode= (String) token.getPrincipal();

        //第二步:根据用户输入的userCode丛数据库查询

        //模拟从数据库查询到的密码
        String password="111111";


        //若是查不到返回null,

        //若是查询到,返回认证信息AuthenticationInfo

        SimpleAuthenticationInfo simpleAuthenticationInfo=new
                SimpleAuthenticationInfo(userCode,password,this.getName());


        return simpleAuthenticationInfo;
    }

    //用于受权,该功能在下篇文章中进行讲解
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

}

 

4.1配置自定义Realm

自定义Realm的代码实现后,咱们就须要将这个自定义的Realm进行配置,在src包下建立一个shiro-reaml.ini文件(你也能够在先前咱们建立的shiro-first.ini文件下进行修改),而该配置文件跟咱们入门程序中的配置文件是不同的,入门程序中的配置文件是模拟的数据库,而这里的配置文件是将Realm进行配置,数据库中的信息咱们在自定义Reaml的代码中已进行模拟,内容以下:

内容中的解释也在注释中给出,内容中的属性值customRealm任意取,不必定要跟Reaml实现java代码中方法setName()的值同样,固然如果你自定义了多个Realm,那么配置文件中的内容以下:

1
2
3
4
5
#声明多个realm  
customRealm1=realm.CustomRealme2  
customRealme=realm.CustomRealme2 
#指定securityManager的realms实现  
securityManager.realms=$myRealm1,$myRealm2

 

完成自定义Realm的代码实现并配置后,咱们即可进行该Reaml的测试了,测试代码以下,仍然模拟的用户登陆认证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//用户的登陆和退出
  @Test
  public void testCustomRealm()
  {
      //建立securityManager工厂
      Factory<SecurityManager> factory=new IniSecurityManagerFactory(
              "classpath:shiro-realm.ini");

      //建立SecurityManager
      SecurityManager securityManager = factory.getInstance();

      //将securityManager设置到当前的运行环境汇中
      SecurityUtils.setSecurityManager(securityManager);

      //丛SecurityUtils里边建立一个
      Subject subject = SecurityUtils.getSubject();

      // 在认证提交前准备token(令牌)
      // 这里的帐号和密码 未来是由用户输入进去
      UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
              "111111");

      try {
          // 执行认证提交
          subject.login(token);
      } catch (AuthenticationException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
      }

      // 是否定证经过
      boolean isAuthenticated = subject.isAuthenticated();

      System.out.println("是否定证经过:" + isAuthenticated);

  }

 

运行程序,控制台输出认证经过,说明用户输入的信息与数据库(配置文件)中的信息符合,因此用户认证成功。这样咱们便经过Shiro完成了认证功能的实现。

扩展:看到这里相信你就会使用Shiro完成认证功能了,可是这里咱们是经过用户输入的密码直接去查询数据库中的明文密码,而每每为了保护用户信息因此数据库中的密码都是通过了MD5的算法进行加密后才放入数据库的,因此实际开发中咱们须要经过Shiro获取数据库中通过md5加密后的密码来和用户输入的密码进行比对。因此接下来我要讲Shiro中是如何将用户输入的信息与数据库中的加密信息进行对比从而实现的认证。

5.散列算法

实际开发中为了保护用户信息的安全,咱们须要对用户在注册时输入的密码进行加密后再保存到数据库,当用户登陆时咱们也要将用户输入的密码进行加密后再与数据库中的密码进行比对。即须要对密码进行散列,经常使用的散列方法有md五、sha。

用md5算法对密码进行散列的问题:若是知道散列后的值能够经过穷举算法获得md5密码对应的明文。解决方法:建议对md5进行散列时加salt(盐),进行加密至关于对原始密码+盐进行散列。

5.1自定义realm支持散列算法

那么接下来咱们就讲解上述reaml支持散列算法的测试,在realm包下新建CustomRealmMd5.java类,并再里面模仿数据库中的用户名和加密密文,内容以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class CustomRealmMd5 extends AuthorizingRealm
{

    // 设置realm的名称
    @Override
    public void setName(String name) {
        super.setName("customRealmMd5");
    }

    // 用于认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken token) throws AuthenticationException {

        // token是用户输入的
        // 第一步从token中取出身份信息
        String userCode = (String) token.getPrincipal();

        // 第二步:根据用户输入的userCode从数据库查询
        // ....

        // 若是查询不到返回null
        // 数据库中用户帐号是zhangsansan
		/*
		 * if(!userCode.equals("zhangsansan")){// return null; }
		 */

        // 模拟根据用户名从数据库查询到的密码,散列值
        String password = "f3694f162729b7d0254c6e40260bf15c";
        // 从数据库获取salt
        String salt = "qwerty";
        //上边散列值和盐对应的明文:111111

        // 若是查询到返回认证信息AuthenticationInfo
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                userCode, password, ByteSource.Util.bytes(salt), this.getName());

        return simpleAuthenticationInfo;
    }

    // 用于受权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(
            PrincipalCollection principals) {
        // TODO Auto-generated method stub
        return null;
    }
}

 

5.2在realm中配置凭证匹配器

而后对该自定义支持md5算法的Realm进行配置,在src包下建立shiro-realm-md5.ini文件,内容以下:

1
2
3
4
5
6
7
8
9
10
11
12
[main]
#自定凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#散列的算法
credentialsMatcher.hashAlgorithmName=md5
#散列的次数
credentialsMatcher.hashIterations=1

#将凭证匹配器设置到咱们定义的realm
customRealm=realm.CustomRealmMd5
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm

 

5.3测试

而后即可进行咱们支持md5算法的realm测试了,测试代码以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MD5Test {

		//注解用的main方法进行测试,你也能够经过junit.jar进行测试
    public static void main(String[] args)
    {
        //模拟用户输入的密码
        String source="111111";

        //加入咱们的盐salt
        String salt="qwerty";

        //密码11111通过散列1次获得的密码:f3694f162729b7d0254c6e40260bf15c
        int hashIterations=1;


        //构造方法中:
        //第一个参数:明文,原始密码
        //第二个参数:盐,经过使用随机数
        //第三个参数:散列的次数,好比散列两次,至关 于md5(md5(''))
        Md5Hash md5Hash=new Md5Hash(source,salt,hashIterations);

        String password_md5=md5Hash.toString();

        System.out.println(password_md5);

    }
}
相关文章
相关标签/搜索