二、Shiro认证

目录:Shiro学习总结(目录贴)

2.1、什么是认证

二、Shiro认证_第1张图片
Shiro Authentication

  认证是一个验证用户是他们本人的过程。用户需要提供系统识别并且信任的身份证或者一些和身份证相同作用的证明。
  用户需要提交 principalscredentialsShiro从而使应用程序来验证身份。

  • PrincipalsSubject的身份属性,可以是任何东西如姓名,用户名等唯一的东西如电子邮箱/用户名。
  • Credentials:只有Subject知道的安全的值,如密码,数字凭证等。

2.2、认证流程

二、Shiro认证_第2张图片
认证流程图
  1. 程序调用Subject.login方法,通过AuthenticationToken实例提交用户的principalscredentials
  2. Subject的实例(DelegatingSubject或者子类)委托给Security Manager
    Security Manager调用securityManager.login(token)方法负责真正的身份验证。
  3. Security Manager委托给Authenticator进行身份验证,Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现。
  4. Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证;
  5. Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

2.3、 Authenticator

  综上所述,SecurityManager实现默认使用 ModularRealmAuthenticator实例,ModularRealmAuthenticator同时支持单个和多个Realm,如果配置了多个Realm,将于AuthenticationStrategy来协调工作。
  如果想SecurityManager用自定义Authenticator实现来配置,你可以这样做,shiro.ini例如:

authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator

在实践中,ModularRealmAuthenticator支持大多数的需要。

2.4、 AuthenticationStrategy

  为应用程序配置两个或更多Realm时,ModularRealmAuthenticator依赖于内部AuthenticationStrategy组件来确定认证成功或失败。AuthenticationStrategy是一个无状态的组件,在认证尝试期间被查询4次(这4个交互所需的任何必要状态将作为方法参数给出):

  • 在任何Realm被调用之前
  • 在一个单独的Realm getAuthenticationInfo方法被调用之前
  • 立即在一个单独的Realm getAuthenticationInfo方法被调用之后
  • 在所有Realm被调用之后
//在所有Realm验证之前调用  
AuthenticationInfo beforeAllAttempts(  
Collection 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;   

Shiro有3个具体AuthenticationStrategy实现:

AuthenticationStrategy 描述
AtLeastOneSuccessfulStrategy 如果一个(或多个)Realm认证成功,则整体尝试被认为是成功的。如果没有任何验证成功,则尝试失败。
FirstSuccessfulStrategy 仅使用从第一个成功验证的Realm返回的信息。所有进一步的领土将被忽略。如果没有任何验证成功,则尝试失败。
AllSuccessfulStrategy 所有配置的Realm都必须成功进行身份验证才能成功进行整体尝试。如果任何一个人未成功认证,则尝试失败。

  在ModularRealmAuthenticator默认的AtLeastOneSuccessfulStrategy实施,因为这是最常用的策略所需。但是,如果想要进行配置,可以配置不同的策略:

authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy

  自定义实现时一般继承org.apache.shiro.authc.pam.AbstractAuthenticationStrategy即可。

2.5、 Realm认证顺序

  指出ModularRealmAuthenticator与Realm实例交互的迭代顺序是非常重要的。
ModularRealmAuthenticator获取SecurityManager所配置的Realm实例。在尝试身份验证时,它将遍历该集合,并为每个Realm支持提交的对象AuthenticationToken调用RealmgetAuthenticationInfo方法。

1. 隐式排序
shiro.ini配置如下:

blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm

将会和

securityManager.realms = $blahRealm, $fooRealm, $barRealm

配置(可无)顺序一致。

2. 显式排序
自定义顺序

blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
securityManager.realms = $fooRealm, $barRealm, $blahRealm

2.6、代码示例

项目地址:GitHub

  1. 构建项目环境,引入shiro-corejunitjar包(maven项目构建请百度)。
  
      
            junit
            junit
            4.11
            test
        
        
        
            org.apache.shiro
            shiro-core
            1.4.0
        
  
  1. 编写测试类
package com.chenjy.shiro.authentication;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

public class AuthenticationTest {

    SimpleAccountRealm realm = new SimpleAccountRealm();

    @Before
    public void addUser() {
        realm.addAccount("Shiro", "1234");
    }

    @Test
    public void testAuthentication() {
        //1. 构建SecurityManager环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        // 1.1 设置realm
        defaultSecurityManager.setRealm(realm);
        // 1.2 设置SecurityManager
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //2. 主体提交认证
        Subject subject = SecurityUtils.getSubject();
        AuthenticationToken token = new UsernamePasswordToken("Shiro", "1234");
        subject.login(token);
        System.out.println("isAuthenticated:" + subject.isAuthenticated());
        subject.logout();
        System.out.println("isAuthenticated:" + subject.isAuthenticated());
    }
}

运行结果为:

isAuthenticated:true
isAuthenticated:false

将用户名更改为shiro,运行结果为:

org.apache.shiro.authc.UnknownAccountException: Realm [org.apache.shiro.realm.SimpleAccountRealm@61baa894] was unable to find account data for the submitted AuthenticationToken [org.apache.shiro.authc.UsernamePasswordToken - shiro, rememberMe=false].
……

抛出异常

org.apache.shiro.authc.UnknownAccountException

将密码改为123456,运行结果为:

org.apache.shiro.authc.IncorrectCredentialsException: Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - Shiro, rememberMe=false] did not match the expected credentials.
……

抛出异常

org.apache.shiro.authc.IncorrectCredentialsException

以下是对认证相关内容补充(2018-05-26)。
跟随代码回顾Shiro的认证过程:
通过SecurityUtils.getSubject()获取Subject实例subject,subject调用 subject.login(token)将AuthenticationToken实例提交认证。

SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken("Shiro", "123456");
subject.login(token);

查看subject.login(token)的实现

 public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);

        PrincipalCollection principals;
        ……
    }

可以看出subject.login(token)将认证委托给SecurityManager,由其实例调用login方法进行认证。在进一步查看SecurityManager认证的实现

  public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            ……
        }

        Subject loggedIn = createSubject(token, info, subject);

        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

  public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

不难看出SecurityManager又将认证委托给了Authenticator,而Authenticator在验证时调用了ModularRealmAuthenticator doAuthenticate(token)方法,而doAuthenticate(token)方法的实现便是通过遍历Realm获取用户的认证权限信息。

 public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {

        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        }

        log.trace("Authentication attempt received for token [{}]", token);

        AuthenticationInfo info;
        try {
            info = doAuthenticate(token);
            if (info == null) {
                ……
            }
        } catch (Throwable t) {
          ……
        }

        log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);

        notifySuccess(token, info);

        return info;
    }


 protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }


 protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
          ……
        }
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
           ……
        }
        return info;
    }

而Realm获取认证信息是通过AuthenticatingRealm调用getAuthenticationInfo方法,具体的实现便是与我们代码中通过SimpleAccountRealm设置的用户名和密码进行对比。

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            ……
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }

        ……
        return info;
    }

//SimpleAccountRealm
 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        SimpleAccount account = getUser(upToken.getUsername());

        if (account != null) {
          ……
        }

        return account;
    }

在就再一次验证了之前提到过的Shiro的认证流程

在此分析一下AuthenticationToken的结构,由下图可看出RememberMeAuthenticationTokenHostAuthenticationToken继承了AuthenticationTokenUsernamePasswordToken实现了RememberMeAuthenticationTokenHostAuthenticationTokenUsernamePasswordToken之中的方法也如下图所示

二、Shiro认证_第3张图片
AuthenticationToken体系结构

Shiro默认提供的Realm
二、Shiro认证_第4张图片
Shiro默认提供的Realm

以后一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)。其中主要默认实现如下:
org.apache.shiro.realm.text.IniRealm:[users]部分指定用户名/密码及其角色;[roles]部分指定角色即权限信息;
org.apache.shiro.realm.text.PropertiesRealmuser.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;
关于更多Realm的信息将在后面的章节详细讲解。在此就不再赘述。

参考文档:

  1. Apache Shiro Reference Documentation
  2. 跟我学Shiro

你可能感兴趣的:(二、Shiro认证)