Shiro登录的使用以及原理(一)

编程入门 行业动态 更新时间:2024-10-25 00:33:29

Shiro登录的使用以及<a href=https://www.elefans.com/category/jswz/34/1770123.html style=原理(一)"/>

Shiro登录的使用以及原理(一)

 

    好久没写博客了,这段时间对最近项目做个总结,先从登入下手,话不多说直奔主题,Shiro的登录使用以及原理。

目录

一、Shiro主要作用

二、登录的使用

2.1 SecurityManager的生成与使用

2.1 SecurityUtils的作用以及使用

三 ThreadLocal小插曲


一、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创建出Subjectsubject = (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;// 需要先设置对应的SecurityManagepublic Builder() {this(SecurityUtils.getSecurityManager());}// 通过securityManager创建出subjectpublic Subject buildSubject() {return this.securityManager.createSubject(this.subjectContext);}..........loading...............}

得出结论 Subject的实例都会(也是必须)绑定一个SecurityManager,对Subject的操作会转为Subject与SecurityManager之间的交互。

看来Subject的生成都是SecurityManager在做苦力活啊。

那么SecurityManager是怎么生成的?

先查阅下官方文档SecurityManager是怎么生成的

根据官方文档:.html

那么我们就先通过使用ini文件进行尝试下:

在resource下创建一个shiro.ini文件放入用户信息

[users]
hu=123

然后通过官方给出的:

        //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManagerFactory<SecurityManager> factory =new IniSecurityManagerFactory("classpath:shiro.ini");//2、得到SecurityManager实例 并绑定给SecurityUtilsSecurityManager 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为其加上对应的realmprivate 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();//生成默认的SecurityManagerSecurityManager 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进行了removeSubject 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并生成subjectContextthis.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和ServletResponsepublic Subject createSubject(SubjectContext subjectContext) {SubjectContext context = this.copy(subjectContext);//确保securityManager非空 context = this.ensureSecurityManager(context);//获取对应的session context = this.resolveSession(context);//获取对应的PrincipalCollectioncontext = this.resolvePrincipals(context);//根据subjectFactory生成subjectSubject subject = this.doCreateSubject(context);//根据subjectDAO保存对应的subject  this.save(subject);return subject;}

获取到了subject后就是login的具体实现

    public void login(AuthenticationToken token) throws AuthenticationException {this.clearRunAsIdentitiesInternal();// 实际为对应的securityManager进行loginSubject 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.3Subject 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的authenticatepublic 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;}}// 进行doAuthenticateprotected 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而保持sessionpublic 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获取sessionprotected 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);}
// 查询对应的sessionpublic 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类似于一个工具类,可以理解为在当前线程中设置对应的一个局部变量,这个变量可以起到上下文等操作,具体可以看另一个篇章。

具体查看

 

 

更多推荐

Shiro登录的使用以及原理(一)

本文发布于:2023-06-18 23:15:33,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/777190.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:原理   Shiro

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!