JavaWeb - Cookie、Session、Token 的区别是什么?

发布时间 2023-08-20 02:23:28作者: Himmelbleu

Cookie

客户端登录之后,假如是管理员,拥有所有权限,本次登录之后,这个角色以及权限保存在哪里?

HTTP 是无状态的,每次服务器收到的请求都不知道你是哪个一个客户端(浏览器)。可以保存到 localStorage,但是每次发起请求时需要在代码层面上为成百上千的接口显式地加上这一段吗?HTTP 提供了一个请求头 Cookie,最后的每次请求浏览器就会自动携带这个 Cookie。

Bilibili 的请求携带的 Cookie

在第一次登录一个网站时,服务器判断本次请求头是否携带了 JSESSIONID 这一个 Cookie,如果不存在就会给本次请求的响应头设置一个 Set-Cookie 字段,浏览器就会自动存储这个 JSESSIONID Cookie。下次请求时,浏览器会自动携带这个 Cookie。如下图所示,bilibili 的一个请求所携带的 Cookie:

第一次登录网站时

更形象地说,把 HTTP 比喻成一辆火车,那么 Cookie 就是本次火车的一节车厢,里面存储的是键值对数据。

@WebServlet(urlPatterns = "/first")
public class FirstServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                String name = cookie.getName();
                String value = cookie.getValue();
                System.out.println(name);
                System.out.println(value);
            }
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
}

Session

Session 可以脱离 Cookie 来谈吗?是不可以的。有说,Session 存储在服务器端,Cookie 存储在客户端,这种说法不严谨。因为 Seesion 需要依托于 Cookie。

服务器如何知道本次请求是同一个浏览器呢?浏览器中存储着一个特殊的 Cookie,即 JSESSIONID。A 浏览器发送的每一个请求都携带了 JSESSIONID(也就是 SessionID)假如,A 浏览器的 SessionID 是 E1BB21D3141C034078F0B2A191018136。

服务器通过 SessionID 查询内存中存储的是否存在 E1BB21D3141C034078F0B2A191018136 这样一个 SessionID,在服务器中操作的任何 getAttributesetAttribute 都是基于本次会话来的,而这个浏览器所对应的 Session 对象存储在服务器端,客户端只存储一个 SeesionID 就可以了。

分析视频:Session - 原理分析

获取 Seesion

@WebServlet(urlPatterns = "/second")
public class SecondServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();
        if (session.getAttribute("username") == null) {
            session.setAttribute("username", "himmelbleu");
        }
        if (session.getAttribute("password") == null) {
            session.setAttribute("password", "123456");
        }
        System.out.println(session.getAttribute("username"));
        System.out.println(session.getAttribute("password"));
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
}

Session 的周期

当客户端(浏览器)关闭之后,SessionID 就会被清除,下一次的 SessionID 将会重新分配。原因是 JSESSIONID 的 max-age 设置为 “会话”,代表该 Cookie 只在用户的会话期间有效,当用户关闭浏览器时会话结束,Cookie 将被删除。

如果内存中某个 Session 不活动的时间超过 30 分钟,服务器会自动删除这个 Session。分析视频:Session - 细节3.

tip:[start]

关于 max-age 更多知识:

设置 max-age 为正整数: 如果你设置了 max-age 属性为一个正整数,比如 max-age=3600,那么该 Cookie 将在创建后的 3600 秒(1 小时)内有效。无论用户是否关闭浏览器,Cookie 都会在指定的时间后过期。

设置 max-age 为 0 或负数: 如果你将 max-age 属性设置为 0 或负数,例如 max-age=0,那么该 Cookie 将会立即过期,即会被删除。

tip:[end]

Session 的不便之处

如果你的网站用户群体越来越大,Session 会话技术可能就不太适用。在特定时间内有大量用户访问服务器,可能就会面临有大量 Seesion 的创建。

如果有多台服务器,一台服务器存储了 Session,会面临需要分享 Seesion 给其他服务器。因为一台服务器面临超载,就需要分配一些用户到其他服务器,其他服务器需要通用的 Seesion 才可以避免用户输入用户名和密码。

因此,可以使用数据库存储 Session,数据库如果崩溃了,也会影响服务器获取 Session。

受限于上面种种原因,就出现了一个新的技术叫作 JWT(JSON Web Token,简称 JWT)。

JWT(Token)

同样的,Token 也不能脱离 Cookie 来谈。与 Session 同样的,Token 是由服务器生成的,但是存储在客户端中。可以让浏览器以 Cookie 或 Storage 的形式进行存储。

假设以 Cookie 的形式保存下来,这样下次请求都可以携带这个 Token 给服务器,用户就不需要重新输入用户名和密码了。

JWT 的组成

JWT 由 header、payload、signature 组成。payload 可以存储数据,header 指明 jwt 使用的算法,signature 是签名,解密时需要。

JWTUtils

public class JwtUtil {

    private static final long EXPIRATION_TIME = 86400000; // 24 小时,单位毫秒

    private static Date getExpire(Integer day) {
        return new Date(System.currentTimeMillis() + EXPIRATION_TIME * day);
    }

    private static SecretKey generateKey(String key) {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(key));
    }

    public static String generateToken(String username) {
        JwtBuilder builder = Jwts.builder();
        return builder
                // header
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                // payload
                .claim("username", "himmelbleu")
                .claim("role", "admin")
                .setSubject("admin-test")
                .setExpiration(getExpire(1))
                .setId(UUID.randomUUID().toString())
                // signature
                .signWith(generateKey("cereshuzhitingnizhenbangcereshuzhitingnizhenbang"))
                .compact();
    }

    public static String getUsername(String token) {
        JwtParser parser = Jwts.parserBuilder().setSigningKey(generateKey("cereshuzhitingnizhenbangcereshuzhitingnizhenbang")).build();
        Jws<Claims> claimsJws = parser.parseClaimsJws(token);
        Claims claims = claimsJws.getBody();
        return claims.get("username").toString();
    }

}

LoginServlet

@WebServlet("/api/login")
public class LoginServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        System.out.println(username);
        System.out.println(password);
        String token = JwtUtil.generateToken(username);
        resp.addCookie(new Cookie("token", token));
    }
}

进入登陆接口时,会获取用户的用户名,将其封装到 Token 的 payload 部分,然后通过 Cookie 响应给浏览器保存。

HomeServlet

@WebServlet("/api/home")
public class HomeServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Cookie[] cookies = req.getCookies();

        if (cookies != null) {
            boolean isFoundToken = false;
            for (Cookie cookie : cookies) {
                // 检查特定的 Cookie 名称
                if ("token".equals(cookie.getName())) {
                    String value = cookie.getValue();
                    String username = JwtUtil.getUsername(value);
                    System.out.println("已经登陆:" + username);
                    isFoundToken = true;
                    break;
                }
            }

            if (!isFoundToken) {
                resp.sendRedirect(req.getContextPath() + "/login.jsp");
            }
        }

    }
}

在进入 /api/home 接口时,检查是否有 token 的 Cookie,如果没有就转发进入登陆页面,如果有 token 就可以访问该接口,并打印用户名信息。

Spring Security + JWT

在学习 Spring Security 时,可以结合 JWT 对接口进行安全校验,如果登录了,才可以访问接口,或者登录之后获取其角色以及权限访问某些接口。

总之,Spring Security 是用于拦截接口的,并做我们的校验,JWT 是一种会话技术手段,保存浏览器的状态。

这样,结合 Spring Security + JWT 可以构建一种记住我功能的网站,并提升服务器接口的安全性,防止无意义的请求,频繁操作数据库等异常行为。