好久没写博客了,这段时间对最近项目做个总结,先从登入下手,话不多说直奔主题,Shiro的登录使用以及原理。
目录
一、Shiro主要作用
shiro主要的作用就是实现用户登录(认证,授权,加密等),用户登录后的用户信息存储(缓存),用户登出等。
二、登录的使用
在使用登录的时候,最常见的一串代码就是通过工具类SecurityUtils获取Subject,然后对Token进行login();
// 得到subject然后对创建用户名/密码身份验证
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("hu", "123");
subject.login(token);
这时候只对这串代码进行编译运行,你会发现会报一个异常信息
org.apache.shiro.UnavailableSecurityManagerException: No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.ThreadContext or as a vm static singleton. This is an invalid application configuration.
2.1 SecurityManager的生成与使用
根据报错信息以及对SecuriTyUtils的源码发现使用SecurityUtils.getSubject()的时候必须要为其设置一个securityManager,具体如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.apache.shiro;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.Subject.Builder;
import org.apache.shiro.util.ThreadContext;
public abstract class SecurityUtils {
private static SecurityManager securityManager;
public SecurityUtils() {
}
public static Subject getSubject() {
// 通过ThreadContext获取对应的Subject,若未在ThredContext中加入该subject必定为空
// ThreadContext可以通过源码了解到为使用过TreadLocal模式 具体看标题三
//因为是TreadLocal所以表示每个线程初次进来的时候,获取到的subeject必为空
Subject subject = ThreadContext.getSubject();
if (subject == null) {
//具体看下列代码块 主要执行为通过SecurityManager创建出Subject
subject = (new Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
public static void setSecurityManager(SecurityManager securityManager) {
SecurityUtils.securityManager = securityManager;
}
public static SecurityManager getSecurityManager() throws UnavailableSecurityManagerException {
SecurityManager securityManager = ThreadContext.getSecurityManager();
if (securityManager == null) {
securityManager = SecurityUtils.securityManager;
}
if (securityManager == null) {
String msg = "No SecurityManager accessible to the calling code, either bound to the " + ThreadContext.class.getName() + " or as a vm static singleton. This is an invalid application " + "configuration.";
throw new UnavailableSecurityManagerException(msg);
} else {
return securityManager;
}
}
}
// 在Subjct初次获取到为空的时候会调用的Subject的静态内部类,创建一个Builder,在通过buildSubject的方法进行实现Subject的生成
public static class Builder {
private final SubjectContext subjectContext;
private final SecurityManager securityManager;
// 需要先设置对应的SecurityManage
public Builder() {
this(SecurityUtils.getSecurityManager());
}
// 通过securityManager创建出subject
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
..........loading...............
}
得出结论 Subject的实例都会(也是必须)绑定一个SecurityManager,对Subject的操作会转为Subject与SecurityManager之间的交互。
看来Subject的生成都是SecurityManager在做苦力活啊。
那么SecurityManager是怎么生成的?
先查阅下官方文档SecurityManager是怎么生成的
根据官方文档:http://greycode.github.io/shiro/doc/tutorial.html
那么我们就先通过使用ini文件进行尝试下:
在resource下创建一个shiro.ini文件放入用户信息
[users]
hu=123
然后通过官方给出的:
//1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<SecurityManager> factory =
new IniSecurityManagerFactory("classpath:shiro.ini");
//2、得到SecurityManager实例 并绑定给SecurityUtils
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
这么一个案例又让我们想起了那个Spring等源码中最喜欢用的工厂模式,看看这里的Factory接口啥样:
package org.apache.shiro.util;
public interface Factory<T> {
T getInstance();
}
就是一个用来生成T对应实例的工厂,一个很一般的代码,噢,我的意思是,我的上帝啊,这真是一个写的很棒的代码,一个很完美的工厂。
那么看看IniSecurityManagerFactory干了啥?
看了代码构造函数就做了这些小事,就是将Ini对象赋值下
// 调用了对应的构造函数,将iniResourcePath路径读取为代码可识别的Ini对象
public IniSecurityManagerFactory(String iniResourcePath) {
this(Ini.fromResourcePath(iniResourcePath));
}
// Ini对象具体就是根据文件路径读取对应的文件内的信息流
// 然后调用对应Ini的构造函数
public IniSecurityManagerFactory(Ini config) {
this.setIni(config);
}
那么就是主要是在getInstance的方法中咯?通过对IniSecurityManagerFactory类中查看并未发现getInstance
那么就可以确定在了子类中的实现(运用了模板设计模式的实现)
public class IniSecurityManagerFactory extends IniFactorySupport<SecurityManager>
public abstract class IniFactorySupport<T> extends AbstractFactory<T>
看看AbstractFactory的源码吧,这里的单例对象为啥不用static?(个人理解,因为Factory是只会创建出一次该实现的bean对象,所以只会一个Factory那么就没必要考虑有多个单例对象,就没必要用static修饰,而且还能用非单例模式进行每次创建对象,如果理解有误希望能帮忙改正)
public abstract class AbstractFactory<T> implements Factory<T> {
// 是否使用单例
private boolean singleton = true;
private T singletonInstance;
public AbstractFactory() {
}
public boolean isSingleton() {
return this.singleton;
}
public void setSingleton(boolean singleton) {
this.singleton = singleton;
}
public T getInstance() {
Object instance;
// 如果为单例模式则只用创建一次对象 懒汉式
if (this.isSingleton()) {
if (this.singletonInstance == null) {
this.singletonInstance = this.createInstance();
}
instance = this.singletonInstance;
} else {
instance = this.createInstance();
}
if (instance == null) {
String msg = "Factory 'createInstance' implementation returned a null object.";
throw new IllegalStateException(msg);
} else {
return instance;
}
}
protected abstract T createInstance();
}
具体的实现方法又回到了对应的父类(模板设计模式)
// 先调用了抽象类IniFactorySupport的实现方法
public T createInstance() {
Ini ini = this.resolveIni();
Object instance;
String msg;
if (CollectionUtils.isEmpty(ini)) {
log.debug("No populated Ini available. Creating a default instance.");
instance = this.createDefaultInstance();
if (instance == null) {
msg = this.getClass().getName() + " implementation did not return a default instance in " + "the event of a null/empty Ini configuration. This is required to support the " + "Factory interface. Please check your implementation.";
throw new IllegalStateException(msg);
}
} else {
log.debug("Creating instance from Ini [" + ini + "]");
instance = this.createInstance(ini);
if (instance == null) {
msg = this.getClass().getName() + " implementation did not return a constructed instance from " + "the createInstance(Ini) method implementation.";
throw new IllegalStateException(msg);
}
}
return instance;
}
//若为空则调用了IniSecurityManagerFactory中创建默认SecurityManager方法 默认的详细就跳过了
protected SecurityManager createDefaultInstance() {
return new DefaultSecurityManager();
}
// 非空则调用了IniSecurityManagerFactory中的方法
protected SecurityManager createInstance(Ini ini) {
if (CollectionUtils.isEmpty(ini)) {
throw new NullPointerException("Ini argument cannot be null or empty.");
} else {
SecurityManager securityManager = this.createSecurityManager(ini);
if (securityManager == null) {
String msg = SecurityManager.class + " instance cannot be null.";
throw new ConfigurationException(msg);
} else {
return securityManager;
}
}
}
//createSecurityManager方法 此处的Section就是将文件内容封装成的对象(其中sectionName就是通过s.startsWith("[") && s.endsWith("]")进行判断) 即表示我们文件中users,然后将内容放入到对应的的映射中,根据分割号分给为map映射(如key为hu,value对应为123)
private SecurityManager createSecurityManager(Ini ini) {
// 因为我们文件中为加入main所以mainSection为null mainStion的具体作用暂不了解
Section mainSection = ini.getSection("main");
if (CollectionUtils.isEmpty(mainSection)) {
mainSection = ini.getSection("");
}
return this.createSecurityManager(ini, mainSection);
}
//继续调用 主要作用就是生成SecurityManager然后根据是否autoApplyRealms为其加上对应的realm
private SecurityManager createSecurityManager(Ini ini, Section mainSection) {
Map<String, ?> defaults = this.createDefaults(ini, mainSection);
Map<String, ?> objects = this.buildInstances(mainSection, defaults);
SecurityManager securityManager = this.getSecurityManagerBean();
boolean autoApplyRealms = this.isAutoApplyRealms(securityManager);
if (autoApplyRealms) {
Collection<Realm> realms = this.getRealms(objects);
if (!CollectionUtils.isEmpty(realms)) {
this.applyRealmsToSecurityManager(realms, securityManager);
}
}
return securityManager;
}
//createDefaults的方法实现
protected Map<String, ?> createDefaults(Ini ini, Section mainSection) {
Map<String, Object> defaults = new LinkedHashMap();
//生成默认的SecurityManager
SecurityManager securityManager = this.createDefaultInstance();
defaults.put("securityManager", securityManager);
//判断是否需要生成对应的realm 主要用来后面的用户验证
// 文件中是根据[roles]或是[users]标签来确定的 ,创建对应的IniRealm,users主要用于登录,roles用与权限的校验
if (this.shouldImplicitlyCreateRealm(ini)) {
Realm realm = this.createRealm(ini);
if (realm != null) {
defaults.put("iniRealm", realm);
}
}
return defaults;
}
上面代码开完对应的SecurityManager(接口)对应父类的具体实例也就生成了,这里用的是DefaultSecurityManager,看看DefaultSecurityManager里具体有哪些参数,基本后面的web等地方调用的SecurityManager都是继承了DefaultSecurityManager而实现的
public class DefaultSecurityManager extends SessionsSecurityManager {
protected RememberMeManager rememberMeManager;
protected SubjectDAO subjectDAO;
protected SubjectFactory subjectFactory;
....
}
// session的管理
public abstract class SessionsSecurityManager extends AuthorizingSecurityManager {
private SessionManager sessionManager = new DefaultSessionManager();
....
}
//用户认证 实际调用Subject登录就是用对应的SecurityManager进行登录认证
public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager {
private Authorizer authorizer = new ModularRealmAuthorizer();
....
}
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
private Authenticator authenticator = new ModularRealmAuthenticator();
}
// 用户认证具体的realm
public abstract class RealmSecurityManager extends CachingSecurityManager {
private Collection<Realm> realms;
}
// 缓存相关
public abstract class CachingSecurityManager implements SecurityManager, Destroyable, CacheManagerAware {
private CacheManager cacheManager;
}
ok,继续回到Subject的登录,SecurityManager的大致生成和作用粗略的进行了描述,接下来就是Subject登录的使用
2.2 Subject 的登录
SecurityManager生成后,进行研究上面的SecurityUtils.getSubject();的方法。
// 根据此处可以看出运用了ThreadContext的方法而该方法对其源码可以看出为InheritableThreadLocal的使用,如果说ThreadLocal的作用大家都基本了解,用于为当前线程创建一个局部线程变量,只能在该线程中使用,那么InheritableThreadLocal也类似,具体可以查看目录三
public static Subject getSubject() {
// 线程初次使用必定为空,则需要对其进行ThreadLocal的set,此处防止内存泄漏在bind的时候对空key的value进行了remove
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
// new Builde()的过程
public Builder() {
this(SecurityUtils.getSecurityManager());
}
public Builder(SecurityManager securityManager) {
if (securityManager == null) {
throw new NullPointerException("SecurityManager method argument cannot be null.");
} else {
// 指定securityManager并生成subjectContext
this.securityManager = securityManager;
this.subjectContext = this.newSubjectContextInstance();
if (this.subjectContext == null) {
throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' cannot be null.");
} else {
this.subjectContext.setSecurityManager(securityManager);
}
}
}
//根据securityManger和subjectContext生成对应的subject(DelegatingSubject或是WebDelegatingSubject)
WebDelegatingSubject继承了DelegatingSubject 多了ServletRequest和ServletResponse
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = this.copy(subjectContext);
//确保securityManager非空
context = this.ensureSecurityManager(context);
//获取对应的session
context = this.resolveSession(context);
//获取对应的PrincipalCollection
context = this.resolvePrincipals(context);
//根据subjectFactory生成subject
Subject subject = this.doCreateSubject(context);
//根据subjectDAO保存对应的subject
this.save(subject);
return subject;
}
获取到了subject后就是login的具体实现
public void login(AuthenticationToken token) throws AuthenticationException {
this.clearRunAsIdentitiesInternal();
// 实际为对应的securityManager进行login
Subject subject = this.securityManager.login(this, token);
String host = null;
PrincipalCollection principals;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject)subject;
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals != null && !principals.isEmpty()) {
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken)token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = this.decorate(session);
} else {
this.session = null;
}
} else {
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);
}
}
// 实际为调用securityManager的login方法
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = this.authenticate(token);
} catch (AuthenticationException var7) {
AuthenticationException ae = var7;
try {
this.onFailedLogin(token, ae, subject);
} catch (Exception var6) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6);
}
}
throw var7;
}
// 验证通过后生成对应的Subject生成 这一步有大量信息报错,如session如下2.3
Subject loggedIn = this.createSubject(token, info, subject);
// 更新各种信息,如session在数据库中的缓存
this.onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
// 通过securityManager的authenticate验证,实际调用Authenticator的authenticate方法
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
//AbstractAuthenticator的authenticate
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
} else {
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = this.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 var8) {
AuthenticationException ae = null;
if (var8 instanceof AuthenticationException) {
ae = (AuthenticationException)var8;
}
if (ae == null) {
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " + "error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, var8);
}
try {
this.notifyFailure(token, ae);
} catch (Throwable var7) {
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, var7);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
this.notifySuccess(token, info);
return info;
}
}
// 进行doAuthenticate
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
this.assertRealmsConfigured();
Collection<Realm> realms = this.getRealms();
return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
}
// 根据对应的realms进行判断是单一realm验证还是多个realm验证 以单一为例子,实际为根据对应的realm进行了验证,所以自己使用指定的securityManager时候,可以通过指定自己的realm进行身份验证
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);
} else {
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);
} else {
return info;
}
}
}
2.3 登录线程结束后同一个sessionId的线程进入,获取对应sessionId对应的session
登录时候的session的生成,登录之前一般是没有session的,只有登录只会才会有对应的session生成,登录完成后会调用session生成的代码如下:
//这一段在登录完成之后
Subject loggedIn = this.createSubject(token, info, subject);
//执行
return this.createSubject(context);
//执行
this.save(subject);
//执行
protected void save(Subject subject) {
this.subjectDAO.save(subject);
}
//继续执行 此处会根据是否保存session而保持session
public Subject save(Subject subject) {
if (this.isSessionStorageEnabled(subject)) {
this.saveToSession(subject);
} else {
log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and authentication state are expected to be initialized on every request or invocation.", subject);
}
return subject;
}
// 主要进行了两个步骤
protected void saveToSession(Subject subject) {
this.mergePrincipals(subject);
this.mergeAuthenticationState(subject);
}
//下列这串中进行了session为空则生成并保持
protected void mergePrincipals(Subject subject) {
PrincipalCollection currentPrincipals = null;
if (subject.isRunAs() && subject instanceof DelegatingSubject) {
try {
Field field = DelegatingSubject.class.getDeclaredField("principals");
field.setAccessible(true);
currentPrincipals = (PrincipalCollection)field.get(subject);
} catch (Exception var5) {
throw new IllegalStateException("Unable to access DelegatingSubject principals property.", var5);
}
}
if (currentPrincipals == null || currentPrincipals.isEmpty()) {
currentPrincipals = subject.getPrincipals();
}
Session session = subject.getSession(false);
// session为空且登录完成则生成并保存
if (session == null) {
if (!CollectionUtils.isEmpty(currentPrincipals)) {
session = subject.getSession();
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
} else {
PrincipalCollection existingPrincipals = (PrincipalCollection)session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (CollectionUtils.isEmpty(currentPrincipals)) {
if (!CollectionUtils.isEmpty(existingPrincipals)) {
session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
}
} else if (!currentPrincipals.equals(existingPrincipals)) {
session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);
}
}
}
因为每个web请求都是一个新的线程,所以需要先对线程进行绑定对应的subject,每次请求的时候都会调用到
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal中调用Subject subject = this.createSubject(request, response);
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = this.copy(subjectContext);
context = this.ensureSecurityManager(context);
// 主要对seesion进行查找和处理
context = this.resolveSession(context);
context = this.resolvePrincipals(context);
Subject subject = this.doCreateSubject(context);
this.save(subject);
return subject;
}
protected SubjectContext resolveSession(SubjectContext context) {
if (context.resolveSession() != null) {
log.debug("Context already contains a session. Returning.");
return context;
} else {
try {
Session session = this.resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException var3) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous (session-less) Subject instance.", var3);
}
return context;
}
}
//根据key获取session
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
SessionKey key = this.getSessionKey(context);
return key != null ? this.getSession(key) : null;
}
public Session getSession(SessionKey key) throws SessionException {
return this.sessionManager.getSession(key);
}
// 查询对应的session
public Session getSession(SessionKey key) throws SessionException {
Session session = this.lookupSession(key);
return session != null ? this.createExposedSession(session, key) : null;
}
private Session lookupSession(SessionKey key) throws SessionException {
if (key == null) {
throw new NullPointerException("SessionKey argument cannot be null.");
} else {
return this.doGetSession(key);
}
}
protected final Session doGetSession(SessionKey key) throws InvalidSessionException {
this.enableSessionValidationIfNecessary();
log.trace("Attempting to retrieve session with key {}", key);
Session s = this.retrieveSession(key);
if (s != null) {
this.validate(s, key);
}
return s;
}
// 通过这里可以看出 重写sessionManager的getSessionId方法来找出不同传值
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = this.getSessionId(sessionKey);
if (sessionId == null) {
log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a session could not be found.", sessionKey);
return null;
} else {
Session s = this.retrieveSessionFromDataSource(sessionId);
if (s == null) {
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
} else {
return s;
}
}
}
//再返回
boolean authenticated = wsc.resolveAuthenticated();中看出由session中的信息来判断是否登录
public boolean resolveAuthenticated() {
Boolean authc = (Boolean)this.getTypedValue(AUTHENTICATED, Boolean.class);
if (authc == null) {
AuthenticationInfo info = this.getAuthenticationInfo();
authc = info != null;
}
if (!authc) {
Session session = this.resolveSession();
if (session != null) {
Boolean sessionAuthc = (Boolean)session.getAttribute(AUTHENTICATED_SESSION_KEY);
authc = sessionAuthc != null && sessionAuthc;
}
}
return authc;
}
三 ThreadLocal小插曲
ThreadLoacl类似于一个工具类,可以理解为在当前线程中设置对应的一个局部变量,这个变量可以起到上下文等操作,具体可以看另一个篇章。