shiro整体架构
流程如下:
-
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的核心,它负责与后边介绍的其他组件进行交互
结构如下:
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 该类的结构图如下:
DefaultSecurityManager 通过继承,组合了Realm、Authenticator、Authrizer等组件。
DefaultSecurityManager提供了整个shiro框架的核心实现,核心功能主要是通过组合的Realm、Authenticator、Authrizer等组件实现的。
Subject
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”,所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者。
该接口定义了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 {
\\\
}
定义了如下的方法:
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。
具体的认证器、授权器、会话管理器等等这些组件,都有自己的默认实现、默认配置。
核心方法介绍
SecurityManager除了扩展了Authorizer、Authenticator、SessionManager,使用门面模式使其拥有其功能之外。还定义了其自己的方法。
分别为登录,登出,创建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支持注解及与Spring集成源码分析
shiro支持注解是通过如下方式实现的:
- 1、在Spring容器中注入DefaultAdvisorAutoProxyCreator。
- 2、定义一个advice,实现MethodInterceptor,并实现Invoke方法。在方法中实现对应的切面逻辑。
- 3、定义一个Advisor继承StaticMethodMatcherPointcutAdvisor,覆盖match方法实现对应的匹配逻辑,并将上面顶一个切面逻辑注入,此时形成一个完整的Advisor。
总结
1、shiro的设计模式SecurityManager主要使用了门面模式和模板模式。实际开发中可以多多借鉴这种方式。
2、shiro的各个模块非常灵活,可扩展性强,考虑是否可以加入SPI方式,取消配置文件的方式。