Cookie/Session/Token

原文使用有道云笔记:为更好浏览体验移步有道云:
 
为什么有cookie/Session?
因为HTTP协议本身是无状态,HTTP无状态协议,是指协议对于事务处理没有记忆能力。
无状态是指Web浏览器与Web服务器之间不需要建立持久的连接,这意味着当一个客户端向服务器端发出请求,然后Web服务器返回响应(Response),连接就被关闭了,在服务器端不保留连接的有关信息。
于是,两种用于保持HTTP连接状态的技术就应运而生了,一个是Cookie,而另一个则是Session
也就是会话管理
Cookie技术:会话数据保存在浏览器客户端。
Session技术:会话数据保存在服务器端。
 
Cookie
Cookie实际上是一小段的文本信息(key-value格式)。它的原理如下:
1:由服务器产生一个cookie;
2:服务器发送给浏览器(响应头中包含set-cookie);
3:浏览器保存;
4:浏览器之后发送请求,请求头中会有cookie信息;
5:服务器接收到cookie。
 
通过一个示例说明:
新建项目,这里使用之前的springboot项目(outwebest)了,懒得建。
我这里使用servlet来写写看:新建两个servlet:
@WebServlet(urlPatterns = "/setCookie")
public class SetCookieServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Cookie cookie = new Cookie("userName","sunsas");
        resp.addCookie(cookie);
    }
}
 
@WebServlet("/getCookie")
public class GetCookieServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Cookie[] cookies = req.getCookies();
        if(cookies != null){
            for(Cookie cookie : cookies){
                System.out.println(cookie.getName() + ":" + cookie.getValue());
            }
        }
        else {
            System.out.println("no cookies");
        }
    }
}
需要注意要在启动类加上注解:@ServletComponentScan
否则404.
启动后,打开浏览器(这里使用chrome)控制台(F12)-》network:
查看刚才的请求:
 
可知,我们在响应中设置的cookie,实际是在响应头添加了:
Set-Cookie: userName=sunsas
浏览器会把它放到请求头中,下一次访问http://localhost:8080/getCookie
已经携带cookie了。后端也接收到了:
当然上面两个servlet也可以使用一个controller代替了:
@Controller
public class CookieSessionTokenController {
    @RequestMapping("/cSetCookie")
    public void cSetCookie(HttpServletResponse resp){
        Cookie cookie = new Cookie("userName","sunsas");
        resp.addCookie(cookie);
    }
    @RequestMapping("/cGetCookie")
    public void cGetCookie(HttpServletRequest req){
        Cookie[] cookies = req.getCookies();
        if(cookies != null){
            for(Cookie cookie : cookies){
                System.out.println(cookie.getName() + ":" + cookie.getValue());
            }
        }
        else {
            System.out.println("no cookies");
        }
    }
}
只能说以前写代码真麻烦,servlet3以上才支持的@Webservlet,更久以前还得自己写xml中配置servlet,一个servlet至少6行,吐了。现在一个controller直接给你搞了,来再多的接口,也不虚。
这里还要说下setMaxAge这个方法:
/**
* Sets the maximum age of the cookie in seconds.
* <p>
* A positive value indicates that the cookie will expire after that many
* seconds have passed. Note that the value is the <i>maximum</i> age when
* the cookie will expire, not the cookie's current age.
* <p>
* A negative value means that the cookie is not stored persistently and
* will be deleted when the Web browser exits. A zero value causes the
* cookie to be deleted.
*
源码已经说的很清楚了,这个方法就是设置cookie的过期时间,单位是秒,
正值就是多少秒过期,0的话就是马上过期。没啥意义。
默认为-1,负值表示浏览器关闭就会丢失cookie。
我这里设置存活时间为10min:
cookie.setMaxAge(60*10);
 
响应头添加上了过期时间。
 
Demo:显示上次访问时间
思路就是每次访问去看看cookie中有没有访问时间,如果没有,是第一次访问,如果有,则打印时间,并更新时间:
@RequestMapping("/lastVisitTime")
@ResponseBody
public String lastVisitTime(HttpServletRequest req,HttpServletResponse resp){
    String cookieKey = "lastVisitTime";
    String lastTime = null;
    // 首先去拿cookie
    Cookie[] cookies = req.getCookies();
    if(cookies != null){
        for(Cookie cookie : cookies){
            if(cookieKey.equals(cookie.getName())){
                lastTime = cookie.getValue();
            }
        }
    }
    // 更新cookie ,注意addCookie时是不能使用空格的 而在ASCII码中32对应的就是空格。
    String time = new SimpleDateFormat("yyyy-MM-dd/hh:mm:ss").format(new Date());
    Cookie cookie1 = new Cookie(cookieKey, time);
    resp.addCookie(cookie1);
    // 返回值
    if(lastTime == null){
        return "first visit";
    }
    return lastTime;
}
注意addCookie时是不能使用空格的 而在ASCII码中32对应的就是空格。
否则会报错:
An invalid character [32] was present in the Cookie value
 
Cookie的局限:
1)Cookie只能存字符串类型。不能保存对象
2)只能存非中文。
3)1个Cookie的容量不超过4KB。
如果大小超过4kb,则使用session。
 
Session
会话数据保存在服务器端。(内存中)
 
当访问服务器否个网页的时候,会在服务器端的内存里开辟一块内存,这块内存就叫做session,而这个内存是跟浏览器关联在一起的。这个浏览器指的是浏览器窗口,或者是浏览器的子窗口,意思就是,只允许当前这个session对应的浏览器访问,就算是在同一个机器上新启的浏览器也是无法访问的。而另外一个浏览器也需要记录session的话,就会再启一个属于自己的session。
 
由于HTTP协议无状态,那么浏览器怎么知道自己的session,答案是sessionId
1:第一次访问创建session对象,分配一个sessionId;
2:把sessionId放在cookie中给浏览器;
3:第二次访问cookie就携带了sessionId;
4:服务器根据sessionId查找对应的session(缓存中);
5:如果找到,返回session对象;
6:没有找到则创建session。
 
新建servlet类:
@WebServlet("/createSessionServlet")
public class CreateSessionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession(true);
        session.setAttribute("userName", "sunsas");
    }
}
 
@WebServlet("/getSessionServlet")
public class GetSessionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        if(session != null){
            String value = (String) session.getAttribute("userName");
            System.out.println(value);
        }
        else {
            System.out.println("no session");
        }
    }
}
注意req.getSession()方法。此方法默认传参true,如果为false,获得session为空也直接返回,为true则会创建一个session:
/**
* Return the session associated with this Request, creating one
* if necessary.
*/
@Override
public HttpSession getSession() {
    return getSession(true);
}
 
@Override
public HttpSession getSession(boolean create) {

    if (crossContext) {

        // There cannot be a session if no context has been assigned yet
        // crossContext为true表示配置的不同context共享一个session,这里省略
    } else {
        return super.getSession(create);
    }
 
// Request类下
@Override
public HttpSession getSession(boolean create) {
    Session session = doGetSession(create);
    if (session == null) {
        return null;
    }
    return session.getSession();
}

// 创建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;
    }

    // Return the current session if it exists and is valid
    if ((session != null) && !session.isValid()) {
        session = null;
    }
    if (session != null) {
        return session;
    }

    // Return the requested session if it exists and is valid
    Manager manager = context.getManager();
    if (manager == null) {
        return null;      // Sessions are not supported
    }
    if (requestedSessionId != null) {
        try {
            session = manager.findSession(requestedSessionId);
        } catch (IOException e) {
            session = null;
        }
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            session.access();
            return session;
        }
    }

    // Create a new session if requested and the response is not committed
    if (!create) {
        return null;
    }
    boolean trackModesIncludesCookie =
            context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
    if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
        throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
    }

    // Re-use session IDs provided by the client in very limited
    // circumstances.
    String sessionId = getRequestedSessionId();
    if (requestedSessionSSL) {
        // If the session ID has been obtained from the SSL handshake then
        // use it.
    } else if (("/".equals(context.getSessionCookiePath())
            && isRequestedSessionIdFromCookie())) {
        /* 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 = manager.createSession(sessionId);

    // Creating a new session cookie based on that session
    if (session != null && trackModesIncludesCookie) {
        Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
                context, session.getIdInternal(), isSecure());

        response.addSessionCookieInternal(cookie);
    }

    if (session == null) {
        return null;
    }

    session.access();
    return session;
}
控制台打印“
后访问
还是F12看一下浏览器请求:
可见里面存的叫JSessionId。
这里我把浏览器关闭了,再次创建session然后获得session,所以他们id不一样。
由于每次重新开启浏览器都会生成新的session,但这并不代表原来的session失效了。
例如我现在复制下原来的sessionId
A7439D2FFDF37F89C87AE5B6D4D64EAE
我们给项目打上断点(Request下的doGetSession方法中),debug运行:
重启浏览器,再次访问http://localhost:8080/createSessionServlet
右键此方法进行运行:
把老的sessionId传进去:
可见session仍然在。
session本身有一个存活时间,在tomcat中默认的是30分钟,
和浏览器是没有关系的
 
Token
在计算机身份认证中是令牌(临时)的意思。一般作为邀请、登录系统使用。
Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。
这有一篇文章关于cookie,session,token的演化讲的很好:
简单来说,我们以前保存用户登录可能就是放在session,现在也可以,服务器返回的的sessionId相当于就是token,但是这样服务器要存很多的sessionId,来做验证。
由于sessionId增建了服务器压力,于是有人发明了token,token存在客户端,在请求时随请求头过来。
但是服务器端没有token,关键就是验证。就是加上比如userId+密钥,加密。这个只有服务器知道,有请求过来,就去解析,拿到userId。
1:客户端请求来拿到token;
2:服务器根据此用户id,加上签名,密钥加密,并返回给客户端;
3:客户端在请求头中保存token;
4:客户端下一次请求,服务器拿到token,并去解密;
5:根据结果做不同操作。
 
现在企业用JWT比较多,有机会再说这个东西。

原文地址:https://www.cnblogs.com/SunSAS/p/12468894.html