【Shiro学习历程03】身份认证,跟踪源码解读认证流程以及自定义realm

上篇讲述了Shiro的组成部分,以及Subject交互等的概念。那么开始身份认证学习~


一、结合代码分析

Controller

public String login(String username, String password, Model model){
    String exMessage = "";
    if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
        // 获取subject
        Subject subject = SecurityUtils.getSubject();
        // 创建表单获取用户名密码
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
        try {
            // 登陆
            subject.login(usernamePasswordToken);
            return "redirect:index.do";
        } catch (UnknownAccountException uae) {
            // 帐号不存在
            exMessage = "帐号不存在";
            logger.debug("UnknownAccountException---->user:{},password:{}",username,password);
        }catch (IncorrectCredentialsException ie) {
            exMessage = "用户名或密码错误,错误信息:" + ie.getMessage();
            logger.debug("IncorrectCredentialsException---->user:{},password:{}",username,password);
        } catch (LockedAccountException le) {
            exMessage = "该账号已锁定,错误信息:" + le.getMessage();
            logger.debug("LockedAccountException---->user:{},password:{}",username,password);
        } catch (DisabledAccountException de) {
            exMessage = "该账号已禁用,错误信息:" + de.getMessage();
            logger.debug("DisabledAccountException---->user:{},password:{}",username,password);
        } catch (ExcessiveAttemptsException eae) {
            exMessage = "该账号登录失败次数过多,错误信息:" + eae.getMessage();
            logger.debug("ExcessiveAttemptsException---->user:{},password:{}",username,password);
        } catch (Exception e){
            exMessage = "未知错误,错误信息:" + e.getMessage();
            logger.debug("Exception---->user:{},password:{}",username,password);
        }

    }
    model.addAttribute("exMessage", exMessage);
    return "/login";
}
1、首先整理身份验证步骤
  • 获取表单username,password
  • 获取shiro的Subject对象 SecurityUtils.getSubject()
  • 创建身份验证的Token,调用subject.login()方法
  • 最后根据返回信息来判断身份验证结果,通常根据返回的异常来判断身份验证状态
  • 如果身份验证失败请捕获AuthenticationException或其子类

捕获AuthenticationException或其子类,常见的如:DisabledAccountException(禁用的帐号)、LockedAccountException(锁定的帐号)、UnknownAccountException(错误的帐号)、ExcessiveAttemptsException(登录失败次数过多)、IncorrectCredentialsException (错误的凭证)、ExpiredCredentialsException(过期的凭证)等,具体请查看其继承关系;对于页面的错误消息展示,最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库

  • 最后可以调用subject.logout退出,其会自动委托给SecurityManager.logout方法退出
2、总结身份验证细节流程
  • 首先调用Subject.login(token) 进行登录,其会自动委托给 Security Manager
  • SecurityManager负责身份验证逻辑,它会委托给Authenticator认证器进行身份验证,可以自定义实现
  • Authenticator可能会委托给相应的AuthenticationStrategy,进行ModularRealmAuthenticator中Realm身份验证,可以选择 单realm多realm认证
  • Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问
3、分析源码
  • Subject.login(token) 进入接口实现类,发现真正调用的是Security Manager

    public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);
        //省略其他
    }
    
  • 然后继续跟进DefaultSecurityManager

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
           //省略其他
        }
        return loggedIn;
    }
    
  • 继续跟进就会发现进入到 Authenticator接口进行验证,点击进入实现到 authenticate() 方法

    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        if (token == null) {
            throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
        }
            info = doAuthenticate(token);
     	// 省略其他
        return info;
    }
    
  • 再跟进就会进入到 ModularRealmAuthenticator,从源码中会发现,首先获取的是 Realms集合,然后会根据集合大小决定选择 多Realm(按照相应的顺序及策略进行访问)还是单Realm进行验证。这里我们选择单realm验证doSingleRealmAuthentication()

    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);
        }
    }
    
  • 继续跟进其实现类,发现进入到realm验证器进行验证,跟踪进入

    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
       //省略其他
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        //省略其他
        return info;
    }
    
  • 这是我们最终目的,到达验证方法,再继续跟踪时,会发现各种Realm子类对其实现也是我们进行自定义realm进行实现的地方如果用于用户身份校验,我们只要继承这个类就可以,可以看到继承树,这个类也集成了CachingRealm,可以使用缓存。

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            //otherwise not cached, perform the lookup:
            info = doGetAuthenticationInfo(token);
            log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
            if (token != null && info != null) {
                cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
            log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
        }
    
        if (info != null) {
            assertCredentialsMatch(token, info);
        } else {
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }
    
        return info;
    }
    

    继承树

  • 然后自定义UserRealm来重写doGetAuthenticationInfo(AuthenticationToken)来实现认证


以上就是身份验证流程、源码跟踪,以及自己实现身份验证的方法的原理


2019.12.08 更新
突然发现少说了**认证策略(AuthenticationStrategy)**这个知识点
AuthenticationStrategy:认证策略,目前shiro只给出三种策略方式(默认是第二种):

  1. FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略
  2. AtLeatOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份校验成功的认证信息
  3. AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份认证成功的认证信息,如果有一个失败就失败了

继续从源码出发,源码第四步,我们可以看到单Realm时,我跟进到doSingleRealmAuthentication()方法里,现在我们查看多Realm时,我们进入到 doMultiRealmAuthentication() 方法

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
        AuthenticationStrategy strategy = getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        if (log.isTraceEnabled()) {
            log.trace("Iterating through {} realms for PAM authentication", realms.size());
        }
        for (Realm realm : realms) {
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if (realm.supports(token)) {
                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable throwable) {
                    t = throwable;
                    if (log.isWarnEnabled()) {
                        String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
                        log.warn(msg, t);
                    }
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }

首先我们会看到获取认证策略:

AuthenticationStrategy strategy = getAuthenticationStrategy()
然后会遍历多个Realm,调用getAuthenticationInfo()去认证
AuthenticationStrategy 我觉得比较牛批的一点是,不但能根据想要的策略去认证,而且可以将没一个成功的结果会统一处理后再返回。

引自官方文档
Also an AuthenticationStrategy is responsible for aggregating the results from each successful Realm and ‘bundling’ them into a single AuthenticationInfo representation. This final aggregate AuthenticatinoInfo instance is what is returned by the Authenticator instance and is what Shiro uses to represent the Subject’s final identity (aka Principals).

好了,多Realm配置相关的配置信息

	<!-- 多Realm时需要配置,同时指定认证策略。-->
    <bean id="modularRealmAuthenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="authenticationStrategy">
            <!-- 认证策略 -->
            <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
        </property>
        <property name="realms">
            <list>
                <ref bean="userRealm"/>
                <ref bean="secordRealm"/>
            </list>
        </property>
    </bean>
	<!-- 同时securityManager中的单Realm引用的配置更换即可,不做代码测试了。 -->
	<!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="authenticator" ref="modularRealmAuthenticator"/>
        <!-- 设置securityManager安全管理器的rememberMeManger -->
        <property name="rememberMeManager" ref="rememberMeManager"/>
    </bean>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值