二、Shiro认证

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

2.1、什么是认证

Shiro Authentication

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

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

2.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<? 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;   

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项目构建请百度)。
  <dependencies>
      <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
  </dependencies>
  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<Realm> 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之中的方法也如下图所示

AuthenticationToken体系结构

Shiro默认提供的Realm
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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容