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);
}
}
二.源码分析
该部分源码将分四篇文章进行分析,本篇文章分析代码的6、7部分。
6
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
private String username;
...
// 返回在身份验证过程中提交的帐户标识。
// 大多数应用程序身份验证都是基于用户名/密码的,并且让这个对象代表一个用户名。
// 最终,返回的对象是特定于应用程序的,可以表示任何帐户标识
public Object getPrincipal() {
// 仅返回getUsername()
return getUsername();
}
// 返回身份验证过程中提交的用户名。
public String getUsername() {
return username;
}
...
}
7
public class DelegatingSubject implements Subject {
...
// 注销此Subject并删除任何与之关联的实体或使其无效。
// 例如Session和授权数据。调用此方法后,Subject被认为是“匿名的”,如果需要,可以继续用于另一次登录。
// Web环境的警告:
// 在web环境中调用此方法通常会移除任何关联的会话cookie,作为会话失效的一部分。
// 因为cookie是HTTP报头的一部分,而报头只能在发送响应体(html, image等)之前设置,所以在web环境中,必须在呈现任何内容之前调用此方法。
// 大多数应用程序在此场景中使用的典型方法是在调用此方法后立即将用户重定向到不同的位置(如主页)。这是HTTP协议本身的影响,而不是Shiro实现的反映。
// 如果需要,非http环境可以使用注销的Subject再次登录。
public void logout() {
try {
// 清理运行时的标识
clearRunAsIdentitiesInternal(); // 7.1
// 调用登出方法
this.securityManager.logout(this); // 7.2
} finally {
this.session = null;
this.principals = null;
this.authenticated = false;
// 不要将securityManager在这里设置为空,因为subject仍然是可以使用的,只是它此时被视为是“匿名的”。
// 当subject再次登录或者请求一个新的session时,就需要用到SecurityManager实例。
//this.securityManager = null;
}
}
...
}
7.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的相关属性
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}
}
7.2
public class DefaultSecurityManager extends SessionsSecurityManager {
// 从系统注销指定的Subject。
// 请注意,大多数应用程序开发人员不应该调用此方法,除非他们有充分的理由这样做。
// 登出Subject的首选方法是调用Subject.logout(),而不是直接调用SecurityManager。
// 另一方面,框架开发人员可能会发现在某些情况下直接调用这个方法很有用。
public void logout(Subject subject) {
// 若传入的subject为空,则抛出异常
if (subject == null) {
throw new IllegalArgumentException("Subject method argument cannot be null.");
}
beforeLogout(subject); // 7.2.1
// 以PrincipalCollection的形式返回这个Subject的主体(标识属性);如果这个Subject是匿名的,因为它还没有任何关联的帐户数据(例如,如果它们还没有登录),则返回null。
PrincipalCollection principals = subject.getPrincipals();
if (principals != null && !principals.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Logging out subject with primary principal {}", principals.getPrimaryPrincipal());
}
// Authenticator负责验证应用程序中的帐户。它是Shiro API的主要入口之一。
// 获取Authenticator
Authenticator authc = getAuthenticator(); // 7.2.2
// 调用onLogout()方法
if (authc instanceof LogoutAware) {
((LogoutAware) authc).onLogout(principals); // 7.2.3
}
}
try {
delete(subject); // 7.2.4
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly unbind Subject. Ignoring (logging out).";
log.debug(msg, e);
}
} finally {
try {
stopSession(subject); // 7.2.5
} catch (Exception e) {
if (log.isDebugEnabled()) {
String msg = "Unable to cleanly stop Session for Subject [" + subject.getPrincipal() + "] " +
"Ignoring (logging out).";
log.debug(msg, e);
}
}
}
}
}
7.2.1
public class DefaultSecurityManager extends SessionsSecurityManager {
protected void beforeLogout(Subject subject) {
rememberMeLogout(subject);
}
protected void rememberMeLogout(Subject subject) {
RememberMeManager rmm = getRememberMeManager();
if (rmm != null) {
try {
// 对从应用程序注销的Subject作出反应,通常是通过忘记以前为Subject记住的任何主体。
rmm.onLogout(subject);
} catch (Exception e) {
if (log.isWarnEnabled()) {
String msg = "Delegate RememberMeManager instance of type [" + rmm.getClass().getName() +
"] threw an exception during onLogout for subject with principals [" +
(subject != null ? subject.getPrincipals() : null) + "]";
log.warn(msg, e);
}
}
}
}
}
7.2.2
// Shiro支持SecurityManager类层次结构,将所有身份验证操作委托给包装好的Authenticator实例。
// 也就是说,这个类实现了SecurityManager接口中的所有Authenticator方法,但实际上,这些方法只是传递到底层的“真正的”Authenticator实例的调用。
// 所有其他SecurityManager(授权、会话等)方法都留给子类来实现。
// 为了与这个层次结构中的其他类保持一致,以及Shiro希望尽可能最小化配置,在实例化时为所有依赖项创建合适的默认实例。
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
// 返回SecurityManager用来执行所有身份验证操作的委托身份验证器实例。
// 除非被setAuthenticator覆盖,否则默认实例是ModularRealmAuthenticator。
public Authenticator getAuthenticator() {
return authenticator;
}
}
7.2.3
public class ModularRealmAuthenticator extends AbstractAuthenticator {
// 当Subject退出系统时触发的回调。
// 首先调用super.onLogout(Subject)以确保发出了注销通知,对于每个封装的实现LogoutAware接口的Realm,调用((LogoutAware)realm). onlogout(主体)以允许每个域在用户注销期间执行注销/清理操作。
// Shiro的Realm实现在默认情况下都实现了LogoutAware接口,并且可以为特定于Realm的注销逻辑重写该接口。
public void onLogout(PrincipalCollection principals) {
super.onLogout(principals);
Collection<Realm> realms = getRealms();
if (!CollectionUtils.isEmpty(realms)) {
for (Realm realm : realms) {
if (realm instanceof LogoutAware) {
((LogoutAware) realm).onLogout(principals);
}
}
}
}
}
7.2.4
public class DefaultSecurityManager extends SessionsSecurityManager {
// 从应用程序中移除(或“解除绑定”)Subject的状态,通常在注销时调用。
// 这个实现仅仅委托给内部的subjectDAO并调用delete(subject)。
protected void delete(Subject subject) {
this.subjectDAO.delete(subject);
}
}
7.2.5
public class DefaultSecurityManager extends SessionsSecurityManager {
protected void stopSession(Subject subject) {
Session s = subject.getSession(false);
if (s != null) {
// 显式地停止(无效)这个会话并释放所有相关的资源。
// 如果这个会话已经被验证过(即拥有这个会话的Subject已经登录),显式调用这个方法可能会有不希望的副作用:
// Subject实现通常会在会话中保留身份验证状态。
// 如果应用程序代码通过直接调用此方法显式地停止会话,则可以清除可能存在的任何身份验证状态,从而有效地删除Subject的“已验证”状态。
// 因此,可以考虑注销“拥有的”Subject,而不是手动调用该方法,因为注销将自动停止相应的会话,并允许框架代码执行额外的清理逻辑。
s.stop();
}
}
}
(完)