Shiro源码剖析——Subject的创建与获取(一次完整的请求执行流程-1

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 本文可能较长,但是通读一定能让你对整个shiro请求的执行流程有清晰的了解总体流程:1、在过滤的过程中创建subjectdoFilter -> SecurityManager -> SubjectContext -> 创建subject -> 解析各种信息并赋值2、若该subject未认证则进行认证并在认证时再次创建subject调用realm中的doAuthenticationInfo()获得返回的信息重新创建subject并保存到session


本文可能较长,但是通读一定能让你对整个shiro请求的执行流程有清晰的了解

总体流程:


1、在过滤的过程中创建subject


doFilter -> SecurityManager -> SubjectContext -> 创建subject -> 解析各种信息并赋值


2、若该subject未认证则进行认证并在认证时再次创建subject


调用realm中的doAuthenticationInfo()获得返回的信息重新创建subject并保存到session

一、AbstractShiroFilter

当我们使用shiro框架时,用户每次发送一个请求给服务端,都会被shiro的AbstractShiroFilter过滤器所拦截(AbstractShiroFilter是shiro的全局过滤器,所有的请求都会经过该过滤器)

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
    Throwable t = null;
    try {
        final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
        final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
        Subject subject = this.createSubject(request, response);
        subject.execute(new Callable() {
            public Object call() throws Exception {
                AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
                AbstractShiroFilter.this.executeChain(request, response, chain);
                return null;
            }
        });
    } catch (ExecutionException var8) {
        t = var8.getCause();
    } catch (Throwable var9) {
        t = var9;
    }
    if (t != null) {
        if (t instanceof ServletException) {
            throw (ServletException)t;
        } else if (t instanceof IOException) {
            throw (IOException)t;
        } else {
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }
}

查看这个过滤器的**doFilterInternal()**方法,我们发现它主要做了两件事

// 创建一个subject
createSubject(request, response)
// 将该subject绑定到当前线程,并更新会话的上次访问时间以及分发合适的过滤器
subject.execute(new Callable() {
  public Object call() throws Exception {
      AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
      AbstractShiroFilter.this.executeChain(request, response, chain);
      return null;
    }
});

二、createSubject(request, response)

我们先看createSubject(request, response)这个方法,追踪后来到以下方法

protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
    return (new Builder(this.getSecurityManager(), request, response)).buildWebSubject();
}

这个方法可以分为两部分

  • new Builder(this.getSecurityManager(), request, response)
  • buildWebSubject()

1、new Builder(this.getSecurityManager(), request, response)

首先通过当前的安全管理器等创建了一个Builder,以下是其构造方法

public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
    super(securityManager);
    if (request == null) {
        throw new IllegalArgumentException("ServletRequest argument cannot be null.");
    } else if (response == null) {
        throw new IllegalArgumentException("ServletResponse argument cannot be null.");
    } else {
        this.setRequest(request);
        this.setResponse(response);
    }
}

首先调用了父类的构造函数,如下

public Builder(SecurityManager securityManager) {
    if (securityManager == null) {
        throw new NullPointerException("SecurityManager method argument cannot be null.");
    } else {
        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);
        }
    }
}

在其中设置了安全管理器,并创建了一个subjectContext,随后通过this.setRequest(request); this.setResponse(response);两个方法为这个subjectContext设置request和response,如下(response设置同理)

protected WebSubject.Builder setRequest(ServletRequest request) {
    if (request != null) {
        ((WebSubjectContext)this.getSubjectContext()).setServletRequest(request);
    }
    return this;
}

至此Builder构造完成

2、buildWebSubject()

追踪源码我们来到**WebSubject类的buildWebSubject()**方法

public WebSubject buildWebSubject() {
    Subject subject = super.buildSubject();
    if (!(subject instanceof WebSubject)) {
        String msg = "Subject implementation returned from the SecurityManager was not a " + WebSubject.class.getName() + " implementation.  Please ensure a Web-enabled SecurityManager has been configured and made available to this builder.";
        throw new IllegalStateException(msg);
    } else {
        return (WebSubject)subject;
    }
}

其中调用了父类**Subject类的buildSubject()**方法

public Subject buildSubject() {
    return this.securityManager.createSubject(this.subjectContext);
}

需要注意的是这里的this.securityManager一般是DefaultWebSecurityManager类型的,继承自DefaultSecurityManager类


其实最终调用的是DefaultSecurityManager类的createSubject()方法

public Subject createSubject(SubjectContext subjectContext) {
    SubjectContext context = this.copy(subjectContext);
    context = this.ensureSecurityManager(context);
    context = this.resolveSession(context);
    context = this.resolvePrincipals(context);
    Subject subject = this.doCreateSubject(context);
    this.save(subject);
    return subject;
}
1)this.copy(SubjectContext subjectContext)

这里copy方法用的是**DefaultWebSecurityManager重写的copy()**方法

protected SubjectContext copy(SubjectContext subjectContext) {
    return (SubjectContext)(subjectContext instanceof WebSubjectContext ? new DefaultWebSubjectContext((WebSubjectContext)subjectContext) : super.copy(subjectContext));
}

将之前调用无参构造初始化的SubjectContext(上文构造Builder时使用this.newSubjectContextInstance()方法创建的,这个方法调用了DefaultSubjectContext的无参构造函数,实例化了一个SubjectContext)作为参数,调用了DefaultSubjectContext的有参构造,最终也调用了MapContext中的有参构造;返回了一个SubjectContext


SubjectContext接口由DefaultSubjectContext实现(还有一个子类是DefaultWebSubjectContext),同时DefaultSubjectContext还继承自MapContext,其中有一个backingMap(本质也是一个map),里面是一路收集的一些信息,比如securityManger,subject,sessionId,principals,session等,key是DefaultSubjectContext中定义的一些常量(如下)。

private static final String SECURITY_MANAGER = DefaultSubjectContext.class.getName() + ".SECURITY_MANAGER";
private static final String SESSION_ID = DefaultSubjectContext.class.getName() + ".SESSION_ID";
private static final String AUTHENTICATION_TOKEN = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_TOKEN";
private static final String AUTHENTICATION_INFO = DefaultSubjectContext.class.getName() + ".AUTHENTICATION_INFO";
private static final String SUBJECT = DefaultSubjectContext.class.getName() + ".SUBJECT";
private static final String PRINCIPALS = DefaultSubjectContext.class.getName() + ".PRINCIPALS";
private static final String SESSION = DefaultSubjectContext.class.getName() + ".SESSION";
private static final String AUTHENTICATED = DefaultSubjectContext.class.getName() + ".AUTHENTICATED";
private static final String HOST = DefaultSubjectContext.class.getName() + ".HOST";
public static final String SESSION_CREATION_ENABLED = DefaultSubjectContext.class.getName() + ".SESSION_CREATION_ENABLED";
public static final String PRINCIPALS_SESSION_KEY = DefaultSubjectContext.class.getName() + "_PRINCIPALS_SESSION_KEY";
public static final String AUTHENTICATED_SESSION_KEY = DefaultSubjectContext.class.getName() + "_AUTHENTICATED_SESSION_KEY";
private static final transient Logger log = LoggerFactory.getLogger(DefaultSubjectContext.class);

这个上下文的作用就是当初始化Subject时,从中获取需要的值来作为初始化Subject的参数


resolve的意思是解析,接下来的三步就是解析上下文中的manager,session和principals,为SubjectContext也就是MapContext中的backingMap的key中添加相应的value;


点进去查看会发现调用了DefaultSubjectContext的父类MapContext中的nullSafePut与put方法;

2)this.ensureSecurityManager(context)

用来确保上下文中已经存在securityManager,如果没有则将当前的securityManager设置进去

3)this.resolveSession(context)

1.resolveSession(subjectContext),首先尝试从context(MapContext)中获取session,没有就获取subject后尝试从subject中获取

2.如果仍不存在则调用resolveContextSession(subjectContext),试着从MapContext中获取sessionId

3.根据sessionId实例化一个SessionKey对象,并通过SessionKey实例获取session

4.getSession(key)的任务直接交给sessionManager来执行

5.如果key中获得的sessionId为null,则前往cookie中获取

具体如下:

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;
    }
}

如果上下文中有session则直接返回,没有则进行解析,**调用this.resolveContextSession(context)**方法

protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
    SessionKey key = this.getSessionKey(context);
    return key != null ? this.getSession(key) : null;
}

注意这里this.getSessionKey(context)是调用DefaultWebSecurityManager类中的方法

protected SessionKey getSessionKey(SubjectContext context) {
    if (WebUtils.isWeb(context)) {
        Serializable sessionId = context.getSessionId();
        ServletRequest request = WebUtils.getRequest(context);
        ServletResponse response = WebUtils.getResponse(context);
        return new WebSessionKey(sessionId, request, response);
    } else {
        return super.getSessionKey(context);
    }
}

如果是web请求(携带着response和request)则context.getSessionId()这个接口会调用DefaultSessionManager的实现(注意此处不是DefaultWebSessionManager),如下:

public Serializable getSessionId() {
    return getTypedValue(SESSION_ID, Serializable.class);
}

可以看这里其实时通过上下文去获取sessionId而不是获取请求中的sessionId


显然在此处上下文中还尚未有该信息,自然是获取不到的


随后获取request和response,根据sessionId(null)和request和response封装为WebSessionKey

回到return key != null ? this.getSession(key) : null;这里key!=null,所以调用this.getSession(key)

public Session getSession(SessionKey key) throws SessionException {
    return this.sessionManager.getSession(key);
}

接着调用在AbstractNativeSessionManager中的实现

public Session getSession(SessionKey key) throws SessionException {
    Session session = lookupSession(key);
    return session != null ? createExposedSession(session, key) : null;
}
private Session lookupSession(SessionKey key) throws SessionException {
    if (key == null) {
        throw new NullPointerException("SessionKey argument cannot be null.");
    }
    return doGetSession(key);
}

这里key不等于null直接执行doGetSession(key),来到AbstractvalidatingSessionMangager类的实现

protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
    enableSessionValidationIfNecessary();
    log.trace("Attempting to retrieve session with key {}", key);
    Session s = retrieveSession(key);
    if (s != null) {
        validate(s, key);
    }
    return s;
}

接着执行Session s = retrieveSession(key),来到DefaultSessionManager中的实现

protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
    Serializable sessionId = 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;
    }
    Session s = retrieveSessionFromDataSource(sessionId);
    if (s == null) {
        //session ID was provided, meaning one is expected to be found, but we couldn't find one:
        String msg = "Could not find session with ID [" + sessionId + "]";
        throw new UnknownSessionException(msg);
    }
    return s;
}

其中第一句Serializable sessionId = getSessionId(sessionKey);由于我们在配置文件中设置的默认sessionManager是DefaultWebSessionManager,所以这里执行的是DefaultWebSessionManager类中的getSessionId(sessionKey)而不是DefaultSessionManager类中的,终于我们来到了下面这一步

public Serializable getSessionId(SessionKey key) {
    Serializable id = super.getSessionId(key);
    if (id == null && WebUtils.isWeb(key)) {
        ServletRequest request = WebUtils.getRequest(key);
        ServletResponse response = WebUtils.getResponse(key);
        id = this.getSessionId(request, response);
    }
    return id;
}

这里第一句就是返回去调用DefaultSessionManager类中的方法, 然而这是通过上下文获取的,显然还是获取不到,接着id为空,我们来到this.getSessionId(request, response);

protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
    return this.getReferencedSessionId(request, response);
}
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
    String id = this.getSessionIdCookieValue(request, response);
    if (id != null) {
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie");
    } else {
        id = this.getUriPathSegmentParamValue(request, "JSESSIONID");
        if (id == null && request instanceof HttpServletRequest) {
            String name = this.getSessionIdName();
            HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
            String queryString = httpServletRequest.getQueryString();
            if (queryString != null && queryString.contains(name)) {
                id = request.getParameter(name);
            }
            if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
                id = request.getParameter(name.toLowerCase());
            }
        }
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url");
        }
    }
    if (id != null) {
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
    }
    request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, this.isSessionIdUrlRewritingEnabled());
    return id;
}

逐层调用最终来到

private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
    if (!this.isSessionIdCookieEnabled()) {
        log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
        return null;
    } else if (!(request instanceof HttpServletRequest)) {
        log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
        return null;
    } else {
        HttpServletRequest httpRequest = (HttpServletRequest)request;
        return this.getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }
}

return this.getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));


看到这一句,终于,我们通过获取cookie来读得其中保存的sessionId


这里this.getSessionIdCookie()获取的是类中定义的cookie——private Cookie sessionIdCookie,他在构造方法中创建

public DefaultWebSessionManager() {
    Cookie cookie = new SimpleCookie("JSESSIONID");
    cookie.setHttpOnly(true);
    this.sessionIdCookie = cookie;
    this.sessionIdCookieEnabled = true;
    this.sessionIdUrlRewritingEnabled = false;
}

之后通过readValue方法,它会在请求中寻找名为JSESSIONID的cookie并返回其值

至此我们终于获得了SessionId

那我们继续回到刚才这个函数

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;
        }
    }
}

如果刚才cookie中获取不到sessionI则返回null,如果获取到则执行:Session s = retrieveSessionFromDataSource(sessionId); 通过SessionDao根据sessionId获取到了Session

如果session不为null返回最开始的那句调用:context.setSession(session);

public void setSession(Session session) {
    this.nullSafePut(SESSION, session);
}
protected void nullSafePut(String key, Object value) {
    if (value != null) {
        this.put(key, value);
    }
}
public Object put(String s, Object o) {
    return this.backingMap.put(s, o);
}

如上文所说的将信息存储在backingMap

至此便解析完上下文中的session,对于第一次请求没有session来说在这里(过滤时)并不会创建新的session

4)this.resolvePrincipals(context)

同理将获得到的principals存储在backingMap中,过滤时如果前面没有获得到session那么这里也将得不到principals(如果是认证时调用到此处则可以获得,差别见第四点)

5)this.doCreateSubject(context)

追踪该方法可以到DefaultWebSubjectFactory类中的如下方法

public Subject createSubject(SubjectContext context) {
    boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject);
    if (context instanceof WebSubjectContext && !isNotBasedOnWebSubject) {
        WebSubjectContext wsc = (WebSubjectContext)context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        Session session = wsc.resolveSession();
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        PrincipalCollection principals = wsc.resolvePrincipals();
        boolean authenticated = wsc.resolveAuthenticated();
        String host = wsc.resolveHost();
        ServletRequest request = wsc.resolveServletRequest();
        ServletResponse response = wsc.resolveServletResponse();
        return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager);
    } else {
        return super.createSubject(context);
    }
}

我们通过subjectContext中保存的信息,执行return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager),这样就能够得到当前操作的主体,知道是谁在操作,是否已经认证了。


至此完成subject的创建

6)this.save(subject)

最终调用的是subjectDao中的save方法

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;
}

这里暂时不往下追踪,等到下面第四点时会再次提到这个函数

至此createSubject执行完成创建,主要步骤如下

1.拿到subjectContext

2.解析security,放入contex(map)中

3.解析session,放入context(map)中

4.解析principals,放入context(map)中

5.通过subjectFactory创建subject

6.通过sessionDAO保存到session中

需要注意的是上面讲述的是这个方法的总体功能,但这是在过滤时调用的这个方法,其实大多数都没有实现,因为此时其实并没有获得到多少信息(除非是第二次请求,可以获得session),故创建的subject也没有多少信息。


如果没有获得session,则此时还没有用户身份信息,这个Subject还没有通过验证,只保留了三个属性:request,response,securityManager。


在过滤时没有session也不会创建session来保存subject信息,具体可以看下文认证时的createSubject来做对比。

相关文章
|
7月前
|
存储 NoSQL 搜索推荐
若依框架----源码分析(@RateLimiter)
若依框架----源码分析(@RateLimiter)
492 0
|
7月前
|
缓存 前端开发 Java
【二十八】springboot之通过threadLocal+参数解析器实现同session一样保存当前登录信息的功能
【二十八】springboot之通过threadLocal+参数解析器实现同session一样保存当前登录信息的功能
171 1
|
2月前
|
缓存 Java Spring
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
文章比较了在Servlet和Spring Boot中获取Cookie、Session和Header的方法,并提供了相应的代码实例,展示了两种方式在实际应用中的异同。
170 3
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
|
存储 XML Java
Servlet进阶(Session对象实现登录)
Servlet进阶(Session对象实现登录)
309 0
|
Java 数据库 数据安全/隐私保护
用shiro框架实现注册登陆,让你快速理解shiro用法
用shiro框架实现注册登陆,让你快速理解shiro用法
498 0
用shiro框架实现注册登陆,让你快速理解shiro用法
|
Java Spring
SpringMVC源码分析:一个request请求的完整流程和各组件介绍
SpringMVC源码分析:一个request请求的完整流程和各组件介绍
52 0
|
存储 XML Java
【spring源码系列-06】refresh中obtainFreshBeanFactory方法的执行流程
【spring源码系列-06】refresh中obtainFreshBeanFactory方法的执行流程
108 0
|
存储
Servlet Session基本概念和使用方法
Session是Web开发中的一种机制,用于在服务器端跟踪和管理用户的状态信息。它允许服务器在用户访问网站期间存储和检索与特定用户相关的数据。 当用户访问服务器时,服务器会为每个用户创建一个唯一的会话,并为该会话分配一个唯一的会话标识符(Session ID)。这个会话标识符通常通过Cookie在客户端保存,但也可以通过URL参数或其他方式传递。通过会话标识符,服务器能够识别特定用户的请求,并在会话中存储和检索数据。 通过使用Session,服务器可以在用户的整个访问过程中保持用户状态,并且可以在不同的页面和请求之间共享数据。这对于实现用户认证、数据共享、购物车管理等功能非常有用。 需要注意的
158 0
|
前端开发 JavaScript Java
java 基于SpringBoot Session拦截器实现登陆功能
java 基于SpringBoot Session拦截器实现登陆功能
150 0