《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 https://my.oschina.net/xiaoqiyiye/blog/1619214 「xiaoqiyiye」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在于分析Shiro源码,对于新学习的朋友可以参考
[开涛博客](http://jinnianshilongnian.iteye.com/blog/2018398)进行学习。

Subject接口

在Shiro中Subject表示系统进行交互的用户或某一个第三方服务,所有Subject实例都被绑定到(且这是必须的)一个SecurityManager上。Subject接口提供了很多方法,主要包括进行认证的登录登出方法、进行授权判断的方法和Session访问的方法。在Shiro中获取当前运行的Subject要使用SecurityUtils.getSubject()方法来获取一个Subject实例。

下面将Subject接口方法进行分类:

  1. 和认证的相关的方法
// 返回可以鉴定Subject唯一性的身份信息,例如:用户名、用户ID等等
Object getPrincipal();

// 以PrincipalCollection形式返回身份信息
PrincipalCollection getPrincipals();

// 是否已经被认证通过
boolean isAuthenticated();

// 登录认证
void login(AuthenticationToken token) throws AuthenticationException;

// 认证登出
void logout();
  1. 和授权相关的方法
// 是否有指定的权限
boolean isPermitted(String permission);

// 是否有指定的权限
boolean isPermitted(Permission permission);

// 是否有指定的权限
boolean[] isPermitted(String... permissions);

// 是否有指定的权限
boolean[] isPermitted(List<Permission> permissions);

// 是否有所有指定的权限
boolean isPermittedAll(String... permissions);

// 是否有所有指定的权限
boolean isPermittedAll(Collection<Permission> permissions);

// 检测是否有权限,如果没有将抛出异常
void checkPermission(String permission) throws AuthorizationException;

// 检测是否有权限,如果没有将抛出异常
void checkPermission(Permission permission) throws AuthorizationException;

// 检测是否有权限,如果没有将抛出异常
void checkPermissions(String... permissions) throws AuthorizationException;

// 检测是否有权限,如果没有将抛出异常
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;

// 是否有指定角色
boolean hasRole(String roleIdentifier);

// 是否有指定角色
boolean[] hasRoles(List<String> roleIdentifiers);

// 是否有所有指定角色
boolean hasAllRoles(Collection<String> roleIdentifiers);

// 检测是否有指定角色,如果没有将抛出异常
void checkRole(String roleIdentifier) throws AuthorizationException;

// 检测是否有所有指定角色,如果没有将抛出异常
void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;

// 检测是否有所有指定角色,如果没有将抛出异常
void checkRoles(String... roleIdentifiers) throws AuthorizationException;
  1. 和Session相关方法
// 获取关联的Session,如果没有将会创建新Session
Session getSession();

// 获取关联的Session,如果Session不存在并且create=false,将不会创建新Session
Session getSession(boolean create);
  1. runAs相关的方法
void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;

boolean isRunAs();

PrincipalCollection getPreviousPrincipals();

PrincipalCollection releaseRunAs();

在Subject接口中还提供了一个静态内部类Builder,这个类辅助创建Subject实例。在Builder中需要引用SubjectContext和SecurityManager,SubjectContext负责收集Subject的上下文信息,SecurityManger才是真实创建Subject的对象,通过createSubject(SubjectContext subjectContext)方法来创建,SubjectContext和SessionContext类是,我们可以将它看作一个Map对象。创建Subject已经在Shiro源码分析(1) - Shiro开篇中分析过了,这里不再赘述。

Shiro登录

Shiro源码分析(1) - Shiro开篇中我们已经了解了Subject是如何创建的,这里我们将分析Shiro是如何处理登录和登出的。为了方便大家看明白些,还是贴出之前使用的代码块。

Subject subject = SecurityUtils.getSubject(); 
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
subject.login(token);

在下面的分析中我们以DelegatingSubject类来说明,DelegatingSubject是Subject接口的直接实现类。我们先看看DelegatingSubject的属性和构造方法。

// 身份凭证集合对象
protected PrincipalCollection principals;
// 是否被认证,表示该Subject是否已经被认证通过
protected boolean authenticated;
// 主机
protected String host;
// 关联的Session
protected Session session;

// 是否创建Session
protected boolean sessionCreationEnabled;

// 安全管理器,DelegatingSubject中所有的操作都是委派给SecurityManager来处理
protected transient SecurityManager securityManager;

public DelegatingSubject(SecurityManager securityManager) {
this(null, false, null, null, securityManager);
}

public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
Session session, SecurityManager securityManager) {
this(principals, authenticated, host, session, true, securityManager);
}

public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host,
Session session, boolean sessionCreationEnabled, SecurityManager securityManager) {
// securityManager 是必须存在的,前面已经多次强调
if (securityManager == null) {
throw new IllegalArgumentException("SecurityManager argument cannot be null.");
}
this.securityManager = securityManager;
this.principals = principals;
this.authenticated = authenticated;
this.host = host;
if (session != null) {
// 包装Session为StoppingAwareProxiedSession
// session销毁时会进行资源释放
this.session = decorate(session);
}
this.sessionCreationEnabled = sessionCreationEnabled;
}

在创建好Subject实例后,就可以调用login(AuthenticationToken token)方法操作登录了。具体分析如下:

public void login(AuthenticationToken token) throws AuthenticationException {

// 清除runAs身份
clearRunAsIdentitiesInternal();

// 委派给SecurityManager去执行登录,如果登录成功会返回一个
// 携带有认证成功数据的Subject对象
Subject subject = securityManager.login(this, token);

PrincipalCollection principals;

String host = null;

// 获取登录后的身份和主机信息
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}

// 如果没有身份,抛出异常
if (principals == null || principals.isEmpty()) {
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);
}

// 设置身份到当前这个Subject实例中
this.principals = principals;
// 标记为已经认证过
this.authenticated = true;

// 获取主机
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}

// 获取Session(就算认证成功了,Session也不一定存在)
// false参数表示在Session不存在的情况下不会主动创建新Session
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}

整理一下Subject的登录流程:

  • 需要清除runAs身份
  • 委派给SecurityManager做登录认证操作
  • 将认证成功的Subject信息设置到当前的Subject中去,标记已经被认证。

Shiro登出

Shiro登出过程很简单,代码如下,不用详细分析。

public void logout() {
try {
// 清除runAs身份
clearRunAsIdentitiesInternal();
// 委派给SecurityManager做登出操作
this.securityManager.logout(this);
} finally {
// 重置属性
this.session = null;
this.principals = null;
this.authenticated = false;
}
}

从上面的分析看,登录登出都是由SecurityManager来做的,后续我们会对SecurityManager进行分析。以及对授权的分析也在SecurityManager中进行说明。

runAs

runAs是指当前用户以一个给定的身份进行认证。给定的身份信息是存放在Session中的,也就是是如果要进行runAs操作,必须开启创建Session参数sessionCreationEnabled=true,否则会抛异常。

public void runAs(PrincipalCollection principals) {
// 首先Subject本身需要存在唯一身份
if (!hasPrincipals()) {
String msg = "This subject does not yet have an identity. Assuming the identity of another " +
"Subject is only allowed for Subjects with an existing identity. Try logging this subject in " +
"first, or using the " + Subject.Builder.class.getName() + " to build ad hoc Subject instances " +
"with identities as necessary.";
throw new IllegalStateException(msg);
}
// 将身份信息存放到Session属性中去
pushIdentity(principals);
}

public boolean isRunAs() {
// 判断runAs栈中是否有身份
List<PrincipalCollection> stack = getRunAsPrincipalsStack();
return !CollectionUtils.isEmpty(stack);
}

/**
* 从runAs栈中获取身份信息
*/
public PrincipalCollection getPreviousPrincipals() {
PrincipalCollection previousPrincipals = null;
List<PrincipalCollection> stack = getRunAsPrincipalsStack();
int stackSize = stack != null ? stack.size() : 0;
if (stackSize > 0) {
if (stackSize == 1) {
previousPrincipals = this.principals;
} else {
//always get the one behind the current:
assert stack != null;
previousPrincipals = stack.get(1);
}
}
return previousPrincipals;
}

public PrincipalCollection releaseRunAs() {
return popIdentity();
}

// 从Session中获取runAs栈
private List<PrincipalCollection> getRunAsPrincipalsStack() {
Session session = getSession(false);
if (session != null) {
return (List<PrincipalCollection>) session.getAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
return null;
}

private void clearRunAsIdentities() {
Session session = getSession(false);
if (session != null) {
session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
}
}

private void pushIdentity(PrincipalCollection principals) throws NullPointerException {
if (CollectionUtils.isEmpty(principals)) {
String msg = "Specified Subject principals cannot be null or empty for 'run as' functionality.";
throw new NullPointerException(msg);
}
// 获取runAs栈,如果不存在就创建
List<PrincipalCollection> stack = getRunAsPrincipalsStack();
if (stack == null) {
stack = new CopyOnWriteArrayList<PrincipalCollection>();
}
// 保存身份到最前面
stack.add(0, principals);
// 获取Session,将runAs栈保存到Session中
Session session = getSession();
session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, stack);
}

private PrincipalCollection popIdentity() {
PrincipalCollection popped = null;

List<PrincipalCollection> stack = getRunAsPrincipalsStack();
if (!CollectionUtils.isEmpty(stack)) {
popped = stack.remove(0);
Session session;
if (!CollectionUtils.isEmpty(stack)) {
session = getSession();
session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, stack);
} else {
//stack is empty, remove it from the session:
clearRunAsIdentities();
}
}

return popped;
}
文章目录
  1. 1. Subject接口
  2. 2. Shiro登录
  3. 3. Shiro登出
  4. 4. runAs