Session

工作原理

  • 服务端存储:在传统的 Session 机制中,用户登录后,服务器会为用户创建一个会话对象,并将会话数据(如用户信息、权限等)存储在服务器内存中。

  • 客户端携带 Session ID:服务器会将一个唯一的会话 ID(通常为 JSESSIONID)通过 Cookie 发送给客户端,客户端在后续请求中自动携带这个 Session ID

优点

  • 简单易用:不需要客户端做额外的处理,只需要依赖 Cookie 或 URL 参数来传递会话 ID。

  • 服务端控制:所有会话数据保存在服务器端,更容易进行管理(如会话过期、登出等)。

  • 自动过期:会话可以设置超时,过期后客户端需要重新登录。

缺点

  • 会话存储压力:随着用户量的增加,服务端需要维护大量的会话数据(可能需要负担更多内存),尤其在分布式系统中,处理会话信息会变得复杂。

  • 不适合分布式系统:当应用部署在多台服务器上时,需要一个集中式存储(如 Redis)来共享会话信息。

图解

代码实现

   @RequestMapping("/savaSessionInfo")
    public String savaSessionInfo(HttpSession session, String msg) {
        if (StringUtils.isBlank(msg)) {
            return "msg不能为空";
        }
        session.setAttribute("session", msg);
        return "保存Session信息成功,sessionId为:" + session.getId();
    }
    @RequestMapping("/getSessionInfo")
    public String getSessionInfo(HttpSession session) {
        return "获取的session信息为:" + session.getAttribute("session");
    }

Token

工作原理

  • 无状态认证:Token 机制(通常是基于 OAuth2 或 JWT)允许客户端在登录后获取一个 token,该 token 是由服务器签发的,包含了用户的认证信息。

  • 客户端存储:客户端将 token 存储在本地(通常是 LocalStorage 或 Cookie 中),每次请求时将 token 作为请求头(Authorization: Bearer <token>)传递给服务端。

  • 服务端验证:服务端接收到请求时,从 token 中提取出用户信息(如用户 ID、权限等)并进行验证。

优点

  • 无状态:Token 是自包含的,服务端不需要存储会话数据,验证用户身份时只需要解析 Token。

  • 易于扩展:Token 可以用于分布式和微服务架构,不依赖于会话存储。

  • 支持跨域认证:Token 作为 HTTP Header 传递,支持跨域请求,而 Session 通常通过 Cookie 实现,不适合跨域。

缺点

  • 安全性问题:Token 存储在客户端,容易受到 XSS 攻击,且 token 一旦泄露,可能会被恶意使用。

  • 没有主动失效机制:Token 通常有有效期,过期后需要重新登录,但是没有像 Session 那样可以主动注销。

图解

   /**
     * 保存 token信息
     *
     * @param msg
     * @return
     */
    @RequestMapping("/savaByToken")
    public String savaByToken(String msg) {
        String token = UUID.randomUUID().toString();
        redisManager.setex(RedisConstant.REDIS_TOKEN + token, msg, CommonConstant.EXPIRES_ONE_MIN * 10);
        return "保存token信息成功,token为:" + token;
    }

    /**
     * 获取 token信息
     *
     * @param token
     * @return
     */
    @RequestMapping("/getByToken")
    public String getByToken(String token) {
        if (StringUtils.isBlank(token)) {
            return "token不能为空";
        }
        return "获取token信息成功,token为:" + redisManager.get(RedisConstant.REDIS_TOKEN + token);
    }

Token+Cookie

工作原理

  • 结合了 Token 和 Cookie:在这种方式中,Token 通常作为 Cookie 中的一个字段(如 token)存储,而不是 LocalStorage。

  • 自动携带 Token:浏览器会自动在每次请求时通过 Cookie 将 Token 携带到服务器。

  • 服务端验证:服务端从请求的 Cookie 中提取 Token,并进行身份验证。

优点

  • 免去手动传递 Token:由于浏览器会自动将 Cookie 中的 Token 携带到每个请求中,开发者不需要显式地在每次请求中添加 Authorization 头。

  • 支持跨页面、跨标签页共享:由于 Cookie 是浏览器的全局状态,跨页面、跨标签页都可以访问 Token。

缺点

  • XSS 攻击的风险:如果 Token 存储在 Cookie 中,且没有设置 HttpOnlySecure 标志,可能会暴露给恶意脚本。

  • CSRF 攻击的风险:如果 Cookie 没有设置 SameSite 属性,可能会导致跨站请求伪造(CSRF)攻击。

提高安全性

  • HTTP-Only Cookie:通过设置 HttpOnly 属性,阻止 JavaScript 访问 Cookie,减少 XSS 风险。

  • SameSite Cookie:通过设置 SameSite 属性,防止 CSRF 攻击。

图解

代码实现

 /**
     * 保存 token信息
     *
     * @param msg
     * @return
     */
    @RequestMapping("/savaByTokenWithCookie")
    public String savaByTokenWithCookie(HttpServletResponse response, String msg) {
        String token = UUID.randomUUID().toString();
        redisManager.setex(RedisConstant.REDIS_TOKEN + token, msg, CommonConstant.EXPIRES_ONE_MIN * 10);
        Cookie cookie = new Cookie("token", token);
        cookie.setMaxAge(CommonConstant.EXPIRES_ONE_MIN / 100);
        response.addCookie(cookie);
        return "保存token信息成功,token为:" + token;
    }

    /**
     * 通过 Cookie获取 token
     *
     * @param request
     * @return
     */
    @RequestMapping("/getByTokenWithCookie")
    public String getByTokenWithCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return "获取token信息为null";
        }
        String token = null;
        for (Cookie cookie : cookies) {
            if ("token".equals(cookie.getName())) {
                token = cookie.getValue();
                break;
            }
        }
        if (StringUtils.isBlank(token)) {
            return "token不能为空";
        }
        return "保存token信息成功,token为:" + redisManager.get(RedisConstant.REDIS_TOKEN + token);
    }

JWT

工作原理

  • 自包含的 Token:JWT 是一种自包含的 Token 格式,通常由三部分组成:

    1. Header:声明签名算法(如 HS256)。

    2. Payload:包含声明(如用户信息、权限等)。

    3. Signature:对 Header 和 Payload 的加密签名,确保数据的完整性和安全性。

  • 存储和传递:JWT 通常通过 Authorization: Bearer <token> 传递在请求头中,或者可以存储在 Cookie 中。

优点

  • 无状态认证:JWT 是自包含的,服务端无需存储会话信息,验证身份只需要解析 Token。

  • 支持跨域认证:由于 JWT 是通过 HTTP Header 传递的,可以方便地用于跨域请求。

  • 灵活性高:JWT 可以携带多种自定义信息,适用于复杂的认证和授权场景。

缺点

  • 过期机制:JWT 的有效期通常较短,过期后客户端需要重新获取(例如通过刷新 Token)。不过可以使用刷新 Token 来延续会话。

  • 泄露风险:JWT 存储在客户端(LocalStorage 或 Cookie)时,可能会被 XSS 攻击获取。

图解

代码实现

   /**
     * 保存 token信息
     *
     * @param msg
     * @return
     */
    @RequestMapping("/savaByJwt")
    public String savaByJwt(String msg) {
        if (StringUtils.isBlank(msg)) {
            return "msg不能为空";
        }
        String token = jwtManager.createToken(RedisConstant.JWT, msg, CommonConstant.EXPIRES_ONE_MIN * 10);
        return "保存jwt的token信息成功,token为:" + token;
    }

    /**
     * 获取 token信息
     *
     * @param token
     * @return
     */
    @RequestMapping("/getByJwt")
    public String getByJwt(String token) {
        if (StringUtils.isBlank(token)) {
            return "token不能为空";
        }
        String tokenData = jwtManager.getTokenData(RedisConstant.JWT, token, String.class);
        return "获取token信息成功,token为:" + tokenData;
    }

比较总结

特性

Session

Token

Token + Cookie

JWT

存储位置

服务端存储

客户端存储

客户端存储 (Cookie)

客户端存储 (Cookie 或 LocalStorage)

认证方式

会话 ID

身份信息通过 Token

Token 通过 Cookie

自包含的 Token

跨域支持

不支持

支持

支持

支持

分布式系统

需要集中存储

无状态,无需集中存储

无状态,无需集中存储

无状态,无需集中存储

安全性

高,服务器控制

依赖 Token 的存储方式,可能暴露

需要防范 XSS 和 CSRF 攻击

需要防范 XSS 攻击和 Token 泄露

过期处理

自动过期

过期后需要重新登录

过期后需要重新登录

可以通过刷新 Token 延续会话

总结

  • Session:适用于传统的单体应用,所有会话信息存储在服务器,较为简单但不适合分布式架构。

  • Token:适合微服务架构和分布式系统,客户端存储 Token,无状态认证。

  • Token + Cookie:结合 Token 的无状态认证和 Cookie 的自动携带,方便开发,但需要注意安全问题。

  • JWT:是一种特殊的 Token 格式,具有自包含性,适合复杂认证和跨域场景,但要注意安全性和过期机制。