2021SC@SDUSC
一.Demo代码
Shiro认证即为我们平时的“登录”,这篇文章我们来探究一下Shieo登录的底层实现。
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.lang.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyQuickStart {
// 使用工厂模式创建日志工具,方便打印日志。
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 创建Shiro SecurityManager并配置realms, users, roles 和权限的最简单方式是通过INI文件。
// 我们向一个工厂中传入.ini文件,然后工厂会返回一个SecurityManager实例。
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:myshiro.ini");
SecurityManager securityManager = factory.getInstance();
// 在这个简单的demo中,将SecurityManager作为一个JVM单例进行访问。
// 大多数应用的代码不这么写,而是依赖他们的容器或是在web应用中的web.xml文件
SecurityUtils.setSecurityManager(securityManager);
// 获取当前正在操作的用户->1
Subject currentUser = SecurityUtils.getSubject();
// 登录当前用户->2
if (!currentUser.isAuthenticated()) {
// 3
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true); // 记住我->4
try {
currentUser.login(token); // 登录(进行认证)->5
} catch (UnknownAccountException uae) { // 用户不存在
// token.getPrincipal()->6
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) { // 密码错误
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) { // 用户被锁定
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... 在这里捕获更多异常,可以在你的应用程序中自定义一个
catch (AuthenticationException ae) {
// 意外情况? 错误?
}
}
// 报告谁登录成功了:
// 打印他们的认证凭据--principal(在这个例子中是用户名):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 退出登录->7
currentUser.logout();
System.exit(0);
}
}
二.源码分析
该部分源码将分四篇文章进行分析,本篇文章分析代码的4、5.1、5.2部分。
4
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
...
// 是否应该记住该登录尝试的用户信息,默认为false
private boolean rememberMe = false;
// 设置提交的用户是否希望他们的身份(主体——principals)在会话中被记住。除非重写,否则默认值为false,表示不跨会话记住用户信息。
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
...
}
5
// Subject接口的实现,该接口将方法调用委托给底层SecurityManager实例进行安全检查。它本质上是一个SecurityManager代理。
// 为了在无状态体系结构中获得更好的性能,此实现不维护角色和权限等状态(仅维护Subject主体,如用户名或用户主键)。相反,它每次都要求底层的SecurityManager执行授权检查。
// 在使用这个实现时,一个常见的误解是每次调用方法时都会“命中”EIS资源(RDBMS等)。实际情况并非如此,这取决于底层SecurityManager实例的实现。如果需要缓存授权数据(以消除EIS往返,从而提高数据库性能),那么让底层的SecurityManager实现或它的委托组件管理缓存(而不是这个类)会被认为更优雅。SecurityManager被认为是一个业务层组件,可以更好地管理缓存策略。
public class DelegatingSubject implements Subject {
// 为这个Subject/用户执行一次登录尝试。
// 如果不成功,则抛出AuthenticationException,其子类标识尝试失败的原因。
// 如果成功,与提交的主体/凭据相关联的帐户数据将与这个Subject相关联,该方法将悄悄返回。
// 在悄悄地返回时,这个Subject实例可以被认为是经过身份验证的,getPrincipal()将是非空的,isAuthenticated()将为真。
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal(); // 5.1
Subject subject = securityManager.login(this, token); // 5.2
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
// 我们必须这样做,以防有假设的身份-避免失去“真正的”principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals(); // 5.3
}
if (principals == null || principals.isEmpty()) { // 5.4
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost(); // 5.5
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false); // 5.6
if (session != null) {
this.session = decorate(session); // 5.7
} else {
this.session = null;
}
}
}
5.1
public class DelegatingSubject implements Subject {
private void clearRunAsIdentitiesInternal() {
//try/catch added for SHIRO-298
try {
clearRunAsIdentities();
} catch (SessionException se) {
log.debug("Encountered session exception trying to clear 'runAs' identities during logout. This can generally safely be ignored.", se);
}
}
private void clearRunAsIdentities() {
Session session = getSession(false);
if (session != null) {
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
}
5.2
// Shiro框架默认的基于一系列realm的SecurityManager接口的具体实现。
// 该实现通过超类实现将其身份验证、授权和会话操作分别委托给包装好的Authenticator、Authorizer和SessionManager实例。
// 为了大大减少和简化配置,这个实现(及其超类)将为所有所需的依赖项创建合适的默认值,除了所需的一个或多个Realms。因为Realm实现通常与应用程序的数据模型交互,所以它们几乎总是特定于应用程序的;用户需要指定至少一个自定义的Realm实现,它“知道”你的应用程序的数据/安全模型(通过setRealm或一个重载的构造函数)。这个类层次结构中的所有其他属性对于大多数企业应用程序都有合适的默认值。
// 请注意:这个类支持为RememberMe标识服务配置一个RememberMeManager,用于登录/注销,但是,在启动时不会为这个属性创建一个默认实例。
// 因为RememberMe服务本质上是特定于客户端层的,因此依赖于应用程序,如果想启用RememberMe服务,必须自己通过setRememberMeManager mutator指定一个实例。
public class DefaultSecurityManager extends SessionsSecurityManager {
// 使用给定的authenticationToken登录指定的Subject,如果成功,返回一个更新的Subject实例,反映已验证的状态;如果不成功,则抛出AuthenticationException。
// 请注意,大多数应用程序开发人员不应该直接调用此方法。登录Subject的首选方式是调用Subject.login(authenticationToken)(通常在通过调用SecurityUtils.getSubject()获得Subject之后调用)。
// 首先验证AuthenticationToken参数,如果成功,则构造一个Subject实例,表示经过验证的帐户的身份。
// 一旦构造好,Subject实例就会在返回给调用者之前绑定到应用程序以进行后续访问。
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
// AuthenticationInfo表示主体(也就是用户)存储的仅与身份验证/登录过程相关的帐户信息。
AuthenticationInfo info;
try {
// 基于提交的AuthenticationToken对用户进行认证。
info = authenticate(token); // 5.2.1
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject); // 5.2.2
} 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); // 5.2.3
onSuccessfulLogin(token, info, loggedIn); // 5.2.4
return loggedIn;
}
}
5.2.1
// Shiro支持SecurityManager类层次结构,将所有身份验证操作委托给包装好的Authenticator实例。也就是说,这个类实现了SecurityManager接口中的所有Authenticator方法,但实际上,这些方法只是传递到底层的“真正的”Authenticator实例的调用。
// 所有其他SecurityManager(授权、会话等)方法都留给子类来实现。
// 为了与这个层次结构中的其他类保持一致,以及Shiro尽可能最小化配置的愿望,在实例化时为所有依赖项创建合适的默认实例。
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
// 如果身份验证成功,则返回一个AuthenticationInfo实例,该实例表示与Shiro相关的用户帐户数据。这个返回的对象通常依次用于构造一个Subject,表示一个更完整的安全特定的帐户“视图”,该帐户也允许访问一个Session。
// 委托包装的验证器进行身份验证。
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
}
// 几乎所有的Authenticator实现的超类,用于执行围绕身份验证尝试的常见工作。
// 这个类将实际的身份验证尝试委托给子类,但支持成功、失败登录和注销的通知。通知被发送到一个或多个已注册的authenticationlistener,以便在这些条件发生时允许自定义处理逻辑。
// 在大多数情况下,子类需要做的唯一一件事(通过它的doAuthenticate实现)是对提交的AuthenticationToken执行实际的主体/凭证验证过程。
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
// Authenticator接口的实现,功能如下:
// 1.调用模板doAuthenticate方法来执行实际的身份验证行为的子类。
// 2.如果在doAuthenticate期间抛出AuthenticationException,则将该异常通知任何已注册的AuthenticationListeners,然后将该异常传递给调用者来处理
// 3.如果没有抛出异常(表示成功登录),则将成功的尝试传递给所有已注册的authenticationlistener
// 4.返回AuthenticationInfo
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); //5.2.1.1
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) {
// 抛出的异常不是预期的AuthenticationException。
// 它可能是更严重或更出乎意料的异常。因此,包装一个AuthenticationException,记录警告,并传播:
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); //5.2.1.2
} 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); // 5.2.1.2
return info;
}
}
5.2.1.1
public class ModularRealmAuthenticator extends AbstractAuthenticator {
// 通过迭代realm的内部集合来验证给定的令牌。对于每个realm,首先调用realm.supports(AuthenticationToken)方法,以确定该realm是否支持AuthenticationToken方法参数。
// 如果一个realm支持令牌,那么它的realm.getauthenticationinfo (AuthenticationToken)方法将被调用。如果域返回一个非空帐户,则该令牌将被视为该域和记录的帐户数据的身份验证。如果域返回null,则将咨询下一个域。如果没有realm支持该令牌,或所有支持的realm返回null,则会抛出一个AuthenticationException,表明用户无法通过身份验证。
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);
}
}
}
5.2.1.2
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware {
// 通知任何已注册的authenticationlistener指定令牌的身份验证失败,从而导致指定的ae异常。
// 这个实现仅仅迭代内部侦听器集合,并为每个侦听器调用onFailure。
protected void notifyFailure(AuthenticationToken token, AuthenticationException ae) {
for (AuthenticationListener listener : this.listeners) {
listener.onFailure(token, ae);
}
}
// 通知任何已注册的authenticationlistener,指定令牌的身份验证成功,从而产生指定的信息。
// 这个实现仅仅迭代内部侦听器集合,并为每个侦听器调用onSuccess。
protected void notifySuccess(AuthenticationToken token, AuthenticationInfo info) {
for (AuthenticationListener listener : this.listeners) {
listener.onSuccess(token, info);
}
}
}
5.2.2
public class DefaultSecurityManager extends SessionsSecurityManager {
protected void onFailedLogin(AuthenticationToken token, AuthenticationException ae, Subject subject) {
rememberMeFailedLogin(token, ae, subject);
}
protected void rememberMeFailedLogin(AuthenticationToken token, AuthenticationException ex, Subject subject) {
// 一个RememberMeManager负责记住一个主题的身份,用于跨Subject的会话与应用程序。
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
// 对失败的登录做出反应。
// 立即忘记先前记住的任何身份。这是一个额外的安全特性,可以防止在预期用户没有执行身份验证尝试时保留任何剩余的身份数据
rmm.onFailedLogin(subject, token, ex);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onFailedLogin for AuthenticationToken [" +
token + "].";
log.warn(msg, e);
}
}
}
}
}
5.2.3
public class DefaultSecurityManager extends SessionsSecurityManager {
// 为给定方法参数表示的用户创建一个Subject实例
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
// 首先创建一个上下文对象
SubjectContext context = createSubjectContext();
// 设置上下文中的各种参数
context.setAuthenticated(true);
context.setAuthenticationToken(token);
context.setAuthenticationInfo(info);
context.setSecurityManager(this);
if (existing != null) {
context.setSubject(existing);
}
// 根据上下文创建subject
return createSubject(context);
}
}
5.2.4
public class DefaultSecurityManager extends SessionsSecurityManager {
protected void onSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
rememberMeSuccessfulLogin(token, info, subject);
}
protected void rememberMeSuccessfulLogin(AuthenticationToken token, AuthenticationInfo info, Subject subject) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
// 对成功登录尝试的反应。
// 首先总是忘记以前存储的任何标识。如果令牌是一个RememberMe令牌,则关联的标识将被记住,以便以后在新用户会话期间检索。
rmm.onSuccessfulLogin(subject, token, info);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onSuccessfulLogin. RememberMe services will not be " +
"performed for account [" + info + "].";
log.warn(msg, e);
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("This " + getClass().getName() + " instance does not have a " +
"[" + RememberMeManager.class.getName() + "] instance configured. RememberMe services " +
"will not be performed for account [" + info + "].");
}
}
}
}
(完)