001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.web.session.mgt;
020
021import org.apache.shiro.session.ExpiredSessionException;
022import org.apache.shiro.session.InvalidSessionException;
023import org.apache.shiro.session.Session;
024import org.apache.shiro.session.mgt.DefaultSessionManager;
025import org.apache.shiro.session.mgt.DelegatingSession;
026import org.apache.shiro.session.mgt.SessionContext;
027import org.apache.shiro.session.mgt.SessionKey;
028import org.apache.shiro.web.servlet.Cookie;
029import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
030import org.apache.shiro.web.servlet.ShiroHttpSession;
031import org.apache.shiro.web.servlet.SimpleCookie;
032import org.apache.shiro.web.util.WebUtils;
033import org.slf4j.Logger;
034import org.slf4j.LoggerFactory;
035
036import javax.servlet.ServletRequest;
037import javax.servlet.ServletResponse;
038import javax.servlet.http.HttpServletRequest;
039import javax.servlet.http.HttpServletResponse;
040import java.io.Serializable;
041
042
043/**
044 * Web-application capable {@link org.apache.shiro.session.mgt.SessionManager SessionManager} implementation.
045 *
046 * @since 0.9
047 */
048public class DefaultWebSessionManager extends DefaultSessionManager implements WebSessionManager {
049
050    private static final Logger log = LoggerFactory.getLogger(DefaultWebSessionManager.class);
051
052    private Cookie sessionIdCookie;
053    private boolean sessionIdCookieEnabled;
054    private boolean sessionIdUrlRewritingEnabled;
055
056    public DefaultWebSessionManager() {
057        Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
058        cookie.setHttpOnly(true); //more secure, protects against XSS attacks
059        this.sessionIdCookie = cookie;
060        this.sessionIdCookieEnabled = true;
061        this.sessionIdUrlRewritingEnabled = false;
062    }
063
064    public Cookie getSessionIdCookie() {
065        return sessionIdCookie;
066    }
067
068    @SuppressWarnings({"UnusedDeclaration"})
069    public void setSessionIdCookie(Cookie sessionIdCookie) {
070        this.sessionIdCookie = sessionIdCookie;
071    }
072
073    public boolean isSessionIdCookieEnabled() {
074        return sessionIdCookieEnabled;
075    }
076
077    @SuppressWarnings({"UnusedDeclaration"})
078    public void setSessionIdCookieEnabled(boolean sessionIdCookieEnabled) {
079        this.sessionIdCookieEnabled = sessionIdCookieEnabled;
080    }
081
082    public boolean isSessionIdUrlRewritingEnabled() {
083        return sessionIdUrlRewritingEnabled;
084    }
085
086    @SuppressWarnings({"UnusedDeclaration"})
087    public void setSessionIdUrlRewritingEnabled(boolean sessionIdUrlRewritingEnabled) {
088        this.sessionIdUrlRewritingEnabled = sessionIdUrlRewritingEnabled;
089    }
090
091    private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
092        if (currentId == null) {
093            String msg = "sessionId cannot be null when persisting for subsequent requests.";
094            throw new IllegalArgumentException(msg);
095        }
096        Cookie template = getSessionIdCookie();
097        Cookie cookie = new SimpleCookie(template);
098        String idString = currentId.toString();
099        cookie.setValue(idString);
100        cookie.saveTo(request, response);
101        log.trace("Set session ID cookie for session with id {}", idString);
102    }
103
104    private void removeSessionIdCookie(HttpServletRequest request, HttpServletResponse response) {
105        getSessionIdCookie().removeFrom(request, response);
106    }
107
108    private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
109        if (!isSessionIdCookieEnabled()) {
110            log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
111            return null;
112        }
113        if (!(request instanceof HttpServletRequest)) {
114            log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
115            return null;
116        }
117        HttpServletRequest httpRequest = (HttpServletRequest) request;
118        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
119    }
120
121    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
122
123        String id = getSessionIdCookieValue(request, response);
124        if (id != null) {
125            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
126                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
127        } else {
128            //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):
129
130            //try the URI path segment parameters first:
131            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
132
133            if (id == null && request instanceof HttpServletRequest) {
134                //not a URI path segment parameter, try the query parameters:
135                String name = getSessionIdName();
136                HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
137                String queryString = httpServletRequest.getQueryString();
138                if (queryString != null && queryString.contains(name)) {
139                    id = request.getParameter(name);
140                }
141                if (id == null && queryString != null && queryString.contains(name.toLowerCase())) {
142                    //try lowercase:
143                    id = request.getParameter(name.toLowerCase());
144                }
145            }
146            if (id != null) {
147                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
148                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
149            }
150        }
151        if (id != null) {
152            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
153            //automatically mark it valid here.  If it is invalid, the
154            //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
155            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
156        }
157
158        // always set rewrite flag - SHIRO-361
159        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
160
161        return id;
162    }
163
164    //SHIRO-351
165    //also see http://cdivilly.wordpress.com/2011/04/22/java-servlets-uri-parameters/
166    //since 1.2.2
167    private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) {
168
169        if (!(servletRequest instanceof HttpServletRequest)) {
170            return null;
171        }
172        HttpServletRequest request = (HttpServletRequest)servletRequest;
173        String uri = request.getRequestURI();
174        if (uri == null) {
175            return null;
176        }
177
178        int queryStartIndex = uri.indexOf('?');
179        if (queryStartIndex >= 0) { //get rid of the query string
180            uri = uri.substring(0, queryStartIndex);
181        }
182
183        int index = uri.indexOf(';'); //now check for path segment parameters:
184        if (index < 0) {
185            //no path segment params - return:
186            return null;
187        }
188
189        //there are path segment params, let's get the last one that may exist:
190
191        final String TOKEN = paramName + "=";
192
193        uri = uri.substring(index+1); //uri now contains only the path segment params
194
195        //we only care about the last JSESSIONID param:
196        index = uri.lastIndexOf(TOKEN);
197        if (index < 0) {
198            //no segment param:
199            return null;
200        }
201
202        uri = uri.substring(index + TOKEN.length());
203
204        index = uri.indexOf(';'); //strip off any remaining segment params:
205        if(index >= 0) {
206            uri = uri.substring(0, index);
207        }
208
209        return uri; //what remains is the value
210    }
211
212    //since 1.2.1
213    private String getSessionIdName() {
214        String name = this.sessionIdCookie != null ? this.sessionIdCookie.getName() : null;
215        if (name == null) {
216            name = ShiroHttpSession.DEFAULT_SESSION_ID_NAME;
217        }
218        return name;
219    }
220
221    protected Session createExposedSession(Session session, SessionContext context) {
222        if (!WebUtils.isWeb(context)) {
223            return super.createExposedSession(session, context);
224        }
225        ServletRequest request = WebUtils.getRequest(context);
226        ServletResponse response = WebUtils.getResponse(context);
227        SessionKey key = new WebSessionKey(session.getId(), request, response);
228        return new DelegatingSession(this, key);
229    }
230
231    protected Session createExposedSession(Session session, SessionKey key) {
232        if (!WebUtils.isWeb(key)) {
233            return super.createExposedSession(session, key);
234        }
235
236        ServletRequest request = WebUtils.getRequest(key);
237        ServletResponse response = WebUtils.getResponse(key);
238        SessionKey sessionKey = new WebSessionKey(session.getId(), request, response);
239        return new DelegatingSession(this, sessionKey);
240    }
241
242    /**
243     * Stores the Session's ID, usually as a Cookie, to associate with future requests.
244     *
245     * @param session the session that was just {@link #createSession created}.
246     */
247    @Override
248    protected void onStart(Session session, SessionContext context) {
249        super.onStart(session, context);
250
251        if (!WebUtils.isHttp(context)) {
252            log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " +
253                    "pair. No session ID cookie will be set.");
254            return;
255
256        }
257        HttpServletRequest request = WebUtils.getHttpRequest(context);
258        HttpServletResponse response = WebUtils.getHttpResponse(context);
259
260        if (isSessionIdCookieEnabled()) {
261            Serializable sessionId = session.getId();
262            storeSessionId(sessionId, request, response);
263        } else {
264            log.debug("Session ID cookie is disabled.  No cookie has been set for new session with id {}", session.getId());
265        }
266
267        request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
268        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
269    }
270
271    @Override
272    public Serializable getSessionId(SessionKey key) {
273        Serializable id = super.getSessionId(key);
274        if (id == null && WebUtils.isWeb(key)) {
275            ServletRequest request = WebUtils.getRequest(key);
276            ServletResponse response = WebUtils.getResponse(key);
277            id = getSessionId(request, response);
278        }
279        return id;
280    }
281
282    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
283        return getReferencedSessionId(request, response);
284    }
285
286    @Override
287    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
288        super.onExpiration(s, ese, key);
289        onInvalidation(key);
290    }
291
292    @Override
293    protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) {
294        super.onInvalidation(session, ise, key);
295        onInvalidation(key);
296    }
297
298    private void onInvalidation(SessionKey key) {
299        ServletRequest request = WebUtils.getRequest(key);
300        if (request != null) {
301            request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID);
302        }
303        if (WebUtils.isHttp(key)) {
304            log.debug("Referenced session was invalid.  Removing session ID cookie.");
305            removeSessionIdCookie(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key));
306        } else {
307            log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
308                    "pair. Session ID cookie will not be removed due to invalidated session.");
309        }
310    }
311
312    @Override
313    protected void onStop(Session session, SessionKey key) {
314        super.onStop(session, key);
315        if (WebUtils.isHttp(key)) {
316            HttpServletRequest request = WebUtils.getHttpRequest(key);
317            HttpServletResponse response = WebUtils.getHttpResponse(key);
318            log.debug("Session has been stopped (subject logout or explicit stop).  Removing session ID cookie.");
319            removeSessionIdCookie(request, response);
320        } else {
321            log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " +
322                    "pair. Session ID cookie will not be removed due to stopped session.");
323        }
324    }
325
326    /**
327     * This is a native session manager implementation, so this method returns {@code false} always.
328     *
329     * @return {@code false} always
330     * @since 1.2
331     */
332    public boolean isServletContainerSessions() {
333        return false;
334    }
335}