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);
}
}
二.源码分析
该部分源码将分四篇文章进行分析,本篇文章分析上述Demo代码的2、3部分。
2
// 实现Subject接口,该接口将方法调用委托给底层SecurityManager实例进行安全检查。它本质上是一个SecurityManager代理,使用了代理模式。
// 此实现不维护角色和权限等状态(仅维护Subject主体,如用户名或用户主键),以便在无状态体系结构中获得更好的性能。相反,它每次都要求底层SecurityManager执行授权检查。
// 使用此实现的一个常见误解是,每次调用方法时都会“命中”EIS资源(RDBMS等)。情况不一定是这样,这取决于底层SecurityManager实例的实现。如果需要缓存授权数据(以消除EIS往返,从而提高数据库性能),那么让底层SecurityManager实现或其委托组件来管理缓存,而不是这个类,会被认为更加优雅。SecurityManager被认为是一个业务层组件,其中缓存策略得到了更好的管理。
// 从大型应用程序、集群应用程序到简单应用程序和jvm本地应用程序都能从无状态架构中受益。这种实现在无状态编程范式中扮演着重要的角色,应该在任何可能的情况下使用。
public class DelegatingSubject implements Subject {
protected boolean authenticated;
...
// 该Subject/用户在当前会话中通过提供与系统已知的有效凭证相匹配的有效凭证证明其身份,认证成功则返回true,否则返回false。
// 注意,即使这个Subject的身份通过'remember me'服务被记住了,这个方法仍然会返回false,除非用户在当前会话中使用正确的凭据实际登录了。
public boolean isAuthenticated() {
return authenticated && hasPrincipals();
}
protected boolean hasPrincipals() {
return !isEmpty(getPrincipals()); // getPrincipals()->2.1
}
// 检查PrincipalCollection是否为空,PrincipalCollection是与相应Subject相关联的所有主体的集合。
private static boolean isEmpty(PrincipalCollection pc) {
return pc == null || pc.isEmpty();
}
...
}
2.1
public class DelegatingSubject implements Subject {
// 以PrincipalCollection的形式返回该Subject的主体(标识属性),如果该主体是匿名的,则返回null,因为它还没有任何关联的帐户数据(例如,还没有登录)。
// “principal”一词只是一个安全术语,用于标识与Subject(即应用程序用户)关联的属性。例如,用户id、姓氏(家庭/姓氏)、给定(名)名、社会安全号码、昵称、用户名等都是主体的示例。
public PrincipalCollection getPrincipals() {
// 获取所有principals
List<PrincipalCollection> runAsPrincipals = getRunAsPrincipalsStack();
// 取PrincipalCollection集合中的第一个返回
return CollectionUtils.isEmpty(runAsPrincipals) ? this.principals : runAsPrincipals.get(0);
}
private List<PrincipalCollection> getRunAsPrincipalsStack() {
Session session = getSession(false); // 2.1.1
if (session != null) {
return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY); // 2.1.2
}
return null;
}
}
2.1.1
public class DelegatingSubject implements Subject {
// 返回与Subject关联的应用程序会话。
// 如果已经存在与该Subject关联的会话,则返回该会话并忽略create参数。
// 如果没有会话存在,并且create参数为true,将创建一个新的会话,并与这个Subject关联,然后返回。
// 如果不存在会话,且create为false,则返回null。
public Session getSession(boolean create) {
if (log.isTraceEnabled()) {
log.trace("attempting to get session; create = " + create +
"; session is null = " + (this.session == null) +
"; session has id = " + (this.session != null && session.getId() != null));
}
// 如果session为null且create为true
if (this.session == null && create) {
//added in 1.2: 如果禁止创建新的会话,则抛出异常
if (!isSessionCreationEnabled()) {
String msg = "Session creation has been disabled for the current subject. This exception indicates " +
"that there is either a programming error (using a session when it should never be " +
"used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
"for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " +
"for more.";
throw new DisabledSessionException(msg);
}
log.trace("Starting session for host {}", getHost());
// 创建会话上下文
SessionContext sessionContext = createSessionContext();
// 根据指定的上下文初始化数据启动一个新的会话,底层实现可以使用该数据来确定如何创建内部session实例。
Session session = this.securityManager.start(sessionContext);
this.session = decorate(session);
}
return this.session;
}
}
2.1.2
// DelegatingSession是服务器端会话的客户端层表示。这个实现基本上是一个服务器端NativeSessionManager的代理,它将为每个方法调用返回正确的结果。
// DelegatingSession将在适当的时候缓存数据,只在必要时与服务器通信,以避免远程方法调用。
// 当然,如果在进程中与NativeSessionManager业务POJO一起使用,就像在基于web的应用程序中,web类和服务器端业务POJO存在于同一个JVM中那样,就不会产生远程方法调用。
public class DelegatingSession implements Session, Serializable {
// 返回由指定键标识的绑定到此会话的对象。如果没有绑定对象,则返回null。
public Object getAttribute(Object attributeKey) throws InvalidSessionException {
return sessionManager.getAttribute(this.key, attributeKey);
}
}
3
// 一个简单的用户名/密码身份验证令牌,支持最广泛使用的身份验证机制。
// 该类还实现了RememberMeAuthenticationToken接口,以支持跨用户会话的“RememberMe”服务,以及HostAuthenticationToken接口,以保留发生身份验证尝试的主机名或IP地址位置。
// “RememberMe”身份验证在默认情况下是禁用的,但是如果应用程序开发人员希望允许它进行登录尝试,那么只需要调用setRememberMe(true)。如果底层的SecurityManager实现也支持RememberMe服务,那么用户的身份将在会话之间被记住。
// 该类将密码存储为char[]而不是String。因为字符串是不可变的,它们的内部值不能被覆盖——这意味着即使是一个空的String实例可能在以后的时间在内存中被访问(例如内存转储)。这对于敏感信息(如密码)来说并不好。
// 为了避免以后访问内存的可能性,应用程序开发人员应该总是在使用令牌执行登录尝试之后调用clear()。
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
/*--------------------------------------------
| C O N S T A N T S |
============================================*/
/*--------------------------------------------
| I N S T A N C E V A R I A B L E S |
============================================*/
/**
* 用户名
*/
private String username;
/**
* 密码,以字符数组形式存储
*/
private char[] password;
/**
* 是否应该在相应的登录尝试中启用'rememberMe';默认为false
*/
private boolean rememberMe = false;
/**
* 尝试登录的位置,如果不知道或显式省略,则为空。
*/
private String host;
// 构造一个新的UsernamePasswordToken,封装提交的用户名和密码,以及用户是否希望在会话之间记住其身份。
// 这是一个方便的构造函数,并通过一个字符数组在内部维护密码,即password. tochararray();
// 需要注意的是,在代码中以String形式存储密码可能会产生安全影响。
public UsernamePasswordToken(final String username, final String password) {
this(username, password != null ? password.toCharArray() : null, false, null);
}
// 构造一个新的UsernamePasswordToken,封装提交的用户名和密码,以及用户是否希望在会话件记住其身份和尝试登录的ip地址或域名
public UsernamePasswordToken(final String username, final char[] password,
final boolean rememberMe, final String host) {
this.username = username;
this.password = password;
this.rememberMe = rememberMe;
this.host = host;
}
...
}
(完)