Cookie和Session

前言

最近在看Tomcat的源码,然后想着就把Servlet的相关知识都复习一下吧。自然就需要关注Cookie和Session了。

Response 操作Cookie的方法就一个 addCookie()
通常我们添加Cookie的操作如下

Cookie cookie=new Cookie("name","kobe");
response.addCookie(cookie);

通常我们这样操作只设置了Cookie的name value
但是Cookie还是有很多其他的属性的。
name String 该Cookie的名称。Cookie一旦创建,名称便不可更改
value Object 该Cookie的值。如果值为Unicode字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64编码
maxAge int 该Cookie失效的时间,单位秒。如果为正数,则该Cookie在maxAge秒之后失效。如果为负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。如果为0,表示删除该Cookie。默认为–1
secure boolean 该Cookie是否仅被使用安全协议传输。安全协议。安全协议有HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false
path String 该Cookie的使用路径。如果设置为“/sessionWeb/”,则只有contextPath为“/sessionWeb”的程序可以访问该Cookie。如果设置为“/”,则本域名下contextPath都可以访问该Cookie。注意最后一个字符必须为“/”
那我们默认没有显式指定的时候是什么呢?
这个就跟我们在哪里添加Cookie有关。
localhost:8080/mywebapp/testcookie/test1.jsp
比如我在/mywebapp/testcookie/test1.jsp中添加的Cookie,没有显示指定path
那么这里的path默认就是/mywebapp/testcookie/
即默认情况下,只有与创建 cookie 的页面在同一个目录或子目录下的网页才可以访问,这个是因为安全方面的考虑,造成不是所有页面都可以随意访问其他页面创建的 cookie。
domain String
可以访问该Cookie的域名。如果设置为“.google.com”,则所有以“google.com”结尾的域名都可以访问该Cookie。注意第一个字符必须为“.”
那么如果没有显示的设置呢?
如网址为www.jb51.net/test/test.aspx,那么domain默认为www.jb51.net。
而跨域访问,如域A为t1.test.com,域B为t2.test.com,那么在域A生产一个令域A和域B都能访问的cookie就要将该cookie的domain设置为.test.com;如果要在域A生产一个令域A不能访问而域B能访问的cookie就要将该cookie的domain设置为t2.test.com。

comment String
该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明
version int 该Cookie使用的版本号。0表示遵循Netscape的Cookie规范,1表示遵循W3C的RFC
2109规范

cookie有大小限制 4kb
同个网站可以创建多个 cookie ,而多个 cookie 可以存放在同一个cookie 文件中。

介绍了这么多关于Cookie的知识,但是还没有介绍Cookie的应用,我们需要很好的掌握基础知识,但是同样的,我们更需要知道怎样去应用这些知识,学以致用才是我们追求的。

很明显,当没有Cookie的时候,如果我们登陆一个网站,而网站的每一个页面都需要用户名密码验证的时候,我们需要在每次提交访问页面的请求的时候都需要显式的带上用户名和密码等消息。(也就是说用户名和密码必须是请求参数),这显然是很糟糕的体验,所以Cookie就出现了,他解决了这一问题。
有了Cookie之后可以这么做呢,第一次登陆页面的请求过来,请求参数里面必然是有用户名和密码的。那么我们在服务器端(以Java语言 web容器:Tomcat 为例),将用户名和密码的信息添加到Cookie当中,使用response.addCookie添加。然后将信息存入数据库中。
你观看Tomcat的源码:org.apache.catalina.connector.Response就可以看到最后Cookie的信息会存到Set-Cookie这个响应头字段中。
然后Cookie信息就会到浏览器中了,这个时候浏览器会根据maxAge即Cookie的有效时间来判断是否需要存放在本地磁盘中,如果maxAge没有在服务器端进行显示的设置,即默认的设置为-1,那么这个Cookie就会随着浏览器窗口的关闭而失效。而如果是一个d大于0的maxAge,就会将其存放到本地磁盘中。(不同的浏览器的存放位置不同),而不同domain或者path的Cookie都会存放在不同的Cookie文件中,而两者都相同的Cookie会存在同一文件中。
那么这里很明显我们需要设置一个显式的maxAge,单位为秒,如果希望密码保存一星期则

cookie.setMaxAge(60*60*24*7);

这时,当我们第二次访问该网站的页面的时候,在我们的请求头里就会出现Cookie的相关信息。
会在请求头的Cookie字段中。(因为domain和path都符合,所以我们存在本地磁盘的用户名和密码cookie会出现在请求头中),这时候我们在服务器端再需要验证登录是否合法的时候,就不需要把用户名和密码显示的拿过来了,因为已经在Cookie中带过来了,那么我们就可以在服务器端中找到相应的Cookie信息,然后再去与数据库中的信息比较,看是否有效。这样就避免了每次都要输入用户名和密码。

但是很明显,如果你不是一个开发小白,你就知道上述的设计存在很多问题。
1.直接把密码放入到Cookie中也太不严谨了把,cookie信息可是可以直接用js获取的,是没有任何安全保障的。
2.是否真的需要每次去把cookie中的信息放到数据库(特指传统的关系型数据库)中去比较。

对于第一点,也就是安全性,相对于第二点的性能,这是我们急需用解决的。
解决方法:可能你一开始就会想到,之前不是说了cookie有一个属性叫做secure,你在设置cookie的时候把他设置成true不就好了么,这样他就只能在HTTPS,SSL等安全协议下传输了。但是他也只能保证 cookie 与服务器之间的数据传输过程加密,而保存在本地的 cookie文件并不加密。所以如果想让本地cookie也加密,就得自己加密数据。
所以,我们不仅用使用安全传输协议,还需要将cookie信息进行加密。

我们进行如下改进,第一次请求时,我们依然会把用户名和密码存入数据库中,或者查询,看是否已经有这个用户,这是跟数据库的第一次交互。
然后我们使用一个密钥将用户名加密,将加密后的密文和用户名分别放入相应的cookie中。而在之后的请求中,服务器端会将用户名再次用密钥进行加密,然后与cookie中的密文进行比较。这样就避免了多次访问数据库以及暴露密码的问题。

Session

在每个context对象,即每个web app都具有一个独立的manager对象。通过server.xml可以配置定制化的manager,也可以不配置。不管怎样,在生成context对象时,都会生成一个manager对象。缺省的是StandardManager类。相应的代码在ContextRuleSet的addRuleInstances方法中。涉及到Digester解析XML的知识,这方面的知识要赶紧啃了。

        digester.addObjectCreate(prefix + "Context/Manager",
                                 "org.apache.catalina.session.StandardManager",
                                 "className");
        digester.addSetProperties(prefix + "Context/Manager");
        digester.addSetNext(prefix + "Context/Manager",
                            "setManager",
                            "org.apache.catalina.Manager");

        digester.addObjectCreate(prefix + "Context/Manager/Store",
                                 null, // MUST be specified in the element
                                 "className");
        digester.addSetProperties(prefix + "Context/Manager/Store");
        digester.addSetNext(prefix + "Context/Manager/Store",
                            "setStore",
                            "org.apache.catalina.Store");

        digester.addObjectCreate(prefix + "Context/Manager/SessionIdGenerator",
                                 "org.apache.catalina.util.StandardSessionIdGenerator",
                                 "className");
        digester.addSetProperties(prefix + "Context/Manager/SessionIdGenerator");
        digester.addSetNext(prefix + "Context/Manager/SessionIdGenerator",
                            "setSessionIdGenerator",
                            "org.apache.catalina.SessionIdGenerator");

第一次请求过来是不会带有Cookie的,也就不会有jsessionId出现。Session其实也是一种Cookie。请求过来后,服务器端就会为这个请求的客户机加上Cookie作为标识了。

browser发送Http request;
tomcat内核Http11Processor会从HTTP request中解析出“jsessionid”(具体的解析过程为先从request的URL中解析,这是为了有的浏览器把cookie功能禁止后,将URL重写考虑的,如果解析不出来,再从cookie中解析相应的jsessionid),解析完后封装成一个request对象(当然还有其他的http header);
servlet中获取session,其过程是根据刚才解析得到的jsessionid(如果有的话),从session池(session maps)中获取相应的session对象;这个地方有个逻辑,就是如果jsessionid为空的话(或者没有其对应的session对象,或者有session对象,但此对象已经过期超时),可以选择创建一个session,或者不创建;
如果创建新session,则将session放入session池中,同时将与其相对应的jsessionid写入cookie通过Http response header的方式发送给browser,然后重复第一步。

以上是session的获取及创建过程。在servlet中获取session,通常是调用request的getSession方法。这个方法需要传入一个boolean参数,这个参数就是实现刚才说的,当jsessionid为空或从session池中获取不到相应的session对象时,选择创建一个新的session还是不创建。


    protected Session doGetSession(boolean create) {

        // There cannot be a session if no context has been assigned yet
        Context context = getContext();
        if (context == null) {
            return (null);
        }
        //如果有session存在但是无效
        // Return the current session if it exists and is valid
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        //如果有效,直接返回
        if (session != null) {
            return (session);
        }
        //       获取所在context的manager对象   

        // Return the requested session if it exists and is valid
        Manager manager = context.getManager();
        if (manager == null) {
            return null;        // Sessions are not supported
        }
        //requestedSessionId就是从Http request中解析出来的  
        if (requestedSessionId != null) {
            try {
                session = manager.findSession(requestedSessionId);
            } catch (IOException e) {
                session = null;
            }
            //如果存在但无效
            if ((session != null) && !session.isValid()) {
                session = null;
            }
            //如果有效,返回
            if (session != null) {
                //session对象有效,记录此次访问时间  

                session.access();
                return (session);
            }
        }
        // 如果参数是false,则不创建新session对象了,直接退出了  

        // Create a new session if requested and the response is not committed
        if (!create) {
            return (null);
        }
        if ((response != null) &&
            context.getServletContext().getEffectiveSessionTrackingModes().
                    contains(SessionTrackingMode.COOKIE) &&
            response.getResponse().isCommitted()) {
            throw new IllegalStateException
              (sm.getString("coyoteRequest.sessionCreateCommitted"));
        }
        //在非常有限的情况下重用客户端提供的会话id。
        // Re-use session IDs provided by the client in very limited
        // circumstances.
        String sessionId = getRequestedSessionId();
        if (requestedSessionSSL) {
            //如果会话ID是从SSL握手获得的
            // If the session ID has been obtained from the SSL handshake then
            // use it.
        } 
        //如果路径为/ ,且sessionId是从url中获得的,即url重写获得
        else if (("/".equals(context.getSessionCookiePath())
                && isRequestedSessionIdFromCookie())) {
                /*

            这是常见的(ish)用例:使用相同的会话ID。
            同一主机上的多个web应用程序。通常这是
            用于Portlet实现。只有在session会话通过cookie跟踪时才有效。
            。cookie必须有一条“/”的路径。
            否则不会提供给所有web应用程序的请求。
            客户提供的任何会话ID都应该是一个会话。
            已经存在于主机的某个地方。只有当上下文
            被标识为要确认的时候才会检查。

                */
            /* This is the common(ish) use case: using the same session ID with
             * multiple web applications on the same host. Typically this is
             * used by Portlet implementations. It only works if sessions are
             * tracked via cookies. The cookie must have a path of "/" else it
             * won't be provided for requests to all web applications.
             *
             * Any session ID provided by the client should be for a session
             * that already exists somewhere on the host. Check if the context
             * is configured for this to be confirmed.
             */
            if (context.getValidateClientProvidedNewSessionId()) {
                boolean found = false;
                for (Container container : getHost().findChildren()) {
                    Manager m = ((Context) container).getManager();
                    if (m != null) {
                        try {
                            if (m.findSession(sessionId) != null) {
                                found = true;
                                break;
                            }
                        } catch (IOException e) {
                            // Ignore. Problems with this manager will be
                            // handled elsewhere.
                        }
                    }
                }
                if (!found) {
                    sessionId = null;
                }
            }
        } else {
            sessionId = null;
        }
        //创建一个session
        session = manager.createSession(sessionId);
        // 将新session的jsessionid写入cookie,传给browser ,佐证了session也是cookie


        // Creating a new session cookie based on that session
        if ((session != null) && (getContext() != null)
               && getContext().getServletContext().
                       getEffectiveSessionTrackingModes().contains(
                               SessionTrackingMode.COOKIE)) {
            Cookie cookie =
                ApplicationSessionCookieConfig.createSessionCookie(
                        context, session.getIdInternal(), isSecure());

            response.addSessionCookieInternal(cookie);
        }

        if (session == null) {
            return null;
        }
        //记录访问时间
        session.access();
        return session;
    }
    session = manager.createSession(sessionId);

StandardManager父类ManagerBase中实现

  @Override
    public Session createSession(String sessionId) {

        if ((maxActiveSessions >= 0) &&
                (getActiveSessions() >= maxActiveSessions)) {
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }

        // Recycle or create a Session instance
        Session session = createEmptySession();

        // Initialize the properties of the new session and return it
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
        String id = sessionId;
        if (id == null) {
            //会去重
            id = generateSessionId();
        }
        //在这里将session放入sessions中
        session.setId(id);
        sessionCounter++;

        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            sessionCreationTiming.add(timing);
            sessionCreationTiming.poll();
        }
        return (session);

    }

生产id的方法

    protected String generateSessionId() {

        String result = null;

        do {
            if (result != null) {
                // Not thread-safe but if one of multiple increments is lost
                // that is not a big deal since the fact that there was any
                // duplicate is a much bigger issue.
                duplicates++;
            }

            result = sessionIdGenerator.generateSessionId();

        } while (sessions.containsKey(result));

        return result;
    }

调用算法生成id,然后会在sessions这个map中查看是否有重复,起到去重的效果。

URL地址重写

URL地址重写是对客户端不支持Cookie的解决方案。URL地址重写的原理是将该用户Session的id信息重写到URL地址中。服务器能够解析重写后的URL获取Session的id。这样即使客户端不支持Cookie,也可以使用Session来记录用户状态。HttpServletResponse类提供了encodeURL(Stringurl)实现URL地址重写

该方法会自动判断客户端是否支持Cookie。如果客户端支持Cookie,会将URL原封不动地输出来。如果客户端不支持Cookie,则会将用户Session的id重写到URL中。
增添的jsessionid字符串既不会影响请求的文件名,也不会影响提交的地址栏参数。用户单击这个链接的时候会把Session的id通过URL提交到服务器上,服务器通过解析URL地址获得Session的id。
对于WAP程序,由于大部分的手机浏览器都不支持Cookie,WAP程序都会采用URL地址重写来跟踪用户会话。

response.encodeURL("index.jsp?c=1&wd=Java");
//用于重定向
response.sendRedirect(response.encodeRedirectURL(“administrator.jsp”));

encodeURL org.apache.catalina.connector.Response(真正我们用户能看到的HttpServletResponse实现类是他的封装类ResponseFacade)

 @Override
    public String encodeURL(String url) {

        String absolute;
        try {
            absolute = toAbsolute(url);
        } catch (IllegalArgumentException iae) {
            // Relative URL
            return url;
        }

        if (isEncodeable(absolute)) {
            // W3c spec clearly said
            if (url.equalsIgnoreCase("")) {
                url = absolute;
            } else if (url.equals(absolute) && !hasPath(url)) {
                url += '/';
            }
            return (toEncoded(url, request.getSessionInternal().getIdInternal()));
        } else {
            return (url);
        }

    }

关键在toEncoded

  protected String toEncoded(String url, String sessionId) {

        if ((url == null) || (sessionId == null)) {
            return (url);
        }

        String path = url;
        String query = "";
        String anchor = "";
        int question = url.indexOf('?');
        if (question >= 0) {
            path = url.substring(0, question);
            query = url.substring(question);
        }
        int pound = path.indexOf('#');
        if (pound >= 0) {
            anchor = path.substring(pound);
            path = path.substring(0, pound);
        }
        StringBuilder sb = new StringBuilder(path);
        if( sb.length() > 0 ) { // jsessionid can't be first.
            sb.append(";");
            sb.append(SessionConfig.getSessionUriParamName(
                    request.getContext()));
            sb.append("=");
            sb.append(sessionId);
        }
        sb.append(anchor);
        sb.append(query);
        return (sb.toString());

    }
}

SessionConfig.getSessionUriParamName(
request.getContext()));
调用了这个方法来获取jsessionId
具体获取方法有兴趣的可以深入研究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值