之前在研究Shiro 源码的过程中,发现Shiro 会对request、response、session 进行包装。 下面研究其包装过程以及原理。
Session是通过包装了request, 重写了其获取Session 的方法。 然后重写了一套Shiro 自己的Session 管理机制(这个session 和 Servlet的HeepSession 没有关系), 只是对外暴露的时候封装成一个ShiroHttpSession 对象(该对象内部包含Shiro 的Session), 最终ShiroHttpSession 相关的操作都会交给Shiro的Session。 Shiro的session 实现了一套自己的生成ID、创建Session、删除、修改、获取所有的等方法; 并且也有定时任务去处理过期的session 等策略。 并且SHiro 提供了可扩展的抽象类,基于抽象类可以快速实现Session 存到Redis 或其他操作。
1. 使用ServletContainerSessionManager 走原来Servlet的一套机制
1. shiro 配置
// 权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中 securityManager.setRealms(Lists.newArrayList(new CustomRealm())); return securityManager; }
2. 增加测试Controller, 查看相关类型
@GetMapping("/login2") public String login2(HttpServletRequest request, HttpServletResponse response, HttpSession session) { System.out.println(request); System.out.println(response); System.out.println(session); System.out.println("华丽的分割线1~~~~"); HttpServletRequest request1 = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); HttpServletResponse response1 = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse(); HttpSession session1 = request1.getSession(false); System.out.println(request1); System.out.println(response1); System.out.println(session1); System.out.println("华丽的分割线2~~~~"); Subject subject = SecurityUtils.getSubject(); AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222"); subject.login(generateToken); return "success"; }
3. shiro 配置该路径允许匿名访问
4. 测试
访问后日志如下:
org.apache.shiro.web.servlet.ShiroHttpServletRequest@7adc6ef1 org.apache.catalina.connector.ResponseFacade@4802cdcd org.apache.catalina.session.StandardSessionFacade@1374f85e 华丽的分割线1~~~~ org.apache.shiro.web.servlet.ShiroHttpServletRequest@7adc6ef1 org.apache.catalina.connector.ResponseFacade@4802cdcd org.apache.catalina.session.StandardSessionFacade@1374f85e 华丽的分割线2~~~~
可以看到默认对Request 进行了包装,Response和sesson 仍然使用原来Servlet 中使用的对象。
5. 原理
1. org.apache.shiro.web.mgt.DefaultWebSecurityManager#DefaultWebSecurityManager() 构造如下:
public DefaultWebSecurityManager() { super(); DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator(); ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(webEvalutator); this.sessionMode = HTTP_SESSION_MODE; setSubjectFactory(new DefaultWebSubjectFactory()); setRememberMeManager(new CookieRememberMeManager()); setSessionManager(new ServletContainerSessionManager()); webEvalutator.setSessionManager(getSessionManager()); }
可以看到默认的sessionManager 是 org.apache.shiro.web.session.mgt.ServletContainerSessionManager:
类图图像:
源码如下:
public class ServletContainerSessionManager implements WebSessionManager { //TODO - complete JavaDoc //TODO - read session timeout value from web.xml public ServletContainerSessionManager() { } public Session start(SessionContext context) throws AuthorizationException { return createSession(context); } public Session getSession(SessionKey key) throws SessionException { if (!WebUtils.isHttp(key)) { String msg = "SessionKey must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(key); Session session = null; HttpSession httpSession = request.getSession(false); if (httpSession != null) { session = createSession(httpSession, request.getRemoteHost()); } return session; } private String getHost(SessionContext context) { String host = context.getHost(); if (host == null) { ServletRequest request = WebUtils.getRequest(context); if (request != null) { host = request.getRemoteHost(); } } return host; } /** * @since 1.0 */ protected Session createSession(SessionContext sessionContext) throws AuthorizationException { if (!WebUtils.isHttp(sessionContext)) { String msg = "SessionContext must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(sessionContext); HttpSession httpSession = request.getSession(); //SHIRO-240: DO NOT use the 'globalSessionTimeout' value here on the acquired session. //see: https://issues.apache.org/jira/browse/SHIRO-240 String host = getHost(sessionContext); return createSession(httpSession, host); } protected Session createSession(HttpSession httpSession, String host) { return new HttpServletSession(httpSession, host); } /** * This implementation always delegates to the servlet container for sessions, so this method returns * {@code true} always. * * @return {@code true} always * @since 1.2 */ public boolean isServletContainerSessions() { return true; } }
2. 代理代码查看:
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 是Shiro的过滤器执行的入口:
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); } }
(1) 包装Request的代码: org.apache.shiro.web.servlet.AbstractShiroFilter#prepareServletRequest
protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) { ServletRequest toUse = request; if (request instanceof HttpServletRequest) { HttpServletRequest http = (HttpServletRequest) request; toUse = wrapServletRequest(http); } return toUse; } protected ServletRequest wrapServletRequest(HttpServletRequest orig) { return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions()); } protected boolean isHttpSessions() { return getSecurityManager().isHttpSessionMode(); }
1》org.apache.shiro.web.servlet.ShiroHttpServletRequest#ShiroHttpServletRequest 构造如下:
public class ShiroHttpServletRequest extends HttpServletRequestWrapper { public static final String COOKIE_SESSION_ID_SOURCE = "cookie"; public static final String URL_SESSION_ID_SOURCE = "url"; public static final String REFERENCED_SESSION_ID = ShiroHttpServletRequest.class.getName() + "_REQUESTED_SESSION_ID"; public static final String REFERENCED_SESSION_ID_IS_VALID = ShiroHttpServletRequest.class.getName() + "_REQUESTED_SESSION_ID_VALID"; public static final String REFERENCED_SESSION_IS_NEW = ShiroHttpServletRequest.class.getName() + "_REFERENCED_SESSION_IS_NEW"; public static final String REFERENCED_SESSION_ID_SOURCE = ShiroHttpServletRequest.class.getName() + "REFERENCED_SESSION_ID_SOURCE"; public static final String IDENTITY_REMOVED_KEY = ShiroHttpServletRequest.class.getName() + "_IDENTITY_REMOVED_KEY"; public static final String SESSION_ID_URL_REWRITING_ENABLED = ShiroHttpServletRequest.class.getName() + "_SESSION_ID_URL_REWRITING_ENABLED"; protected ServletContext servletContext = null; protected HttpSession session = null; protected boolean httpSessions = true; public ShiroHttpServletRequest(HttpServletRequest wrapped, ServletContext servletContext, boolean httpSessions) { super(wrapped); this.servletContext = servletContext; this.httpSessions = httpSessions; }
2》判断isHttpSessions 是否是 httpSession 的方法如下:
org.apache.shiro.web.mgt.DefaultWebSecurityManager#isHttpSessionMode:
public boolean isHttpSessionMode() { SessionManager sessionManager = getSessionManager(); return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions(); }
也就是判断是否是WebSessionManager, 然后调用 isServletContainerSessions 判断。 默认的ServletContainerSessionManager 返回true。 所以是HttpSessionMode。
(2) 包装Response 的代码 org.apache.shiro.web.servlet.AbstractShiroFilter#prepareServletResponse
protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) { ServletResponse toUse = response; if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) && (response instanceof HttpServletResponse)) { //the ShiroHttpServletResponse exists to support URL rewriting for session ids. This is only needed if //using Shiro sessions (i.e. not simple HttpSession based sessions): toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request); } return toUse; }
可以看到包装的条件是: !isHttpSessions() 并且request是 ShiroHttpServletRequest; 并且 response instanceof HttpServletResponse。 第一个条件不满足,所以不会进行包装。
3》 session 进行包装的条件:
createSubject(request, response); 创建Subject, 会进行解析相关session。
(1) 调用到: org.apache.shiro.mgt.DefaultSecurityManager#resolveContextSession
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException { SessionKey key = getSessionKey(context); if (key != null) { return getSession(key); } return null; }
- 调用 org.apache.shiro.web.mgt.DefaultWebSecurityManager#getSessionKey 获取SessionKey
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); } }
第一次获取到的sessionId 为空,返回一个WebSessionKey 对象, sessionId 为空。
- 调用org.apache.shiro.mgt.SessionsSecurityManager#getSession 获取session
public Session getSession(SessionKey key) throws SessionException { return this.sessionManager.getSession(key); }
继续调用调用到:org.apache.shiro.web.session.mgt.ServletContainerSessionManager#getSession
public Session getSession(SessionKey key) throws SessionException { if (!WebUtils.isHttp(key)) { String msg = "SessionKey must be an HTTP compatible implementation."; throw new IllegalArgumentException(msg); } HttpServletRequest request = WebUtils.getHttpRequest(key); Session session = null; HttpSession httpSession = request.getSession(false); if (httpSession != null) { session = createSession(httpSession, request.getRemoteHost()); } return session; } protected Session createSession(HttpSession httpSession, String host) { return new HttpServletSession(httpSession, host); }
继续调用到: org.apache.shiro.web.servlet.ShiroHttpServletRequest#getSession(boolean)
public HttpSession getSession(boolean create) { HttpSession httpSession; if (isHttpSessions()) { httpSession = super.getSession(false); if (httpSession == null && create) { //Shiro 1.2: assert that creation is enabled (SHIRO-266): if (WebUtils._isSessionCreationEnabled(this)) { httpSession = super.getSession(create); } else { throw newNoSessionCreationException(); } } } else { boolean existing = getSubject().getSession(false) != null; if (this.session == null || !existing) { Session shiroSession = getSubject().getSession(create); if (shiroSession != null) { this.session = new ShiroHttpSession(shiroSession, this, this.servletContext); if (!existing) { setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE); } } else if (this.session != null) { this.session = null; } } httpSession = this.session; } return httpSession; }
可以看到isHttpSessions 是true, 所以走上面不包装的代码逻辑。也就是所有的session 都走的原来session的一套机制。
2. 修改Sessionmanager为DefaultWebSessionManager走Shiro的机制
1. 修改配置
@Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中 securityManager.setRealms(Lists.newArrayList(new CustomRealm())); securityManager.setSessionManager(new DefaultWebSessionManager()); return securityManager; }
2. 测试
查看控制台如下:
org.apache.shiro.web.servlet.ShiroHttpServletRequest@675f5183 org.apache.shiro.web.servlet.ShiroHttpServletResponse@6998a318 org.apache.shiro.web.servlet.ShiroHttpSession@6da73af3 华丽的分割线1~~~~ org.apache.shiro.web.servlet.ShiroHttpServletRequest@675f5183 org.apache.shiro.web.servlet.ShiroHttpServletResponse@6998a318 org.apache.shiro.web.servlet.ShiroHttpSession@6da73af3 华丽的分割线2~~~~
可以看到对request、response、session 都进行了包装。查看相关对象如下:
(1) request:
(2) response:
(3) session:
4. 包装原理
1. org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 在Filter 内部, Shiro 对request、response 进行了包装。
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); } }
prepareServletRequest 包装request; prepareServletResponse 包装response。 因为org.apache.shiro.web.session.mgt.DefaultWebSessionManager#isServletContainerSessions 返回是false, 所以取后是true。 那么满足包装的条件。
protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) { ServletResponse toUse = response; if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) && (response instanceof HttpServletResponse)) { //the ShiroHttpServletResponse exists to support URL rewriting for session ids. This is only needed if //using Shiro sessions (i.e. not simple HttpSession based sessions): toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request); } return toUse; }
2. 进行后续调用 executeChain(request, response, chain); 用的都是包装后的request, response。
3. 过滤器执行完后会进入Servlet 调用, 调用SpringMVC 的方法org.springframework.web.servlet.DispatcherServlet#doService 之前会先进入其父类 org.springframework.web.servlet.FrameworkServlet#processRequest:
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long startTime = System.currentTimeMillis(); Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContext localeContext = buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor()); initContextHolders(request, localeContext, requestAttributes); try { doService(request, response); } catch (ServletException | IOException ex) { failureCause = ex; throw ex; } catch (Throwable ex) { failureCause = ex; throw new NestedServletException("Request processing failed", ex); } finally { resetContextHolders(request, previousLocaleContext, previousAttributes); if (requestAttributes != null) { requestAttributes.requestCompleted(); } logResult(request, response, failureCause, asyncManager); publishRequestHandledEvent(request, response, startTime, failureCause); } }
1》 调用 buildRequestAttributes(request, response, previousAttributes); 将Request、Response 维护起来, 这里的request和response 对象都是包装后的对象。
2》 org.springframework.web.servlet.FrameworkServlet#initContextHolders 将上面的对象保存到ThreadLocal 中, 也就是当前环境使用的request、response 以及传递到SpringMVC、Controller 的request、response 都是shiro 包装后的对象。
private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) { if (localeContext != null) { LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable); } if (requestAttributes != null) { RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); } }
RequestContextHolder 是Spring 记录请求上下文环境的对象,用的也是ThreadLocal 对象进行维护。
3. session管理
修改为DefaultWebSessionManager 之后,实际就是将session 交给shiro 管理。
1. 类图:
2. 重要类
1. org.apache.shiro.web.session.mgt.DefaultWebSessionManager#DefaultWebSessionManager 构造如下:
public DefaultWebSessionManager() { Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME); cookie.setHttpOnly(true); //more secure, protects against XSS attacks this.sessionIdCookie = cookie; this.sessionIdCookieEnabled = true; this.sessionIdUrlRewritingEnabled = true; }
sessionIdCookie 默认是读取名称为JSESSIONID 的cookie。
父类构造: org.apache.shiro.session.mgt.DefaultSessionManager#DefaultSessionManager 可以看到创建了一个 MemorySessionDAO
public DefaultSessionManager() { this.deleteInvalidSessions = true; this.sessionFactory = new SimpleSessionFactory(); this.sessionDAO = new MemorySessionDAO(); }
涉及到的重要的类如下:
(1) org.apache.shiro.session.mgt.SimpleSessionFactory 源码如下: 简单创建一个session
package org.apache.shiro.session.mgt; import org.apache.shiro.session.Session; /** * {@code SessionFactory} implementation that generates {@link SimpleSession} instances. * * @since 1.0 */ public class SimpleSessionFactory implements SessionFactory { /** * Creates a new {@link SimpleSession SimpleSession} instance retaining the context's * {@link SessionContext#getHost() host} if one can be found. * * @param initData the initialization data to be used during {@link Session} creation. * @return a new {@link SimpleSession SimpleSession} instance */ public Session createSession(SessionContext initData) { if (initData != null) { String host = initData.getHost(); if (host != null) { return new SimpleSession(host); } } return new SimpleSession(); } }
(2) org.apache.shiro.session.mgt.eis.SessionDAO 是一个接口,提供了session 的 增删改查
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import java.io.Serializable; import java.util.Collection; /** * Data Access Object design pattern specification to enable {@link Session} access to an * EIS (Enterprise Information System). It provides your four typical CRUD methods: * {@link #create}, {@link #readSession(java.io.Serializable)}, {@link #update(org.apache.shiro.session.Session)}, * and {@link #delete(org.apache.shiro.session.Session)}. * <p/> * The remaining {@link #getActiveSessions()} method exists as a support mechanism to pre-emptively orphaned sessions, * typically by {@link org.apache.shiro.session.mgt.ValidatingSessionManager ValidatingSessionManager}s), and should * be as efficient as possible, especially if there are thousands of active sessions. Large scale/high performance * implementations will often return a subset of the total active sessions and perform validation a little more * frequently, rather than return a massive set and infrequently validate. * * @since 0.1 */ public interface SessionDAO { /** * Inserts a new Session record into the underling EIS (e.g. Relational database, file system, persistent cache, * etc, depending on the DAO implementation). * <p/> * After this method is invoked, the {@link org.apache.shiro.session.Session#getId()} * method executed on the argument must return a valid session identifier. That is, the following should * always be true: * <pre> * Serializable id = create( session ); * id.equals( session.getId() ) == true</pre> * <p/> * Implementations are free to throw any exceptions that might occur due to * integrity violation constraints or other EIS related errors. * * @param session the {@link org.apache.shiro.session.Session} object to create in the EIS. * @return the EIS id (e.g. primary key) of the created {@code Session} object. */ Serializable create(Session session); /** * Retrieves the session from the EIS uniquely identified by the specified * {@code sessionId}. * * @param sessionId the system-wide unique identifier of the Session object to retrieve from * the EIS. * @return the persisted session in the EIS identified by {@code sessionId}. * @throws UnknownSessionException if there is no EIS record for any session with the * specified {@code sessionId} */ Session readSession(Serializable sessionId) throws UnknownSessionException; /** * Updates (persists) data from a previously created Session instance in the EIS identified by * {@code {@link Session#getId() session.getId()}}. This effectively propagates * the data in the argument to the EIS record previously saved. * <p/> * In addition to UnknownSessionException, implementations are free to throw any other * exceptions that might occur due to integrity violation constraints or other EIS related * errors. * * @param session the Session to update * @throws org.apache.shiro.session.UnknownSessionException * if no existing EIS session record exists with the * identifier of {@link Session#getId() session.getSessionId()} */ void update(Session session) throws UnknownSessionException; /** * Deletes the associated EIS record of the specified {@code session}. If there never * existed a session EIS record with the identifier of * {@link Session#getId() session.getId()}, then this method does nothing. * * @param session the session to delete. */ void delete(Session session); /** * Returns all sessions in the EIS that are considered active, meaning all sessions that * haven't been stopped/expired. This is primarily used to validate potential orphans. * <p/> * If there are no active sessions in the EIS, this method may return an empty collection or {@code null}. * <h4>Performance</h4> * This method should be as efficient as possible, especially in larger systems where there might be * thousands of active sessions. Large scale/high performance * implementations will often return a subset of the total active sessions and perform validation a little more * frequently, rather than return a massive set and validate infrequently. If efficient and possible, it would * make sense to return the oldest unstopped sessions available, ordered by * {@link org.apache.shiro.session.Session#getLastAccessTime() lastAccessTime}. * <h4>Smart Results</h4> * <em>Ideally</em> this method would only return active sessions that the EIS was certain should be invalided. * Typically that is any session that is not stopped and where its lastAccessTimestamp is older than the session * timeout. * <p/> * For example, if sessions were backed by a relational database or SQL-92 'query-able' enterprise cache, you might * return something similar to the results returned by this query (assuming * {@link org.apache.shiro.session.mgt.SimpleSession SimpleSession}s were being stored): * <pre> * select * from sessions s where s.lastAccessTimestamp < ? and s.stopTimestamp is null * </pre> * where the {@code ?} parameter is a date instance equal to 'now' minus the session timeout * (e.g. now - 30 minutes). * * @return a Collection of {@code Session}s that are considered active, or an * empty collection or {@code null} if there are no active sessions. */ Collection<Session> getActiveSessions(); }
org.apache.shiro.session.mgt.eis.AbstractSessionDAO 抽象dao:
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SimpleSession; import java.io.Serializable; /** * An abstract {@code SessionDAO} implementation that performs some sanity checks on session creation and reading and * allows for pluggable Session ID generation strategies if desired. The {@code SessionDAO} * {@link SessionDAO#update update} and {@link SessionDAO#delete delete} methods are left to * subclasses. * <h3>Session ID Generation</h3> * This class also allows for plugging in a {@link SessionIdGenerator} for custom ID generation strategies. This is * optional, as the default generator is probably sufficient for most cases. Subclass implementations that do use a * generator (default or custom) will want to call the * {@link #generateSessionId(org.apache.shiro.session.Session)} method from within their {@link #doCreate} * implementations. * <p/> * Subclass implementations that rely on the EIS data store to generate the ID automatically (e.g. when the session * ID is also an auto-generated primary key), they can simply ignore the {@code SessionIdGenerator} concept * entirely and just return the data store's ID from the {@link #doCreate} implementation. * * @since 1.0 */ public abstract class AbstractSessionDAO implements SessionDAO { /** * Optional SessionIdGenerator instance available to subclasses via the * {@link #generateSessionId(org.apache.shiro.session.Session)} method. */ private SessionIdGenerator sessionIdGenerator; /** * Default no-arg constructor that defaults the {@link #setSessionIdGenerator sessionIdGenerator} to be a * {@link org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator}. */ public AbstractSessionDAO() { this.sessionIdGenerator = new JavaUuidSessionIdGenerator(); } /** * Returns the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)} * method. Unless overridden by the {@link #setSessionIdGenerator(SessionIdGenerator)} method, the default instance * is a {@link JavaUuidSessionIdGenerator}. * * @return the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)} * method. */ public SessionIdGenerator getSessionIdGenerator() { return sessionIdGenerator; } /** * Sets the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)} * method. Unless overridden by this method, the default instance ss a {@link JavaUuidSessionIdGenerator}. * * @param sessionIdGenerator the {@code SessionIdGenerator} to use in the * {@link #generateSessionId(org.apache.shiro.session.Session)} method. */ public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) { this.sessionIdGenerator = sessionIdGenerator; } /** * Generates a new ID to be applied to the specified {@code session} instance. This method is usually called * from within a subclass's {@link #doCreate} implementation where they assign the returned id to the session * instance and then create a record with this ID in the EIS data store. * <p/> * Subclass implementations backed by EIS data stores that auto-generate IDs during record creation, such as * relational databases, don't need to use this method or the {@link #getSessionIdGenerator() sessionIdGenerator} * attribute - they can simply return the data store's generated ID from the {@link #doCreate} implementation * if desired. * <p/> * This implementation uses the {@link #setSessionIdGenerator configured} {@link SessionIdGenerator} to create * the ID. * * @param session the new session instance for which an ID will be generated and then assigned * @return the generated ID to assign */ protected Serializable generateSessionId(Session session) { if (this.sessionIdGenerator == null) { String msg = "sessionIdGenerator attribute has not been configured."; throw new IllegalStateException(msg); } return this.sessionIdGenerator.generateId(session); } /** * Creates the session by delegating EIS creation to subclasses via the {@link #doCreate} method, and then * asserting that the returned sessionId is not null. * * @param session Session object to create in the EIS and associate with an ID. */ public Serializable create(Session session) { Serializable sessionId = doCreate(session); verifySessionId(sessionId); return sessionId; } /** * Ensures the sessionId returned from the subclass implementation of {@link #doCreate} is not null and not * already in use. * * @param sessionId session id returned from the subclass implementation of {@link #doCreate} */ private void verifySessionId(Serializable sessionId) { if (sessionId == null) { String msg = "sessionId returned from doCreate implementation is null. Please verify the implementation."; throw new IllegalStateException(msg); } } /** * Utility method available to subclasses that wish to * assign a generated session ID to the session instance directly. This method is not used by the * {@code AbstractSessionDAO} implementation directly, but it is provided so subclasses don't * need to know the {@code Session} implementation if they don't need to. * <p/> * This default implementation casts the argument to a {@link SimpleSession}, Shiro's default EIS implementation. * * @param session the session instance to which the sessionId will be applied * @param sessionId the id to assign to the specified session instance. */ protected void assignSessionId(Session session, Serializable sessionId) { ((SimpleSession) session).setId(sessionId); } /** * Subclass hook to actually persist the given <tt>Session</tt> instance to the underlying EIS. * * @param session the Session instance to persist to the EIS. * @return the id of the session created in the EIS (i.e. this is almost always a primary key and should be the * value returned from {@link org.apache.shiro.session.Session#getId() Session.getId()}. */ protected abstract Serializable doCreate(Session session); /** * Retrieves the Session object from the underlying EIS identified by <tt>sessionId</tt> by delegating to * the {@link #doReadSession(java.io.Serializable)} method. If {@code null} is returned from that method, an * {@link UnknownSessionException} will be thrown. * * @param sessionId the id of the session to retrieve from the EIS. * @return the session identified by <tt>sessionId</tt> in the EIS. * @throws UnknownSessionException if the id specified does not correspond to any session in the EIS. */ public Session readSession(Serializable sessionId) throws UnknownSessionException { Session s = doReadSession(sessionId); if (s == null) { throw new UnknownSessionException("There is no session with id [" + sessionId + "]"); } return s; } /** * Subclass implementation hook that retrieves the Session object from the underlying EIS or {@code null} if a * session with that ID could not be found. * * @param sessionId the id of the <tt>Session</tt> to retrieve. * @return the Session in the EIS identified by <tt>sessionId</tt> or {@code null} if a * session with that ID could not be found. */ protected abstract Session doReadSession(Serializable sessionId); }
下面有两个具体的实现:
org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO 和 org.apache.shiro.session.mgt.eis.MemorySessionDAO:(默认用的这个DAO) - 可以看到session默认是存在自己内部的map 中
package org.apache.shiro.session.mgt.eis; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * Simple memory-based implementation of the SessionDAO that stores all of its sessions in an in-memory * {@link ConcurrentMap}. <b>This implementation does not page to disk and is therefore unsuitable for applications * that could experience a large amount of sessions</b> and would therefore cause {@code OutOfMemoryException}s. It is * <em>not</em> recommended for production use in most environments. * <h2>Memory Restrictions</h2> * If your application is expected to host many sessions beyond what can be stored in the * memory available to the JVM, it is highly recommended to use a different {@code SessionDAO} implementation which * uses a more expansive or permanent backing data store. * <p/> * In this case, it is recommended to instead use a custom * {@link CachingSessionDAO} implementation that communicates with a higher-capacity data store of your choice * (file system, database, etc). * <h2>Changes in 1.0</h2> * This implementation prior to 1.0 used to subclass the {@link CachingSessionDAO}, but this caused problems with many * cache implementations that would expunge entries due to TTL settings, resulting in Sessions that would be randomly * (and permanently) lost. The Shiro 1.0 release refactored this implementation to be 100% memory-based (without * {@code Cache} usage to avoid this problem. * * @see CachingSessionDAO * @since 0.1 */ public class MemorySessionDAO extends AbstractSessionDAO { private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class); private ConcurrentMap<Serializable, Session> sessions; public MemorySessionDAO() { this.sessions = new ConcurrentHashMap<Serializable, Session>(); } protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); storeSession(sessionId, session); return sessionId; } protected Session storeSession(Serializable id, Session session) { if (id == null) { throw new NullPointerException("id argument cannot be null."); } return sessions.putIfAbsent(id, session); } protected Session doReadSession(Serializable sessionId) { return sessions.get(sessionId); } public void update(Session session) throws UnknownSessionException { storeSession(session.getId(), session); } public void delete(Session session) { if (session == null) { throw new NullPointerException("session argument cannot be null."); } Serializable id = session.getId(); if (id != null) { sessions.remove(id); } } public Collection<Session> getActiveSessions() { Collection<Session> values = sessions.values(); if (CollectionUtils.isEmpty(values)) { return Collections.emptySet(); } else { return Collections.unmodifiableCollection(values); } } }
(3) 关于session 实现定时任务处理:
org.apache.shiro.session.mgt.ValidatingSessionManager 接口如下:
package org.apache.shiro.session.mgt; /** * A ValidatingSessionManager is a SessionManager that can proactively validate any or all sessions * that may be expired. * * @since 0.1 */ public interface ValidatingSessionManager extends SessionManager { /** * Performs session validation for all open/active sessions in the system (those that * have not been stopped or expired), and validates each one. If a session is * found to be invalid (e.g. it has expired), it is updated and saved to the EIS. * <p/> * This method is necessary in order to handle orphaned sessions and is expected to be run at * a regular interval, such as once an hour, once a day or once a week, etc. * The "best" frequency to run this method is entirely dependent upon the application * and would be based on factors such as performance, average number of active users, hours of * least activity, and other things. * <p/> * Most enterprise applications use a request/response programming model. * This is obvious in the case of web applications due to the HTTP protocol, but it is * equally true of remote client applications making remote method invocations. The server * essentially sits idle and only "works" when responding to client requests and/or * method invocations. This type of model is particularly efficient since it means the * security system only has to validate a session during those cases. Such * "lazy" behavior enables the system to lie stateless and/or idle and only incur * overhead for session validation when necessary. * <p/> * However, if a client forgets to log-out, or in the event of a server failure, it is * possible for sessions to be orphaned since no further requests would utilize that session. * Because of these lower-probability cases, it might be required to regularly clean-up the sessions * maintained by the system, especially if sessions are backed by a persistent data store. * <p/> * Even in applications that aren't primarily based on a request/response model, * such as those that use enterprise asynchronous messaging (where data is pushed to * a client without first receiving a client request), it is almost always acceptable to * utilize this lazy approach and run this method at defined interval. * <p/> * Systems that want to proactively validate individual sessions may simply call the * {@link #getSession(SessionKey) getSession(SessionKey)} method on any * {@code ValidatingSessionManager} instance as that method is expected to * validate the session before retrieving it. Note that even with proactive calls to {@code getSession}, * this {@code validateSessions()} method should be invoked regularly anyway to <em>guarantee</em> no * orphans exist. * <p/> * <b>Note:</b> Shiro supports automatic execution of this method at a regular interval * by using {@link SessionValidationScheduler}s. The Shiro default SecurityManager implementations * needing session validation will create and use one by default if one is not provided by the * application configuration. */ void validateSessions(); }
org.apache.shiro.session.mgt.AbstractValidatingSessionManager#validateSessions 方法如下:
/** * @see ValidatingSessionManager#validateSessions() */ public void validateSessions() { if (log.isInfoEnabled()) { log.info("Validating all active sessions..."); } int invalidCount = 0; Collection<Session> activeSessions = getActiveSessions(); if (activeSessions != null && !activeSessions.isEmpty()) { for (Session s : activeSessions) { try { //simulate a lookup key to satisfy the method signature. //this could probably stand to be cleaned up in future versions: SessionKey key = new DefaultSessionKey(s.getId()); validate(s, key); } catch (InvalidSessionException e) { if (log.isDebugEnabled()) { boolean expired = (e instanceof ExpiredSessionException); String msg = "Invalidated session with id [" + s.getId() + "]" + (expired ? " (expired)" : " (stopped)"); log.debug(msg); } invalidCount++; } } } if (log.isInfoEnabled()) { String msg = "Finished session validation."; if (invalidCount > 0) { msg += " [" + invalidCount + "] sessions were stopped."; } else { msg += " No sessions were stopped."; } log.info(msg); } } protected void validate(Session session, SessionKey key) throws InvalidSessionException { try { doValidate(session); } catch (ExpiredSessionException ese) { onExpiration(session, ese, key); throw ese; } catch (InvalidSessionException ise) { onInvalidation(session, ise, key); throw ise; } }
在org.apache.shiro.session.mgt.AbstractValidatingSessionManager#createSession 方法会检测是否开启validation 的定时任务。
protected Session createSession(SessionContext context) throws AuthorizationException { enableSessionValidationIfNecessary(); return doCreateSession(context); } private void enableSessionValidationIfNecessary() { SessionValidationScheduler scheduler = getSessionValidationScheduler(); if (isSessionValidationSchedulerEnabled() && (scheduler == null || !scheduler.isEnabled())) { enableSessionValidation(); } } protected synchronized void enableSessionValidation() { SessionValidationScheduler scheduler = getSessionValidationScheduler(); if (scheduler == null) { scheduler = createSessionValidationScheduler(); setSessionValidationScheduler(scheduler); } // it is possible that that a scheduler was already created and set via 'setSessionValidationScheduler()' // but would not have been enabled/started yet if (!scheduler.isEnabled()) { if (log.isInfoEnabled()) { log.info("Enabling session validation scheduler..."); } scheduler.enableSessionValidation(); afterSessionValidationEnabled(); } } protected SessionValidationScheduler createSessionValidationScheduler() { ExecutorServiceSessionValidationScheduler scheduler; if (log.isDebugEnabled()) { log.debug("No sessionValidationScheduler set. Attempting to create default instance."); } scheduler = new ExecutorServiceSessionValidationScheduler(this); scheduler.setInterval(getSessionValidationInterval()); if (log.isTraceEnabled()) { log.trace("Created default SessionValidationScheduler instance of type [" + scheduler.getClass().getName() + "]."); } return scheduler; }
这里就是创建了一个定时任务, 然后调用 org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler#enableSessionValidation:
public void enableSessionValidation() { if (this.interval > 0l) { this.service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { private final AtomicInteger count = new AtomicInteger(1); public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setName(threadNamePrefix + count.getAndIncrement()); return thread; } }); this.service.scheduleAtFixedRate(this, interval, interval, TimeUnit.MILLISECONDS); } this.enabled = true; }
3. Session 创建以及处理
1. 创建:
org.apache.shiro.web.servlet.ShiroHttpServletRequest#getSession(boolean) 开始创建
public HttpSession getSession(boolean create) { HttpSession httpSession; if (isHttpSessions()) { httpSession = super.getSession(false); if (httpSession == null && create) { //Shiro 1.2: assert that creation is enabled (SHIRO-266): if (WebUtils._isSessionCreationEnabled(this)) { httpSession = super.getSession(create); } else { throw newNoSessionCreationException(); } } } else { boolean existing = getSubject().getSession(false) != null; if (this.session == null || !existing) { Session shiroSession = getSubject().getSession(create); if (shiroSession != null) { this.session = new ShiroHttpSession(shiroSession, this, this.servletContext); if (!existing) { setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE); } } else if (this.session != null) { this.session = null; } } httpSession = this.session; } return httpSession; }
create 为true, 所以走org.apache.shiro.subject.support.DelegatingSubject#getSession(boolean)
(1) 继续调用org.apache.shiro.subject.support.DelegatingSubject#getSession(boolean):
public Session getSession(boolean create) { if (log.isTraceEnabled()) { log.trace("attempting to get session; create = " + create + "; session is null = " + (this.session == null) + "; session has id = " + (this.session != null && session.getId() != null)); } if (this.session == null && create) { //added in 1.2: if (!isSessionCreationEnabled()) { String msg = "Session creation has been disabled for the current subject. This exception indicates " + "that there is either a programming error (using a session when it should never be " + "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " + "for the current Subject. See the " + DisabledSessionException.class.getName() + " JavaDoc " + "for more."; throw new DisabledSessionException(msg); } log.trace("Starting session for host {}", getHost()); SessionContext sessionContext = createSessionContext(); Session session = this.securityManager.start(sessionContext); this.session = decorate(session); } return this.session; }
调用 org.apache.shiro.mgt.SessionsSecurityManager#start: 创建session
public Session start(SessionContext context) throws AuthorizationException { return this.sessionManager.start(context); }
继续调用到:org.apache.shiro.session.mgt.AbstractNativeSessionManager#start (创建完session 之后使用全局失效时间,然后进行包装一下返回)
public Session start(SessionContext context) { Session session = createSession(context); applyGlobalSessionTimeout(session); onStart(session, context); notifyStart(session); //Don't expose the EIS-tier Session object to the client-tier: return createExposedSession(session, context); }
1》org.apache.shiro.session.mgt.AbstractValidatingSessionManager#createSession:
protected Session createSession(SessionContext context) throws AuthorizationException { // 开启定时验证任务 enableSessionValidationIfNecessary(); return doCreateSession(context); }
org.apache.shiro.session.mgt.DefaultSessionManager#doCreateSession:
protected Session doCreateSession(SessionContext context) { Session s = newSessionInstance(context); if (log.isTraceEnabled()) { log.trace("Creating session for host {}", s.getHost()); } create(s); return s; }
调用 newSessionInstance 调用到org.apache.shiro.session.mgt.SimpleSessionFactory#createSession 创建session
public Session createSession(SessionContext initData) { if (initData != null) { String host = initData.getHost(); if (host != null) { return new SimpleSession(host); } } return new SimpleSession(); }
然后调用org.apache.shiro.session.mgt.DefaultSessionManager#create 创建:
protected void create(Session session) { if (log.isDebugEnabled()) { log.debug("Creating new EIS record for new session instance [" + session + "]"); } sessionDAO.create(session); }
然后调用: org.apache.shiro.session.mgt.eis.AbstractSessionDAO#create
public Serializable create(Session session) { Serializable sessionId = doCreate(session); verifySessionId(sessionId); return sessionId; }
然后调用org.apache.shiro.session.mgt.eis.MemorySessionDAO#doCreate 创建:(生成ID, 赋值, 存储)
protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); storeSession(sessionId, session); return sessionId; }
2》 org.apache.shiro.web.session.mgt.DefaultWebSessionManager#createExposedSession(org.apache.shiro.session.Session, org.apache.shiro.session.mgt.SessionContext) 包装方法如下:
protected Session createExposedSession(Session session, SessionContext context) { if (!WebUtils.isWeb(context)) { return super.createExposedSession(session, context); } ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); SessionKey key = new WebSessionKey(session.getId(), request, response); return new DelegatingSession(this, key); }
3》 org.apache.shiro.subject.support.DelegatingSubject#decorate 装饰:
protected Session decorate(Session session) { if (session == null) { throw new IllegalArgumentException("session cannot be null"); } return new StoppingAwareProxiedSession(session, this); }
(2) 包装成 org.apache.shiro.web.servlet.ShiroHttpSession#ShiroHttpSession:
public ShiroHttpSession(Session session, HttpServletRequest currentRequest, ServletContext servletContext) { if (session instanceof HttpServletSession) { String msg = "Session constructor argument cannot be an instance of HttpServletSession. This is enforced to " + "prevent circular dependencies and infinite loops."; throw new IllegalArgumentException(msg); } this.session = session; this.currentRequest = currentRequest; this.servletContext = servletContext; }
(3) 创建完成之后设置到当前request 的内部属性中, 后续通过request 获取的session 都是上面创建且装饰的ShiroHttpSession 对象。
总结:
1. 可以看到。 Shiro 是有一套自己的session 机制。最后将自己的session 封装成ShiroHttpSession 转换为HttpSession。
2. shiro 的session 的类图如下, 该Session 接口与Servlet 中的HttpSession 没有关系:
3. org.apache.shiro.web.servlet.ShiroHttpSession 是对外暴露的的HttpSession, 其实现了接口javax.servlet.http.HttpSession。 源码如下:
package org.apache.shiro.web.servlet; import org.apache.shiro.session.InvalidSessionException; import org.apache.shiro.session.Session; import org.apache.shiro.web.session.HttpServletSession; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import java.util.*; /** * Wrapper class that uses a Shiro {@link Session Session} under the hood for all session operations instead of the * Servlet Container's session mechanism. This is required in heterogeneous client environments where the Session * is used on both the business tier as well as in multiple client technologies (web, swing, flash, etc) since * Servlet container sessions alone cannot support this feature. * * @since 0.2 */ public class ShiroHttpSession implements HttpSession { //TODO - complete JavaDoc public static final String DEFAULT_SESSION_ID_NAME = "JSESSIONID"; private static final Enumeration EMPTY_ENUMERATION = new Enumeration() { public boolean hasMoreElements() { return false; } public Object nextElement() { return null; } }; @SuppressWarnings({"deprecation"}) private static final javax.servlet.http.HttpSessionContext HTTP_SESSION_CONTEXT = new javax.servlet.http.HttpSessionContext() { public HttpSession getSession(String s) { return null; } public Enumeration getIds() { return EMPTY_ENUMERATION; } }; protected ServletContext servletContext = null; protected HttpServletRequest currentRequest = null; protected Session session = null; //'real' Shiro Session public ShiroHttpSession(Session session, HttpServletRequest currentRequest, ServletContext servletContext) { if (session instanceof HttpServletSession) { String msg = "Session constructor argument cannot be an instance of HttpServletSession. This is enforced to " + "prevent circular dependencies and infinite loops."; throw new IllegalArgumentException(msg); } this.session = session; this.currentRequest = currentRequest; this.servletContext = servletContext; } public Session getSession() { return this.session; } public long getCreationTime() { try { return getSession().getStartTimestamp().getTime(); } catch (Exception e) { throw new IllegalStateException(e); } } public String getId() { return getSession().getId().toString(); } public long getLastAccessedTime() { return getSession().getLastAccessTime().getTime(); } public ServletContext getServletContext() { return this.servletContext; } public void setMaxInactiveInterval(int i) { try { getSession().setTimeout(i * 1000L); } catch (InvalidSessionException e) { throw new IllegalStateException(e); } } public int getMaxInactiveInterval() { try { return (new Long(getSession().getTimeout() / 1000)).intValue(); } catch (InvalidSessionException e) { throw new IllegalStateException(e); } } @SuppressWarnings({"deprecation"}) public javax.servlet.http.HttpSessionContext getSessionContext() { return HTTP_SESSION_CONTEXT; } public Object getAttribute(String s) { try { return getSession().getAttribute(s); } catch (InvalidSessionException e) { throw new IllegalStateException(e); } } public Object getValue(String s) { return getAttribute(s); } @SuppressWarnings({"unchecked"}) protected Set<String> getKeyNames() { Collection<Object> keySet; try { keySet = getSession().getAttributeKeys(); } catch (InvalidSessionException e) { throw new IllegalStateException(e); } Set<String> keyNames; if (keySet != null && !keySet.isEmpty()) { keyNames = new HashSet<String>(keySet.size()); for (Object o : keySet) { keyNames.add(o.toString()); } } else { keyNames = Collections.EMPTY_SET; } return keyNames; } public Enumeration getAttributeNames() { Set<String> keyNames = getKeyNames(); final Iterator iterator = keyNames.iterator(); return new Enumeration() { public boolean hasMoreElements() { return iterator.hasNext(); } public Object nextElement() { return iterator.next(); } }; } public String[] getValueNames() { Set<String> keyNames = getKeyNames(); String[] array = new String[keyNames.size()]; if (keyNames.size() > 0) { array = keyNames.toArray(array); } return array; } protected void afterBound(String s, Object o) { if (o instanceof HttpSessionBindingListener) { HttpSessionBindingListener listener = (HttpSessionBindingListener) o; HttpSessionBindingEvent event = new HttpSessionBindingEvent(this, s, o); listener.valueBound(event); } } protected void afterUnbound(String s, Object o) { if (o instanceof HttpSessionBindingListener) { HttpSessionBindingListener listener = (HttpSessionBindingListener) o; HttpSessionBindingEvent event = new HttpSessionBindingEvent(this, s, o); listener.valueUnbound(event); } } public void setAttribute(String s, Object o) { try { getSession().setAttribute(s, o); afterBound(s, o); } catch (InvalidSessionException e) { //noinspection finally try { afterUnbound(s, o); } finally { //noinspection ThrowFromFinallyBlock throw new IllegalStateException(e); } } } public void putValue(String s, Object o) { setAttribute(s, o); } public void removeAttribute(String s) { try { Object attribute = getSession().removeAttribute(s); afterUnbound(s, attribute); } catch (InvalidSessionException e) { throw new IllegalStateException(e); } } public void removeValue(String s) { removeAttribute(s); } public void invalidate() { try { getSession().stop(); } catch (InvalidSessionException e) { throw new IllegalStateException(e); } } public boolean isNew() { Boolean value = (Boolean) currentRequest.getAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW); return value != null && value.equals(Boolean.TRUE); } }
2. 获取
1. org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 入口会创建Subject
2. 调用org.apache.shiro.session.mgt.DefaultSessionManager#retrieveSession :
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; }
1》 调用到: org.apache.shiro.web.session.mgt.DefaultWebSessionManager#getReferencedSessionId 实际也是从cookie 中拿名字为JSESSIONID的值作为sessionId
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { String id = getSessionIdCookieValue(request, response); if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); } else { //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; }
2》 retrieveSessionFromDataSource 调用到: org.apache.shiro.session.mgt.eis.MemorySessionDAO#doReadSession
protected Session doReadSession(Serializable sessionId) { return sessions.get(sessionId); }
实际就是从缓存Map 中获取数据。
3. 如果获取的时候没获取到,那么session 为空, 如果获取之后就调用 org.apache.shiro.session.mgt.AbstractValidatingSessionManager#validate 进行验证
protected void validate(Session session, SessionKey key) throws InvalidSessionException { try { doValidate(session); } catch (ExpiredSessionException ese) { onExpiration(session, ese, key); throw ese; } catch (InvalidSessionException ise) { onInvalidation(session, ise, key); throw ise; } }
继续调用: org.apache.shiro.session.mgt.SimpleSession#validate
public void validate() throws InvalidSessionException { //check for stopped: if (isStopped()) { //timestamp is set, so the session is considered stopped: String msg = "Session with id [" + getId() + "] has been " + "explicitly stopped. No further interaction under this session is " + "allowed."; throw new StoppedSessionException(msg); } //check for expiration if (isTimedOut()) { expire(); //throw an exception explaining details of why it expired: Date lastAccessTime = getLastAccessTime(); long timeout = getTimeout(); Serializable sessionId = getId(); DateFormat df = DateFormat.getInstance(); String msg = "Session with id [" + sessionId + "] has expired. " + "Last access time: " + df.format(lastAccessTime) + ". Current time: " + df.format(new Date()) + ". Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" + timeout / MILLIS_PER_MINUTE + " minutes)"; if (log.isTraceEnabled()) { log.trace(msg); } throw new ExpiredSessionException(msg); } }
可以看到session 的过期也是用每次访问固定续期,基于 org.apache.shiro.session.mgt.SimpleSession#lastAccessTime 与当前时间进行比多。
4. 修改session 存放到redis
在上面简单了解到Session 交给Shiro 管理之后,所有的操作都是通过SessionDAO 接口进行的,如果我们想redis 存到redis 中, 只需要重写一个SessionDAO, 其中 AbstractSessionDAO 作为一个抽象类,我们继承该类实现几个抽象方法即可。
1. pom 引入:
<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>3.2.2</version> </dependency>
2. 重新设置SessionManager
@Bean public SessionManager sessionManager() { DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager(); RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); RedisManager redisManager = new RedisManager(); redisManager.setHost("127.0.0.1:6379"); redisSessionDAO.setRedisManager(redisManager); defaultWebSessionManager.setSessionDAO(redisSessionDAO); return defaultWebSessionManager; }
3. 测试: 访问后查看redis:
127.0.0.1:6379> keys *
1) "shiro:session:66745c77-625b-4c74-8799-2f9db8c8fe47"
127.0.0.1:6379> type "shiro:session:66745c77-625b-4c74-8799-2f9db8c8fe47"
string
127.0.0.1:6379> get "shiro:session:66745c77-625b-4c74-8799-2f9db8c8fe47"
"xacxedx00x05srx00*org.apache.shiro.session.mgt.SimpleSessionx9dx1cxa1xb8xd5x8cbnx03x00x00xpwx02x00xdbtx00$66745c77-625b-4c74-8799-2f9db8c8fe47srx00x0ejava.util.Datehjx81x01KYtx19x03x00x00xpwx00x00x01|xd6xd0x05x1bxqx00~x00x04wx19x00x00x00x00x00x1bw@x00x0f0:0:0:0:0:0:0:1srx00x11java.util.HashMapx05axdaxc1xc3x16`xd1x03x00x02Fx00
loadFactorIx00 thresholdxp?@x00x00x00x00x00x0cwx00x00x00x10x00x00x00x03tx00x04testtx00x06value2tx00Porg.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEYsrx00x11java.lang.Booleanxcd rx80xd5x9cxfaxeex02x00x01Zx00x05valuexpx01tx00Morg.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEYsrx002org.apache.shiro.subject.SimplePrincipalCollectionxa8x7fX%xc6xa3Jx03x00x01Lx00x0frealmPrincipalstx00x0fLjava/util/Map;xpsrx00x17java.util.LinkedHashMap4xc0N\x10lxc0xfbx02x00x01Zx00x0baccessOrderxqx00~x00x05?@x00x00x00x00x00x0cwx00x00x00x10x00x00x00x01tx00$com.zd.bx.config.shiro.CustomRealm_0srx00x17java.util.LinkedHashSetxd8lxd7Zx95xdd*x1ex02x00x00xrx00x11java.util.HashSetxbaDx85x95x96xb8xb74x03x00x00xpwx0cx00x00x00x10?@x00x00x00x00x00x01srx00x18com.zd.bx.bean.user.Userxa19zxfeMxa09xb2x02x00x10Lx00aaddresstx00x12Ljava/lang/String;Lx00x0bdepartmentstx00x0fLjava/util/Set;Lx00
dingUserIdqx00~x00x17Lx00x05emailqx00~x00x17Lx00fullnameqx00~x00x17Lx00numIndextx00x13Ljava/lang/Integer;Lx00passwordqx00~x00x17Lx00x05phoneqx00~x00x17Lx00x05rolesqx00~x00x18Lx00x1aselectDeptAndPositionNodesqx00~x00x18Lx00x03sexqx00~x00x17Lx00
updatetimetx00x10Ljava/util/Date;Lx00 userblankqx00~x00x17Lx00usercodeqx00~x00x17Lx00usernameqx00~x00x17Lx00x0cweixinUserIdqx00~x00x17xrx00%com.zd.bx.bean.AbstractSequenceEntityxf6xa2xa9xec`xc5x01xb2x02x00x01Jx00x02idxrx00x1dcom.zd.bx.bean.AbstractEntityx03rxadxf5xbbx04xcaxa1x02x00x03Lx00
createtimeqx00~x00x1aLx00acreatorqx00~x00x17Lx00
uniqueCodeqx00~x00x17xpsqx00~x00x03wx00x00x01|xd6xd0x05 xtx00x00tx00$31f53619-75f9-4aa2-86ce-22b518319a74x00x00x00x00x00x00x00x00psqx00~x00x14wx0cx00x00x00x10?@x00x00x00x00x00x00xpppsrx00x11java.lang.Integerx12xe2xa0xa4xf7x81x878x02x00x01Ix00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x03xe7tx00x06111222psqx00~x00x14wx0cx00x00x00x10?@x00x00x00x00x00x00xsqx00~x00x14wx0cx00x00x00x10?@x00x00x00x00x00x00xppppppxxx00wx01x01qx00~x00x11xxx"
4. 源码查看:
(1) org.crazycake.shiro.RedisManager:
package org.crazycake.shiro; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Protocol; public class RedisManager extends WorkAloneRedisManager implements IRedisManager { private static final String DEFAULT_HOST = "127.0.0.1:6379"; private String host = DEFAULT_HOST; // timeout for jedis try to connect to redis server, not expire time! In milliseconds private int timeout = Protocol.DEFAULT_TIMEOUT; private String password; private int database = Protocol.DEFAULT_DATABASE; private JedisPool jedisPool; private void init() { synchronized (this) { if (jedisPool == null) { String[] hostAndPort = host.split(":"); jedisPool = new JedisPool(getJedisPoolConfig(), hostAndPort[0], Integer.parseInt(hostAndPort[1]), timeout, password, database); } } } @Override protected Jedis getJedis() { if (jedisPool == null) { init(); } return jedisPool.getResource(); } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getDatabase() { return database; } public void setDatabase(int database) { this.database = database; } public JedisPool getJedisPool() { return jedisPool; } public void setJedisPool(JedisPool jedisPool) { this.jedisPool = jedisPool; } }
(2) org.crazycake.shiro.RedisSessionDAO
package org.crazycake.shiro; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; import org.crazycake.shiro.exception.SerializationException; import org.crazycake.shiro.serializer.ObjectSerializer; import org.crazycake.shiro.serializer.RedisSerializer; import org.crazycake.shiro.serializer.StringSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.*; public class RedisSessionDAO extends AbstractSessionDAO { private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class); private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:"; private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX; private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 1000L; /** * doReadSession be called about 10 times when login. * Save Session in ThreadLocal to resolve this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal. * The default value is 1000 milliseconds (1s). * Most of time, you don't need to change it. */ private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT; private static final boolean DEFAULT_SESSION_IN_MEMORY_ENABLED = true; private boolean sessionInMemoryEnabled = DEFAULT_SESSION_IN_MEMORY_ENABLED; // expire time in seconds private static final int DEFAULT_EXPIRE = -2; private static final int NO_EXPIRE = -1; /** * Please make sure expire is longer than sesion.getTimeout() */ private int expire = DEFAULT_EXPIRE; private static final int MILLISECONDS_IN_A_SECOND = 1000; private IRedisManager redisManager; private RedisSerializer keySerializer = new StringSerializer(); private RedisSerializer valueSerializer = new ObjectSerializer(); private static ThreadLocal sessionsInThread = new ThreadLocal(); @Override public void update(Session session) throws UnknownSessionException { this.saveSession(session); if (this.sessionInMemoryEnabled) { this.setSessionToThreadLocal(session.getId(), session); } } /** * save session * @param session * @throws UnknownSessionException */ private void saveSession(Session session) throws UnknownSessionException { if (session == null || session.getId() == null) { logger.error("session or session id is null"); throw new UnknownSessionException("session or session id is null"); } byte[] key; byte[] value; try { key = keySerializer.serialize(getRedisSessionKey(session.getId())); value = valueSerializer.serialize(session); } catch (SerializationException e) { logger.error("serialize session error. session id=" + session.getId()); throw new UnknownSessionException(e); } if (expire == DEFAULT_EXPIRE) { this.redisManager.set(key, value, (int) (session.getTimeout() / MILLISECONDS_IN_A_SECOND)); return; } if (expire != NO_EXPIRE && expire * MILLISECONDS_IN_A_SECOND < session.getTimeout()) { logger.warn("Redis session expire time: " + (expire * MILLISECONDS_IN_A_SECOND) + " is less than Session timeout: " + session.getTimeout() + " . It may cause some problems."); } this.redisManager.set(key, value, expire); } @Override public void delete(Session session) { if (session == null || session.getId() == null) { logger.error("session or session id is null"); return; } try { redisManager.del(keySerializer.serialize(getRedisSessionKey(session.getId()))); } catch (SerializationException e) { logger.error("delete session error. session id=" + session.getId()); } } @Override public Collection<Session> getActiveSessions() { Set<Session> sessions = new HashSet<Session>(); try { Set<byte[]> keys = redisManager.keys(this.keySerializer.serialize(this.keyPrefix + "*")); if (keys != null && keys.size() > 0) { for (byte[] key:keys) { Session s = (Session) valueSerializer.deserialize(redisManager.get(key)); sessions.add(s); } } } catch (SerializationException e) { logger.error("get active sessions error."); } return sessions; } @Override protected Serializable doCreate(Session session) { if (session == null) { logger.error("session is null"); throw new UnknownSessionException("session is null"); } Serializable sessionId = this.generateSessionId(session); this.assignSessionId(session, sessionId); this.saveSession(session); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { if (sessionId == null) { logger.warn("session id is null"); return null; } if (this.sessionInMemoryEnabled) { Session session = getSessionFromThreadLocal(sessionId); if (session != null) { return session; } } Session session = null; logger.debug("read session from redis"); try { session = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(getRedisSessionKey(sessionId)))); if (this.sessionInMemoryEnabled) { setSessionToThreadLocal(sessionId, session); } } catch (SerializationException e) { logger.error("read session error. settionId=" + sessionId); } return session; } private void setSessionToThreadLocal(Serializable sessionId, Session s) { Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get(); if (sessionMap == null) { sessionMap = new HashMap<Serializable, SessionInMemory>(); sessionsInThread.set(sessionMap); } SessionInMemory sessionInMemory = new SessionInMemory(); sessionInMemory.setCreateTime(new Date()); sessionInMemory.setSession(s); sessionMap.put(sessionId, sessionInMemory); } private Session getSessionFromThreadLocal(Serializable sessionId) { Session s = null; if (sessionsInThread.get() == null) { return null; } Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get(); SessionInMemory sessionInMemory = sessionMap.get(sessionId); if (sessionInMemory == null) { return null; } Date now = new Date(); long duration = now.getTime() - sessionInMemory.getCreateTime().getTime(); if (duration < sessionInMemoryTimeout) { s = sessionInMemory.getSession(); logger.debug("read session from memory"); } else { sessionMap.remove(sessionId); } return s; } private String getRedisSessionKey(Serializable sessionId) { return this.keyPrefix + sessionId; } public IRedisManager getRedisManager() { return redisManager; } public void setRedisManager(IRedisManager redisManager) { this.redisManager = redisManager; } public String getKeyPrefix() { return keyPrefix; } public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } public RedisSerializer getKeySerializer() { return keySerializer; } public void setKeySerializer(RedisSerializer keySerializer) { this.keySerializer = keySerializer; } public RedisSerializer getValueSerializer() { return valueSerializer; } public void setValueSerializer(RedisSerializer valueSerializer) { this.valueSerializer = valueSerializer; } public long getSessionInMemoryTimeout() { return sessionInMemoryTimeout; } public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) { this.sessionInMemoryTimeout = sessionInMemoryTimeout; } public int getExpire() { return expire; } public void setExpire(int expire) { this.expire = expire; } public boolean getSessionInMemoryEnabled() { return sessionInMemoryEnabled; } public void setSessionInMemoryEnabled(boolean sessionInMemoryEnabled) { this.sessionInMemoryEnabled = sessionInMemoryEnabled; } }
5. 前后端分离项目session标识从header中获取
有的时候我们可能在做前后端分离项目时,生成的token信息可能从head中传递。这时候我们可以复用这一套流程。也就是修改session获取方式。
原来获取是从org.apache.shiro.web.session.mgt.DefaultWebSessionManager#getSessionId(javax.servlet.ServletRequest, javax.servlet.ServletResponse) 为入口,从cookie 中获取key 为JSESSION的值。所以如果想从自己的header 获取,重写该方法即可。这样还可以复用shiro的逻辑。
登录成功之后前端每次请求在自己的请求头携带名称为mytoken的header,值为登录成功时的SESSIONID。
package com.zd.bx.config.shiro; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; public class MySessionManager extends DefaultWebSessionManager { @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { HttpServletRequest request1 = (HttpServletRequest) request; String mytoken = request1.getHeader("mytoken"); System.out.println(mytoken); return mytoken; } }
然后切换sessionManager 为上面Manager 即可。
补充: 如果session 交给session 管理,可以看到其失效时间也是根据最后一次访问时间递推。
修改lastAccessTime 是在:org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 入口调用org.apache.shiro.web.servlet.AbstractShiroFilter#updateSessionLastAccessTime:
protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) { if (!isHttpSessions()) { //'native' sessions Subject subject = SecurityUtils.getSubject(); //Subject should never _ever_ be null, but just in case: if (subject != null) { Session session = subject.getSession(false); if (session != null) { try { session.touch(); } catch (Throwable t) { log.error("session.touch() method invocation has failed. Unable to update " + "the corresponding session's last access time based on the incoming request.", t); } } } } }
在 !isHttpSessions 的条件下(该条件下session 由shiro 管理), 调用: org.apache.shiro.session.mgt.SimpleSession#touch
public void touch() { this.lastAccessTime = new Date(); }
补充: shiro 管理session 后相关相关属性存放原理
1. 如下方法:
session1.setAttribute("test", "value2");
调用到: org.apache.shiro.web.servlet.ShiroHttpSession#setAttribute
public void setAttribute(String s, Object o) { try { getSession().setAttribute(s, o); afterBound(s, o); } catch (InvalidSessionException e) { //noinspection finally try { afterUnbound(s, o); } finally { //noinspection ThrowFromFinallyBlock throw new IllegalStateException(e); } } }
继续调用: org.apache.shiro.session.ProxiedSession#setAttribute
public void setAttribute(Object key, Object value) throws InvalidSessionException { delegate.setAttribute(key, value); }
继续调用:org.apache.shiro.session.mgt.DelegatingSession#setAttribute
public void setAttribute(Object attributeKey, Object value) throws InvalidSessionException { if (value == null) { removeAttribute(attributeKey); } else { sessionManager.setAttribute(this.key, attributeKey, value); } }
继续调用: org.apache.shiro.session.mgt.AbstractNativeSessionManager#setAttribute
public void setAttribute(SessionKey sessionKey, Object attributeKey, Object value) throws InvalidSessionException { if (value == null) { removeAttribute(sessionKey, attributeKey); } else { Session s = lookupRequiredSession(sessionKey); s.setAttribute(attributeKey, value); onChange(s); } }
继续调用: org.apache.shiro.session.mgt.SimpleSession#setAttribute (也就是最终相关属性存放的是在一个Map中)
private transient Map<Object, Object> attributes; public void setAttribute(Object key, Object value) { if (value == null) { removeAttribute(key); } else { getAttributesLazy().put(key, value); } } private Map<Object, Object> getAttributesLazy() { Map<Object, Object> attributes = getAttributes(); if (attributes == null) { attributes = new HashMap<Object, Object>(); setAttributes(attributes); } return attributes; }
2. 最终经过上面调用放到了org.apache.shiro.session.mgt.SimpleSession#attributes 属性中。 org.apache.shiro.session.mgt.SimpleSession 会存放在内存中, 所以每个 JSESSIONID 对应的Session 放的东西不会丢失。