前阵子对shiro进行分布式环境下的改造时跟了一遍源码,当时只是使用了思维带图简要的记录了一下方法的调用过程。最近有空了决定用博客详细的记录分析一下这个流程,以帮助自己更好的理解。
###配置
首先看看shiro在web.xml文件中的配置
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
shiroFilter
/*
可以看到使用的
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// If no target bean name specified, use filter name.
//如果没有delegate则根据去Spring容器中寻找对应的bean
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
于是Spring中应该配置了name为shiroFilter的bean,下面看看Spring中与shiro相关的配置
/user/loginpage = anon
/user/login = anon
/* = authc
/user/perms1 = perms["user:delete"]
/user/perms2 = perms["user:select"]
/user/admin = roles["admin"]
#自定义的过滤器,只要多个权限中有一个满足即可
/user/users = rolesOr["admin","user"]
ShiroFilterFactoryBean的源码这里不进行讨论,先看看ShiroFilterFactoryBean.class中生成ShiroFilter的createInstance()方法
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
spring的DelegatingFilterProxy由此获得了对AbstractShiroFilter的代理。下面我们在DelegatingFilterProxy的doFilter方法上打上断点,跟踪shiro在一次登录请求中都会做哪些处理。
1.入口:invokeDelegate(delegateToUse, request, response, filterChain)
// Let the delegate perform the actual doFilter operation.
2.接着执行OncePerRequestFilter的doFilter方法
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());
filterChain.doFilter(request, response);
} else //noinspection deprecation
if (/* added in 1.2: */ !isEnabled(request, response) ||
/* retain backwards compatibility: */ shouldNotFilter(request) ) {
log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",
getName());
filterChain.doFilter(request, response);
} else {
// Do invoke this filter...
log.trace("Filter '{}' not yet executed. Executing now.", getName());
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(request, response, filterChain);
} finally {
// Once the request has finished, we're done and we don't
// need to mark as 'already filtered' any more.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
可以看到这个方法会先判断请求是否是过滤的了,在最后一个分支调用了doFilterInternal(request, response, filterChain);这个方法,我们跟进方法中看看。
3.断点跳进了AbstractShiroFilter中,观察这个类,发现他继承了OncePerRequestFilter 并重写了其中的doFilterInternal
public abstract class AbstractShiroFilter extends OncePerRequestFilter
接下来看看doFilterInternal中的逻辑
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
主要的逻辑就是创建一个subject,并在创建完成后异步执行一个callable任务用于更新 updateSessionLastAccessTime。接下里看看subject的创建过程。
4.createSubject()
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
}
使用建造者模式建造了一个WebSubject对象,继续跟进
Builder的构造方法
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
super(securityManager);
if (request == null) {
throw new IllegalArgumentException("ServletRequest argument cannot be null.");
}
if (response == null) {
throw new IllegalArgumentException("ServletResponse argument cannot be null.");
}
setRequest(request);
setResponse(response);
}
build方法
public WebSubject buildWebSubject() {
//调用父类的buildSubject()
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);
}
return (WebSubject) subject;
}
可以看到WebSubject的build方法最终调用了父类的buildSubject方法,跟进这个方法。
5.跟进父类Subject的buildSubject方法
public Subject buildSubject() {
return this.securityManager.createSubject(this.subjectContext);
}
发现调用的是securityManager的createSubject方法,继续跟进
public Subject createSubject(SubjectContext subjectContext) {
//create a copy so we don't modify the argument's backing map:
SubjectContext context = copy(subjectContext);
//ensure that the context has a SecurityManager instance, and if not, add one:
context = ensureSecurityManager(context);
//Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
//sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the
//process is often environment specific - better to shield the SF from these details:
context = resolveSession(context);
//Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
//if possible before handing off to the SubjectFactory:
context = resolvePrincipals(context);
Subject subject = doCreateSubject(context);
//save this subject for future reference if necessary:
//(this is needed here in case rememberMe principals were resolved and they need to be stored in the
//session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
//Added in 1.2:
save(subject);
return subject;
}
方法的的作用注释已经说明的很清楚,我们需要注意的是**doCreateSubject(context)**这个方法,securityManger通过这个方法根据传入的subjectContext构建了一个Subject对象。
protected Subject doCreateSubject(SubjectContext context) {
return getSubjectFactory().createSubject(context);
}
跟进发现securityManger使用了内部的subjectFactoy对象进行subject的创建。
public Subject createSubject(SubjectContext context) {
if (!(context instanceof WebSubjectContext)) {
return super.createSubject(context);
}
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);
}
方法首先对传入的SubjectContext的类型做了一个判断,我们只关心wsc的情况。发现方法通过wsc获取了shiro框架中一系列重要对象如principal,session后构建了一个WebDelegatingSubject对象。先看看resolveSession这个方法。
6.SecurityManaget中的resolveSession
protected SubjectContext resolveSession(SubjectContext context) {
if (context.resolveSession() != null) {
log.debug("Context already contains a session. Returning.");
return context;
}
try {
//Context couldn't resolve it directly, let's see if we can since we have direct access to
//the session manager:
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " +
"(session-less) Subject instance.", e);
}
return context;
}
首先试图从SubjectContext中获取session,因此让我跟进一下这个方法:
sc中的resolveSession方法
//SubjectContext.class
public Session resolveSession() {
Session session = getSession();
if (session == null) {
//try the Subject if it exists:
Subject existingSubject = getSubject();
if (existingSubject != null) {
session = existingSubject.getSession(false);
}
}
return session;
}
首先会检查subjectContext中的session是否是null。因为此时session还没有与sc做绑定因此getSession方法必定返回null,跳入第二个分支试图从与sc绑定的subject中获取,同理此时subject也为null,因此return session 返回的一定是一个null对象。让我们回到securityManager中的resolveSession方法,接下来会执行session为null的那一个分支。即
try {
//Context couldn't resolve it directly, let's see if we can since we have direct access to
//the session manager:
Session session = resolveContextSession(context);
if (session != null) {
context.setSession(session);
}
} catch (InvalidSessionException e) {
log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " +
"(session-less) Subject instance.", e);
}
这个代码片段。通过resolveContextSession(context)方法获取session并在获取成功之后与context进行绑定(因此接下来如果再调用这个方法可以直接走从Context获取的分支)。于是我们分析的重点就转移到了**resolveContextSession(context)**这个方法上。
7.resolveContextSession(context)
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
SessionKey key = getSessionKey(context);
if (key != null) {
return getSession(key);
}
return null;
}
这里通过context获取了一个新的对象SessionKey,只有一个方法getSessionId(),通过注释可以得知通过SessionKey可以找到唯一指定的session。弄清楚SessionKey的作用后我们开始分析getSessionKey方法
//DefaultWebSecurityManager.class
protected SessionKey getSessionKey(SubjectContext context) {
//首先判断是否是web环境
if (WebUtils.isWeb(context)) {
//获取sessionId
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);
}
}
context.getSessionId()
public Serializable getSessionId() {
return getTypedValue(SESSION_ID, Serializable.class);
}
从context中根据键SESSION_ID进行取值,因为还没有进行设置因此返回的sessionId为null。
getSessionKey最终构造了一个WebSessionKey对象并返回。因此resolveContextSession方法走入执行getSession(key)方法的分支。
getSession(key)
//SessionSecurityManager
public Session getSession(SessionKey key) throws SessionException {
return this.sessionManager.getSession(key);
}
继续跟进SessionManager的getSession(key)
//AbstractNativeSessionManager.class
public Session getSession(SessionKey key) throws SessionException {
//首先根据key寻找session
Session session = lookupSession(key);
return session != null ? createExposedSession(session, key) : null;
}
lookupSession(key)
private Session lookupSession(SessionKey key) throws SessionException {
if (key == null) {
throw new NullPointerException("SessionKey argument cannot be null.");
}
return doGetSession(key);
}
doGetSession(key)
@Override
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;
}
重点关注retrieveSession(key);这个方法,使用该方法获取session后使用validate方法校验后即可返回。
retrieveSession(key)
//这里使用的是我在CustomeSessionManager中覆写的retrieveSession
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//通过SessionKey对象获取sessionId
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey){
request = ((WebSessionKey)sessionKey).getServletRequest();
}
//尝试从request中取而不是每次都请求数据库
if(request !=null && sessionId !=null){
Session session = (Session) request.getAttribute(sessionId.toString());
if(session != null){
return session;
}
}
//如果request中没有session则从数据库中请求并把请求结果设置给sessionKey
Session session = super.retrieveSession(sessionKey);
if(request !=null && sessionId != null){
request.setAttribute(sessionId.toString(),session);
}
return session;
}
首先关注getSessionId(sessionKey)这个方法,因为到目前为止我们的sessionKey对象中的sessionId属性仍然是空的。
getSessionId(sessionKey)
//DefaultWebSessionManager.class
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 = getSessionId(request, response);
}
return id;
}
super.getSessionId(key);的逻辑很简单,就是获取传入sessionKey的sessionId属性,获取到的当然是空值因此走入if分支。从sessionKey中获取request和response对象然后通过getSessionId(request, response)方法生成sessionId。
getSessionId(request, response)
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
return getReferencedSessionId(request, response);
}
getReferencedSessionId(request, response)
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
//试图从cookie中获取sessionId(这里已经可以看出shiro的session实现原理也是基于cookie的)
String id = getSessionIdCookieValue(request, response);
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
} else {//cookie被禁用的情况,使用url后缀裹挟sessionId的方式实现session
//not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
//try the URI path segment parameters first:
id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
if (id == null) {
//not a URI path segment parameter, try the query parameters:
String name = getSessionIdName();
id = request.getParameter(name);
if (id == null) {
//try lowercase:
id = request.getParameter(name.toLowerCase());
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
}
}
if (id != null) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
//automatically mark it valid here. If it is invalid, the
//onUnknownSession method below will be invoked and we'll remove the attribute at that time.
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
}
// always set rewrite flag - SHIRO-361
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
return id;
}
getSessionIdCookieValue()
private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
if (!isSessionIdCookieEnabled()) {
log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
return null;
}
if (!(request instanceof HttpServletRequest)) {
log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null.");
return null;
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
}
readValue()
//SimpleCookie.class
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
String name = getName();
String value = null;
javax.servlet.http.Cookie cookie = getCookie(request, name);
if (cookie != null) {
// Validate that the cookie is used at the correct place.
String path = StringUtils.clean(getPath());
if (path != null && !pathMatches(path, request.getRequestURI())) {
log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path});
} else {
value = cookie.getValue();
log.debug("Found '{}' cookie value [{}]", name, value);
}
} else {
log.trace("No '{}' cookie value", name);
}
return value;
}
readValue()的逻辑很清晰不多做解释
此时结果一番折腾终于获得了sessionId,让我们回到retrieveSession方法继续往下执行
Session session = super.retrieveSession(sessionKey);
super.retrieveSession(sessionKey)
//DefaultWebSessionManager.class
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//虽然此时sessionKey中的sessionId依然是null但由于我们在
//getReferencedSessionId方法中获取到sessionId后将sessionId存在了
//request对象中,因此sessionId的值是从request中获取的
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;
}
//根据sessionId去数据源取相应的session
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;
}
这里需要重视 Session s = retrieveSessionFromDataSource(sessionId)这句代码,通过sessionId去相应的数据源获取对应的session,跟进一下。
retrieveSessionFromDataSource(sessionId)
protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
return sessionDAO.readSession(sessionId);
}
可以看出调用了SessionDAO的readSession方法,由于sessionDAO是可以自由定义与替换的,所以我们可以根据实际场景更换相应的SessionDao。那么到这了就取得了session。可以看到shiro框架内部自身实现了一套session机制,因此shiro的session是可以脱离web容器使用的。
###总结
我们从一次请求开始简单分析了shiro框架对于session的处理流程,下一篇博客准备以同样的模式分析shiro对于身份验证以及权限认证的处理流程。(最近比较懒散更新时间待定哈哈)。