shiro源码解析

Scroll Down

shiro整体架构

image.png

流程如下:

  • 1、首先调用Subject.login(token)进行登录,其会自动委托给 Security Manager,调用之前必须通过SecurityUtils.setSecurityManager()设置;

  • 2、SecurityManager负责真正的身份验证逻辑,它会委托给 Authenticator进行身份验证;

  • 3、Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;

  • 4、Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多Realm身份验证;

  • 5、Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

shiro核心组件

Ini 配置类

Ini作为shiro的配置类,主要的作用存取用户指定的配置数据,shiro可以读取指定路径下的*.ini后缀的文件并解析成对应的Ini类。
Ini中不仅定义了ini文件的关键信息,还定义了对应文件的解析逻辑。

核心代码如下:

  • 1、获取对应路径下的文件字节流
  • 2、解析文件字节流内容并将信息放入

public static final String DEFAULT_SECTION_NAME = ""; //empty string means the first unnamed section
public static final String DEFAULT_CHARSET_NAME = "UTF-8";

public static final String COMMENT_POUND = "#";
public static final String COMMENT_SEMICOLON = ";";
public static final String SECTION_PREFIX = "[";
public static final String SECTION_SUFFIX = "]";

protected static final char ESCAPE_TOKEN = '\\';

private final Map<String, Section> sections;

/**
 * Loads data from the specified resource path into this current {@code Ini} instance.  The
 * resource path may be any value interpretable by the
 * {@link ResourceUtils#getInputStreamForPath(String) ResourceUtils.getInputStreamForPath} method.
 *
 * @param resourcePath the resource location of the INI data to load into this instance.
 * @throws ConfigurationException if the path cannot be loaded
 */
public void loadFromPath(String resourcePath) throws ConfigurationException {
    InputStream is;
    try {
        is = ResourceUtils.getInputStreamForPath(resourcePath);
    } catch (IOException e) {
        throw new ConfigurationException(e);
    }
    load(is);
}

/**
 * Loads the INI-formatted text backed by the given Scanner.  This implementation will close the
 * scanner after it has finished loading.
 *
 * @param scanner the {@code Scanner} from which to read the INI-formatted text
 */
public void load(Scanner scanner) {

    String sectionName = DEFAULT_SECTION_NAME;
    StringBuilder sectionContent = new StringBuilder();

    while (scanner.hasNextLine()) {

        String rawLine = scanner.nextLine();
        String line = StringUtils.clean(rawLine);
        // 如果是注释文件则直接跳过
        if (line == null || line.startsWith(COMMENT_POUND) || line.startsWith(COMMENT_SEMICOLON)) {
            //skip empty lines and comments:
            continue;
        }
        // 获取段落信息
        String newSectionName = getSectionName(line);
        if (newSectionName != null) {
            //found a new section - convert the currently buffered one into a Section object
            addSection(sectionName, sectionContent);

            //reset the buffer for the new section:
            sectionContent = new StringBuilder();

            sectionName = newSectionName;

            if (log.isDebugEnabled()) {
                log.debug("Parsing " + SECTION_PREFIX + sectionName + SECTION_SUFFIX);
            }
        } else {
            //normal line - add it to the existing content buffer:
            sectionContent.append(rawLine).append("\n");
        }
    }

    //finish any remaining buffered content:
    addSection(sectionName, sectionContent);
}

SecurityManager安全管理器

SecurityManager:安全管理器,即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互

结构如下:

image.png

SecurityManager接口继承了Authenticator、Authrizer、SessionManager,在此基础上定义了属于自己的功能:

/**
 * A {@code SecurityManager} executes all security operations for <em>all</em> Subjects (aka users) across a
 * single application.
 * <p/>
 * The interface itself primarily exists as a convenience - it extends the {@link org.apache.shiro.authc.Authenticator},
 * {@link Authorizer}, and {@link SessionManager} interfaces, thereby consolidating
 * these behaviors into a single point of reference.  For most Shiro usages, this simplifies configuration and
 * tends to be a more convenient approach than referencing {@code Authenticator}, {@code Authorizer}, and
 * {@code SessionManager} instances separately;  instead one only needs to interact with a single
 * {@code SecurityManager} instance.
 * <p/>
 * In addition to the above three interfaces, this interface provides a number of methods supporting
 * {@link Subject} behavior. A {@link org.apache.shiro.subject.Subject Subject} executes
 * authentication, authorization, and session operations for a <em>single</em> user, and as such can only be
 * managed by {@code A SecurityManager} which is aware of all three functions.  The three parent interfaces on the
 * other hand do not 'know' about {@code Subject}s to ensure a clean separation of concerns.
 * <p/>
 * <b>Usage Note</b>: In actuality the large majority of application programmers won't interact with a SecurityManager
 * very often, if at all.  <em>Most</em> application programmers only care about security operations for the currently
 * executing user, usually attained by calling
 * {@link org.apache.shiro.SecurityUtils#getSubject() SecurityUtils.getSubject()}.
 * <p/>
 * Framework developers on the other hand might find working with an actual SecurityManager useful.
 *
 * @see org.apache.shiro.mgt.DefaultSecurityManager
 * @since 0.2
 */
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {

    /**
     * Logs in the specified Subject using the given {@code authenticationToken}, returning an updated Subject
     * instance reflecting the authenticated state if successful or throwing {@code AuthenticationException} if it is
     * not.
     * <p/>
     * Note that most application developers should probably not call this method directly unless they have a good
     * reason for doing so.  The preferred way to log in a Subject is to call
     * <code>subject.{@link org.apache.shiro.subject.Subject#login login(authenticationToken)}</code> (usually after
     * acquiring the Subject by calling {@link org.apache.shiro.SecurityUtils#getSubject() SecurityUtils.getSubject()}).
     * <p/>
     * Framework developers on the other hand might find calling this method directly useful in certain cases.
     *
     * @param subject             the subject against which the authentication attempt will occur
     * @param authenticationToken the token representing the Subject's principal(s) and credential(s)
     * @return the subject instance reflecting the authenticated state after a successful attempt
     * @throws AuthenticationException if the login attempt failed.
     * @since 1.0
     */
    Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;

    /**
     * Logs out the specified Subject from the system.
     * <p/>
     * Note that most application developers should not call this method unless they have a good reason for doing
     * so.  The preferred way to logout a Subject is to call
     * <code>{@link org.apache.shiro.subject.Subject#logout Subject.logout()}</code>, not the
     * {@code SecurityManager} directly.
     * <p/>
     * Framework developers on the other hand might find calling this method directly useful in certain cases.
     *
     * @param subject the subject to log out.
     * @since 1.0
     */
    void logout(Subject subject);

    /**
     * Creates a {@code Subject} instance reflecting the specified contextual data.
     * <p/>
     * The context can be anything needed by this {@code SecurityManager} to construct a {@code Subject} instance.
     * Most Shiro end-users will never call this method - it exists primarily for
     * framework development and to support any underlying custom {@link SubjectFactory SubjectFactory} implementations
     * that may be used by the {@code SecurityManager}.
     * <h4>Usage</h4>
     * After calling this method, the returned instance is <em>not</em> bound to the application for further use.
     * Callers are expected to know that {@code Subject} instances have local scope only and any
     * other further use beyond the calling method must be managed explicitly.
     *
     * @param context any data needed to direct how the Subject should be constructed.
     * @return the {@code Subject} instance reflecting the specified initialization data.
     * @see SubjectFactory#createSubject(SubjectContext)
     * @see Subject.Builder
     * @since 1.0
     */
    Subject createSubject(SubjectContext context);

}

默认的实现类是:DefaultSecurityManager 该类的结构图如下:

image.png

DefaultSecurityManager 通过继承,组合了Realm、Authenticator、Authrizer等组件。

DefaultSecurityManager提供了整个shiro框架的核心实现,核心功能主要是通过组合的Realm、Authenticator、Authrizer等组件实现的。

Subject

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”,所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。

image.png

该接口定义了shiro全部的提供的权限相关方法。是这个框架的门面。

具体操作逻辑会专门讨论。

Authenticator

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了

/**
 * An Authenticator is responsible for authenticating accounts in an application.  It
 * is one of the primary entry points into the Shiro API.
 * <p/>
 * Although not a requirement, there is usually a single 'master' Authenticator configured for
 * an application.  Enabling Pluggable Authentication Module (PAM) behavior
 * (Two Phase Commit, etc.) is usually achieved by the single {@code Authenticator} coordinating
 * and interacting with an application-configured set of {@link org.apache.shiro.realm.Realm Realm}s.
 * <p/>
 * Note that most Shiro users will not interact with an {@code Authenticator} instance directly.
 * Shiro's default architecture is based on an overall {@code SecurityManager} which typically
 * wraps an {@code Authenticator} instance.
 *
 * @see org.apache.shiro.mgt.SecurityManager
 * @see AbstractAuthenticator AbstractAuthenticator
 * @see org.apache.shiro.authc.pam.ModularRealmAuthenticator ModularRealmAuthenticator
 * @since 0.1
 */
public interface Authenticator {

    /**
     * Authenticates a user based on the submitted {@code AuthenticationToken}.
     * <p/>
     * If the authentication is successful, an {@link AuthenticationInfo} instance is returned that represents the
     * user's account data relevant to Shiro.  This returned object is generally used in turn to construct a
     * {@code Subject} representing a more complete security-specific 'view' of an account that also allows access to
     * a {@code Session}.
     *
     * @param authenticationToken any representation of a user's principals and credentials submitted during an
     *                            authentication attempt.
     * @return the AuthenticationInfo representing the authenticating user's account data.
     * @throws AuthenticationException if there is any problem during the authentication process.
     *                                 See the specific exceptions listed below to as examples of what could happen
     *                                 in order to accurately handle these problems and to notify the user in an
     *                                 appropriate manner why the authentication attempt failed.  Realize an
     *                                 implementation of this interface may or may not throw those listed or may
     *                                 throw other AuthenticationExceptions, but the list shows the most common ones.
     * @see ExpiredCredentialsException
     * @see IncorrectCredentialsException
     * @see ExcessiveAttemptsException
     * @see LockedAccountException
     * @see ConcurrentAccessException
     * @see UnknownAccountException
     */
    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException;
}

Authenticator接口只定义了authenticate方法用于主体认证。
该类作为一个可扩展的组件存在于SecurityManager中,最后被Subject门面作为一个对外提供的接口存在。
框架使用者可以通过实现Authenticator并扩展authenticate方法来实现定制化逻辑。

在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=com.***.realm.MyRealm1 
myRealm2=com.***.realm.MyRealm2 
myRealm3=com.***.realm.MyRealm3
securityManager.realms=$myRealm1,$myRealm3

Authorizer

Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能

接口定义如下:

/**
 * An <tt>Authorizer</tt> performs authorization (access control) operations for any given Subject
 * (aka 'application user').
 *
 * <p>Each method requires a subject principal to perform the action for the corresponding Subject/user.
 *
 * <p>This principal argument is usually an object representing a user database primary key or a String username or
 * something similar that uniquely identifies an application user.  The runtime value of the this principal
 * is application-specific and provided by the application's configured Realms.
 *
 * <p>Note that there are many *Permission methods in this interface overloaded to accept String arguments instead of
 * {@link Permission Permission} instances. They are a convenience allowing the caller to use a String representation of
 * a {@link Permission Permission} if desired.  Most implementations of this interface will simply convert these
 * String values to {@link Permission Permission} instances and then just call the corresponding type-safe method.
 * (Shiro's default implementations do String-to-Permission conversion for these methods using
 * {@link org.apache.shiro.authz.permission.PermissionResolver PermissionResolver}s.)
 *
 * <p>These overloaded *Permission methods <em>do</em> forego type-safety for the benefit of convenience and simplicity,
 * so you should choose which ones to use based on your preferences and needs.
 *
 * @since 0.1
 */
public interface Authorizer {
    \\\
}

定义了如下的方法:
image.png

Realm

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源
接口定义如下:

/**
 * A <tt>Realm</tt> is a security component that can access application-specific security entities
 * such as users, roles, and permissions to determine authentication and authorization operations.
 *
 * <p><tt>Realm</tt>s usually have a 1-to-1 correspondence with a datasource such as a relational database,
 * file system, or other similar resource.  As such, implementations of this interface use datasource-specific APIs to
 * determine authorization data (roles, permissions, etc), such as JDBC, File IO, Hibernate or JPA, or any other
 * Data Access API.  They are essentially security-specific
 * <a href="http://en.wikipedia.org/wiki/Data_Access_Object" target="_blank">DAO</a>s.
 *
 * <p>Because most of these datasources usually contain Subject (a.k.a. User) information such as usernames and
 * passwords, a Realm can act as a pluggable authentication module in a
 * <a href="http://en.wikipedia.org/wiki/Pluggable_Authentication_Modules">PAM</a> configuration.  This allows a Realm to
 * perform <i>both</i> authentication and authorization duties for a single datasource, which caters to the large
 * majority of applications.  If for some reason you don't want your Realm implementation to perform authentication
 * duties, you should override the {@link #supports(org.apache.shiro.authc.AuthenticationToken)} method to always
 * return <tt>false</tt>.
 *
 * <p>Because every application is different, security data such as users and roles can be
 * represented in any number of ways.  Shiro tries to maintain a non-intrusive development philosophy whenever
 * possible - it does not require you to implement or extend any <tt>User</tt>, <tt>Group</tt> or <tt>Role</tt>
 * interfaces or classes.
 *
 * <p>Instead, Shiro allows applications to implement this interface to access environment-specific datasources
 * and data model objects.  The implementation can then be plugged in to the application's Shiro configuration.
 * This modular technique abstracts away any environment/modeling details and allows Shiro to be deployed in
 * practically any application environment.
 *
 * <p>Most users will not implement the <tt>Realm</tt> interface directly, but will extend one of the subclasses,
 * {@link org.apache.shiro.realm.AuthenticatingRealm AuthenticatingRealm} or {@link org.apache.shiro.realm.AuthorizingRealm}, greatly reducing the effort requird
 * to implement a <tt>Realm</tt> from scratch.</p>
 *
 * @see org.apache.shiro.realm.CachingRealm CachingRealm
 * @see org.apache.shiro.realm.AuthenticatingRealm AuthenticatingRealm
 * @see org.apache.shiro.realm.AuthorizingRealm AuthorizingRealm
 * @see org.apache.shiro.authc.pam.ModularRealmAuthenticator ModularRealmAuthenticator
 * @since 0.1
 */
public interface Realm {

    /**
     * Returns the (application-unique) name assigned to this <code>Realm</code>. All realms configured for a single
     * application must have a unique name.
     *
     * @return the (application-unique) name assigned to this <code>Realm</code>.
     */
    String getName();

    /**
     * Returns <tt>true</tt> if this realm wishes to authenticate the Subject represented by the given
     * {@link org.apache.shiro.authc.AuthenticationToken AuthenticationToken} instance, <tt>false</tt> otherwise.
     *
     * <p>If this method returns <tt>false</tt>, it will not be called to authenticate the Subject represented by
     * the token - more specifically, a <tt>false</tt> return value means this Realm instance's
     * {@link #getAuthenticationInfo} method will not be invoked for that token.
     *
     * @param token the AuthenticationToken submitted for the authentication attempt
     * @return <tt>true</tt> if this realm can/will authenticate Subjects represented by specified token,
     *         <tt>false</tt> otherwise.
     */
    boolean supports(AuthenticationToken token);

    /**
     * Returns an account's authentication-specific information for the specified <tt>token</tt>,
     * or <tt>null</tt> if no account could be found based on the <tt>token</tt>.
     *
     * <p>This method effectively represents a login attempt for the corresponding user with the underlying EIS datasource.
     * Most implementations merely just need to lookup and return the account data only (as the method name implies)
     * and let Shiro do the rest, but implementations may of course perform eis specific login operations if so
     * desired.
     *
     * @param token the application-specific representation of an account principal and credentials.
     * @return the authentication information for the account associated with the specified <tt>token</tt>,
     *         or <tt>null</tt> if no account could be found.
     * @throws org.apache.shiro.authc.AuthenticationException
     *          if there is an error obtaining or constructing an AuthenticationInfo object based on the
     *          specified <tt>token</tt> or implementation-specific login behavior fails.
     */
    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

}

核心组件补充

  • SessionManager:Session管理器,如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所以呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据

  • SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

  • CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

  • Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

概述

SecurityManager作为shiro的一个核心组件,Subject调用的操作,基本都由SecurityManager去处理并作出相应的回应,SecurityManager管理着很多功能模块,SecurityManager是个接口,每一个功能模块都对应着一个自己的Manager,这样使得shiro框架的代码结构清晰有条理。shiro使用门面模式,SecurityManager继承了三个shiro的核心模块,分别是Authorizer、Authenticator、SessionManager。

具体的认证器、授权器、会话管理器等等这些组件,都有自己的默认实现、默认配置。

image.png

核心方法介绍

SecurityManager除了扩展了Authorizer、Authenticator、SessionManager,使用门面模式使其拥有其功能之外。还定义了其自己的方法。

image.png

分别为登录,登出,创建Subject。

login逻辑实现

我们先来看下login的方法定义:

/**
 * Logs in the specified Subject using the given {@code authenticationToken}, returning an updated Subject
 * instance reflecting the authenticated state if successful or throwing {@code AuthenticationException} if it is
 * not.
 * <p/>
 * Note that most application developers should probably not call this method directly unless they have a good
 * reason for doing so.  The preferred way to log in a Subject is to call
 * <code>subject.{@link org.apache.shiro.subject.Subject#login login(authenticationToken)}</code> (usually after
 * acquiring the Subject by calling {@link org.apache.shiro.SecurityUtils#getSubject() SecurityUtils.getSubject()}).
 * <p/>
 * Framework developers on the other hand might find calling this method directly useful in certain cases.
 *
 * @param subject             the subject against which the authentication attempt will occur
 * @param authenticationToken the token representing the Subject's principal(s) and credential(s)
 * @return the subject instance reflecting the authenticated state after a successful attempt
 * @throws AuthenticationException if the login attempt failed.
 * @since 1.0
 */
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;

login方法使用一个原始的subject入参及用户认证信息进行验证,如果成功了则返回一个被更新状态的subject。否则抛出异常AuthenticationException。

login方法作为一个门面方法存在,其内部最终是调用Authentication得authenticate方法实现:
SecurityManager的默认实现类是DefaultSecurityManager,代码实现如下所示:

/**
 * First authenticates the {@code AuthenticationToken} argument, and if successful, constructs a
 * {@code Subject} instance representing the authenticated account's identity.
 * <p/>
 * Once constructed, the {@code Subject} instance is then {@link #bind bound} to the application for
 * subsequent access before being returned to the caller.
 *
 * @param token the authenticationToken to process for the login attempt.
 * @return a Subject representing the authenticated user.
 * @throws AuthenticationException if there is a problem authenticating the specified {@code token}.
 */
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
        // 此处调用父类AuthenticatingSecurityManager的authenticate方法
        info = authenticate(token);
    } catch (AuthenticationException ae) {
        try {
            // 记录登录失败后的动作
            onFailedLogin(token, ae, subject);
        } catch (Exception e) {
            if (log.isInfoEnabled()) {
                log.info("onFailedLogin method threw an " +
                        "exception.  Logging and propagating original AuthenticationException.", e);
            }
        }
        throw ae; //propagate
    }

    // 登录成功后更新上下文信息
    Subject loggedIn = createSubject(token, info, subject);
    // 记录登录成功后的动作
    onSuccessfulLogin(token, info, loggedIn);
    // 返回更新后的subject供后续调用
    return loggedIn;
}

该方法的核心逻辑总结为一下几点:

  • 1、调用父类AuthenticatingSecurityManager的authenticate方法执行真正的认证逻辑。
  • 2、如果登录失败则记录登录失败后的动作。
  • 3、如果登录成功后更新上下文信息。
  • 4、记录登录成功后的动作。
  • 5、返回更新后的subject供后续调用。

大体逻辑很简单。我们来看AuthenticatingSecurityManager的authenticate方法认证逻辑。

/**
 * Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication.
 */
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    // 调用Authenticator的authenticate方法。如果不特殊指定那么默认的Authenticator的是ModularRealmAuthenticator
    return this.authenticator.authenticate(token);
}

所以真正的执行逻辑是在ModularRealmAuthenticator的authenticate方法中而ModularRealmAuthenticator继承自AbstractAuthenticator,ModularRealmAuthenticator单独定义了关于authenticate方法中不同场景的处理方式,该处使用了模板模式:
抽象类中的authenticate实现如下所示。

/**
 * Implementation of the {@link Authenticator} interface that functions in the following manner:
 * <ol>
 * <li>Calls template {@link #doAuthenticate doAuthenticate} method for subclass execution of the actual
 * authentication behavior.</li>
 * <li>If an {@code AuthenticationException} is thrown during {@code doAuthenticate},
 * {@link #notifyFailure(AuthenticationToken, AuthenticationException) notify} any registered
 * {@link AuthenticationListener AuthenticationListener}s of the exception and then propagate the exception
 * for the caller to handle.</li>
 * <li>If no exception is thrown (indicating a successful login),
 * {@link #notifySuccess(AuthenticationToken, AuthenticationInfo) notify} any registered
 * {@link AuthenticationListener AuthenticationListener}s of the successful attempt.</li>
 * <li>Return the {@code AuthenticationInfo}</li>
 * </ol>
 *
 * @param token the submitted token representing the subject's (user's) login principals and credentials.
 * @return the AuthenticationInfo referencing the authenticated user's account data.
 * @throws AuthenticationException if there is any problem during the authentication process - see the
 *                                 interface's JavaDoc for a more detailed explanation.
 */
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) {
            String msg = "No account information found for authentication token [" + token + "] by this " +
                    "Authenticator instance.  Please check that it is configured correctly.";
            throw new AuthenticationException(msg);
        }
    } catch (Throwable t) {
        AuthenticationException ae = null;
        if (t instanceof AuthenticationException) {
            ae = (AuthenticationException) t;
        }
        if (ae == null) {
            //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
            //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:
            String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
                    "error? (Typical or expected login exceptions should extend from AuthenticationException).";
            ae = new AuthenticationException(msg, t);
            if (log.isWarnEnabled())
                log.warn(msg, t);
        }
        try {
            // 通知失败
            notifyFailure(token, ae);
        } catch (Throwable t2) {
            if (log.isWarnEnabled()) {
                String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +
                        "Please check your AuthenticationListener implementation(s).  Logging sending exception " +
                        "and propagating original AuthenticationException instead...";
                log.warn(msg, t2);
            }
        }


        throw ae;
    }

    log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
    // 通知成功
    notifySuccess(token, info);

    return info;
}

/**
 * Template design pattern hook for subclasses to implement specific authentication behavior.
 * <p/>
 * Common behavior for most authentication attempts is encapsulated in the
 * {@link #authenticate} method and that method invokes this one for custom behavior.
 * <p/>
 * <b>N.B.</b> Subclasses <em>should</em> throw some kind of
 * {@code AuthenticationException} if there is a problem during
 * authentication instead of returning {@code null}.  A {@code null} return value indicates
 * a configuration or programming error, since {@code AuthenticationException}s should
 * indicate any expected problem (such as an unknown account or username, or invalid password, etc).
 *
 * @param token the authentication token encapsulating the user's login information.
 * @return an {@code AuthenticationInfo} object encapsulating the user's account information
 *         important to Shiro.
 * @throws AuthenticationException if there is a problem logging in the user.
 */
protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token)
        throws AuthenticationException;

这个模板方法在子类ModularRealmAuthenticator中进行了实现:

/**
 * Attempts to authenticate the given token by iterating over the internal collection of
 * {@link Realm}s.  For each realm, first the {@link Realm#supports(org.apache.shiro.authc.AuthenticationToken)}
 * method will be called to determine if the realm supports the {@code authenticationToken} method argument.
 * <p/>
 * If a realm does support
 * the token, its {@link Realm#getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}
 * method will be called.  If the realm returns a non-null account, the token will be
 * considered authenticated for that realm and the account data recorded.  If the realm returns {@code null},
 * the next realm will be consulted.  If no realms support the token or all supporting realms return null,
 * an {@link AuthenticationException} will be thrown to indicate that the user could not be authenticated.
 * <p/>
 * After all realms have been consulted, the information from each realm is aggregated into a single
 * {@link AuthenticationInfo} object and returned.
 *
 * @param authenticationToken the token containing the authentication principal and credentials for the
 *                            user being authenticated.
 * @return account information attributed to the authenticated user.
 * @throws IllegalStateException   if no realms have been configured at the time this method is invoked
 * @throws AuthenticationException if the user could not be authenticated or the user is denied authentication
 *                                 for the given principal and credentials.
 */
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);
    }
}

doSingleRealmAuthentication方法的实现如下所示:

/**
 * Performs the authentication attempt by interacting with the single configured realm, which is significantly
 * simpler than performing multi-realm logic.
 *
 * @param realm the realm to consult for AuthenticationInfo.
 * @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials.
 * @return the AuthenticationInfo associated with the user account corresponding to the specified {@code token}
 */
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    if (!realm.supports(token)) {
        String msg = "Realm [" + realm + "] does not support authentication token [" +
                token + "].  Please ensure that the appropriate Realm implementation is " +
                "configured correctly or that the realm accepts AuthenticationTokens of this type.";
        throw new UnsupportedTokenException(msg);
    }
    // 此处又委托了数据源AuthenticatingRealm的认证操作
    AuthenticationInfo info = realm.getAuthenticationInfo(token);
    if (info == null) {
        String msg = "Realm [" + realm + "] was unable to find account data for the " +
                "submitted AuthenticationToken [" + token + "].";
        throw new UnknownAccountException(msg);
    }
    return info;
}

AuthenticatingRealm是Realm的抽象实现,目的是进行认证操作。

/**
 * This implementation functions as follows:
 * <ol>
 * <li>It attempts to acquire any cached {@link AuthenticationInfo} corresponding to the specified
 * {@link AuthenticationToken} argument.  If a cached value is found, it will be used for credentials matching,
 * alleviating the need to perform any lookups with a data source.</li>
 * <li>If there is no cached {@link AuthenticationInfo} found, delegate to the
 * {@link #doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)} method to perform the actual
 * lookup.  If authentication caching is enabled and possible, any returned info object will be
 * {@link #cacheAuthenticationInfoIfPossible(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo) cached}
 * to be used in future authentication attempts.</li>
 * <li>If an AuthenticationInfo instance is not found in the cache or by lookup, {@code null} is returned to
 * indicate an account cannot be found.</li>
 * <li>If an AuthenticationInfo instance is found (either cached or via lookup), ensure the submitted
 * AuthenticationToken's credentials match the expected {@code AuthenticationInfo}'s credentials using the
 * {@link #getCredentialsMatcher() credentialsMatcher}.  This means that credentials are always verified
 * for an authentication attempt.</li>
 * </ol>
 *
 * @param token the submitted account principal and credentials.
 * @return the AuthenticationInfo corresponding to the given {@code token}, or {@code null} if no
 *         AuthenticationInfo could be found.
 * @throws AuthenticationException if authentication failed.
 */
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;
}

/**
 * Retrieves authentication data from an implementation-specific datasource (RDBMS, LDAP, etc) for the given
 * authentication token.
 * <p/>
 * For most datasources, this means just 'pulling' authentication data for an associated subject/user and nothing
 * more and letting Shiro do the rest.  But in some systems, this method could actually perform EIS specific
 * log-in logic in addition to just retrieving data - it is up to the Realm implementation.
 * <p/>
 * A {@code null} return value means that no account could be associated with the specified token.
 *
 * @param token the authentication token containing the user's principal and credentials.
 * @return an {@link AuthenticationInfo} object containing account data resulting from the
 *         authentication ONLY if the lookup is successful (i.e. account exists and is valid, etc.)
 * @throws AuthenticationException if there is an error acquiring data or performing
 *                                 realm-specific authentication logic for the specified <tt>token</tt>
 */
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

用户此时可以选择继承AuthorizingRealm的doGetAuthenticationInfo方法进行定制化的认证逻辑的实现。

实际执行流程如下所示:

shiro

image.png

shiro支持注解及与Spring集成源码分析

shiro支持注解是通过如下方式实现的:

  • 1、在Spring容器中注入DefaultAdvisorAutoProxyCreator。
  • 2、定义一个advice,实现MethodInterceptor,并实现Invoke方法。在方法中实现对应的切面逻辑。
  • 3、定义一个Advisor继承StaticMethodMatcherPointcutAdvisor,覆盖match方法实现对应的匹配逻辑,并将上面顶一个切面逻辑注入,此时形成一个完整的Advisor。

总结

1、shiro的设计模式SecurityManager主要使用了门面模式和模板模式。实际开发中可以多多借鉴这种方式。
2、shiro的各个模块非常灵活,可扩展性强,考虑是否可以加入SPI方式,取消配置文件的方式。