一文搞懂Session和JWT登录认证

前言

目前在开发的小组结课项目中用到了JWT认证,简单分享一下,并看看与Session认证的异同。

登录认证(Authentication)的概念非常简单,就是通过一定手段对用户的身份进行确认。

我们都知道 HTTP 是无状态的,服务器接收的每一次请求,对它来说都是 “新来的”,并不知道客户端来过。

举个例子:
客户端A: 我是A, 给我一瓶水。
服务端B: 好,给你。
客户端A: 再给我来个面包。
服务端B: 啥,你是谁?

即每一个请求对服务器来说都是新的。

我们不可能每次操作都让用户输入用户名和密码,那么我们如何让服务器记住我们登录过了呢?

那就是凭证。即每次请求都给服务器一个凭证告诉服务器我是谁。

现在一般使用比较多的认证方式有四种:

  • Session
  • Token
  • SSO单点登录
  • OAtuth登录

下面就来说说比较常用的前两种。

Session

Cookie + Session

最常见的就是 Cookie + Session 认证。

Session,是一种有状态的会话管理机制,其目的就是为了解决HTTP无状态请求带来的问题。

当用户登录认证请求通过时,服务端会将用户的信息存储起来,并生成一个 SessionId 发送给前端,前端将这个 SessionId 保存起来。之后前端再发送请求时都携带 SessionId,服务器端再根据这个 SessionId 来检查该用户有没有登录过。

这个 SessionId, 一般是保存在Cookie中。

如果用户第一次访问某个服务器时,服务器响应数据时会在响应头的 Set-Cookie 标识里将Session Id 返回给浏览器,浏览器就将标识中的数据存在Cookie中。

下面我们来简单写个 demo 测试一下:

初始化一个spring boot 项目,并且代码如下:

demo

我们只需要在用户登录的时候将用户信息存在HttpSession中

@RestController
public class UserController {

    @PostMapping("login")
    public String login(@RequestBody User user, HttpSession session) {
        if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
            // 登录成功 写入Session
            session.setAttribute("sessionId", user);
            return "login success";
        }
        return "username or password incorrect";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        // 注销 删除Session
        session.removeAttribute("sessionId");
        return "logout success";
    }

    public String api(HttpSession session) {
        // 用户操作 判断是否登录
        User user = (User) session.getAttribute("sessionId");
        if (user == null) {
            return "please login";
        }
        return "return data";
    }
}

下面我们向 login 地址 请求,并查看响应。 可以看到,用户登录时服务端会返回 Set-Cookie 字段。这些工作 Servlet帮我们做好了

下面我们向 api 地址请求。可以看到, 后续访问服务端自动就会携带Cookie:

服务端认证身份成功,返回数据。


Session + Header认证

当前开发的几个项目都是采用这种模式。

即 将 Session 会话放进 请求头中作为认证信息。

下面简单写个 demo 并用 postman 测试

demo

pom.xml:

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

HeaderAndParamHttpSessionStrategy:

将cookie认证改为Header认证, 请求关键字为 x-auth-token

/**
 * Header或是请求参数中的带有 token 的认证策略
 * */
public class HeaderAndParamHttpSessionStrategy extends HeaderHttpSessionStrategy {
  /**
   * header认证关键字名称
   */
  private String headerName = "x-auth-token";

  @Override
  public String getRequestedSessionId(HttpServletRequest request) {
    String token = request.getHeader(this.headerName);
    return (token != null && !token.isEmpty()) ? token : request.getParameter(this.headerName);
  }
}

MvcSecurityConfig:
使用spring 提供的 MapSessionRepository 来帮助我们管理Session

@Configuration
@EnableWebSecurity
@EnableSpringHttpSession
public class MvcSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 开放端口
                .antMatchers("**").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and().cors()
                .and().csrf().disable();
        http.headers().frameOptions().disable();
        return http.build();
    }

    /**
     * 使用header认证来替换默认的cookie认证
     */
    @Bean
    public HttpSessionStrategy httpSessionStrategy() {
        return new HeaderAndParamHttpSessionStrategy();
    }


    /**
     * 由于我们启用了@EnableSpringHttpSession后,而非RedisHttpSession.
     * 所以应该为SessionRepository提供一个实现。
     * 而Spring中默认给了一个SessionRepository的实现MapSessionRepository.
     *
     * @return session策略
     */
    @Bean
    public SessionRepository sessionRepository() {
        return new MapSessionRepository();
    }
}

重新启动项目:

尝试1: 向 login 地址请求:

可以看到,服务端的响应头上带有了 x-auth-token, 这种将 session 放在请求头部的认证就是 header认证。

结果: 响应头返回 x-auth-token 字段


尝试2: 请求头不带 x-auth-token, 向服务端请求 api

而这时候,假如客户端发送接下来的请求的时候,请求头不带上服务端返回的 x-auth-token,那么是无法得到认证的。

结果: 认证失败

尝试3: 请求头带 x-auth-token, 向服务端请求 api

将登录成功之后,服务端返回

结果: 认证成功

通过以上的方式: 我们成功将 Cookie + Session 认证 ,替换为了 Header + Session 认证。

那么换之后,有什么好处呢?

Header + Session 相较 Cookie + Session 有几点好处

  • 防止跨站脚本攻击(XSS):使用 Cookie 存储会话 ID 的话,Cookie 是通过浏览器自动管理的,容易受到 XSS 攻击的影响。而将会话 ID 存储在头部,可以避免这种攻击。
  • 避免 CSRF 攻击:使用 Cookie 存储会话 ID 的话,攻击者可以利用 CSRF 攻击来获取 Cookie 中的会话 ID,从而伪造用户请求。将会话 ID 存储在头部的话,可以避免这种攻击。
  • 不受第三方 Cookie 支持的限制:如果用户的浏览器禁用了第三方 Cookie,那么使用 Cookie + Session 的方式就无法使用。而将会话 ID 存储在头部,不需要使用 Cookie,不受这个限制。

缺点:

  • 会话 ID 存储在头部,可能被重放攻击利用
  • 执行性能代价较高:由于 HTTP 头比 Cookie 更大,因此将会话 ID 存储在头部通常会占用更多的网络资源,增加传输延迟。

因此,应该根据具体的应用场景、协议、需求和安全要求来选择合适的身份认证方式。

Token 认证

除了Session之外,目前比较流行的做法就是使用JWT(JSON Web Token)。

JWT具有以下俩种特性:

  • 可以将一段数据加密成一段字符串,也可以从这字符串解密回数据
  • 可以对这个字符串进行校验,比如有没有过期,有没有被篡改

看到这,这不和 Session + Header 认证一样嘛!就是把 SessionId 换成了JWT字符串而已,有必要么?🤔

Session 和 JWT有一个重要的区别,就是 Session 是有状态的,JWT是无状态的。

即,Session 在服务端保存了用户信息,而JWT在服务端没有保存任何信息。

当前端携带Session Id到服务端时,服务端要检查其对应的 HttpSession 中有没有保存用户信息,保存了就代表登录了。

当使用JWT时,服务端只需要对这个字符串进行校验,校验通过就代表登录了。

下面继续从一个Demo体验:

demo

pom.xml:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

写一个工具类

public interface CommonService {
  /**
   * 签名秘钥
   */
  String SECRET = "shareMusic";

  // 根据用户id生成token
  static String createJwtToken(Long id) {
    long ttlMillis = -1; // 表示不添加过期时间
    return createJwtToken(id.toString(), ttlMillis);
  }

  /**
   * 生成Token
   *
   * @param id        编号
   * @param ttlMillis 签发时间 (有效时间,过期会报错)
   * @return token String
   */
  static String createJwtToken(String id, long ttlMillis) {

    // 签名算法 ,将对token进行签名
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成签发时间
    long nowMillis = System.currentTimeMillis();
    Date now = new Date(nowMillis);

    // 通过秘钥签名JWT
    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET);
    Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

    // Let's set the JWT Claims
    JwtBuilder builder = Jwts.builder().setId(id)
        .setIssuedAt(now)
        .signWith(signatureAlgorithm, signingKey);

    // 如果指定了过期时间,则添加
    if (ttlMillis >= 0) {
      long expMillis = nowMillis + ttlMillis;
      Date exp = new Date(expMillis);
      builder.setExpiration(exp);
    }

    return builder.compact();

  }

    // 验证并解析JWT
    static Claims parseJWT(String jwt) { // 如果是空字符串直接返回null
        if (jwt == null ||jwt.isEmpty()) {
            return null;
        }
        // 这个Claims对象包含了许多属性,比如签发时间、过期时间以及存放的数据等
        Claims claims = null;
        // 解析失败了会抛出异常,所以我们要捕捉一下。token过期、token非法都会导致解析失败
        try {

            claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET))
                    .parseClaimsJws(jwt).getBody();
        } catch (JwtException e) {
            System.err.println("解析失败!");
        }
        return claims;
    }
}

同时改写一下我们的 Controller:
登录成功返回

    @PostMapping("login")
    public String login(@RequestBody User user, HttpServletResponse response) {
        if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
            // 判断成功 返回头加入token
            String token = CommonService.createJwtToken(user.getUsername());
            response.setHeader("Authorization", token);
            return "login success";
        }
        return "username or password incorrect";
    }

    @GetMapping("/api")
    public String api(HttpServletRequest request) {
        String jwt = request.getHeader("Authorization");
        // 解析失败就提示用户登录
        if (CommonService.parseJWT(jwt) == null) {
            return "please login";
        }
        return "return data";
    }

重启服务开始测试。

尝试1: 向 login 地址请求.

可以看到,我们成功地在服务端响应头中返回了 JWT,它是以 Authorization 作为名字的字段。

尝试2: 不带JWT, 向 api 地址请求.

认证失败。

尝试3: 带JWT, 向 api 地址请求.

结果: 认证成功, 返回数据。


上面我们成功使用JWT完成了登录和请求api。下面简单看一下它的原理:

JWT原理:

JWT 通常由三部分组成:Header、Payload 和 Signature。

  • Header:包含 Token 类型(即 JWT)和所使用的签名算法信息(如 HS256)。
  • Payload:存储了一些描述信息,如 Token 的颁发者、过期时间、访问权限等,也可以包含一些用户的自定义数据。
  • Signature:由服务器端生成,用于验证 Token 的正确性和完整性。


我们将刚才获取到的JWT去在线网站解密一下:
可以看到,获取到了我们传输的信息: 用户名

在线网站将 Header 和 Payload 中的 Base64 编码信息通过简单的算法将其还原成原始的明文数据

可以看到签名是无法解密的,这是因为 JWT 的签名主要是用于保证 JWT 的完整性和防止 JWT 被篡改 或伪造

signature 可以选择对称加密算法或者非对称加密算法,常用的就是 HS256、RS256。

具体JWt解析过程可以看这篇文章: https://www.freecodecamp.org/chinese/news/how-to-sign-and-val...

JWT 注销

你可能会留意到, 上面的JWT方法没有注销的功能。那么如何注销?🤔

事实上,JWT 是无状态的认证方式,因此它本身并不提供注销的机制。让我们从后台注销token,这对于jwt来说并不是那么简单,并不能像删除 session 那样来删除token。

JWT的目的与session不同,不可能强制删除或失效已经生成的token。

我们可以采用下面两种方式:

Token 过期⏰。

通过过期时间机制。可以在生成 JWT Token 时设置一个过期时间,一旦 Token 过期后则视为无效。通过这种方式,可以保证 Token 在一定时间内有效,同时也避免了 Token 滥用和被盗用的风险。

我还是想注销

假如我有一个严格的注销功能,无法等待Token自动过期怎么办?🕵️‍

那么存储一个所谓的“名单”,判断Token是有效的。一般可以采用 Redis,

校验时,检查提供的 token 在 redis 中是否有效,如何无效的话就让用户去登录。

从这个方面也体现出了 JWt 更适合分布式结构。

Session 和 JWT

两者的不同

存储位置:Session 信息是存储在服务端的,而 JWT 将认证信息存储在客户端的 Token 中。

是否需要状态:Session 基于状态来维护会话,如果会话状态丢失或者被篡改,服务器将会重新初始化会话。而 JWT 身份认证机制是无状态的,每个请求均包含足够的信息,服务器不需要维持任何状态。这一点使得 JWT 身份认证机制特别适合于分布式系统。

安全性:Session 是基于某种算法生成的 Session ID 来维护用户状态的,如果 Session ID 被窃取或者伪造,会话会受到攻击,凭证会失效。而 JWT 通过签名来防止伪造和篡改,只有在经过验证后才能使用。

扩展性:Session 方案一般适用于单一的服务或者单个应用,而 JWT 身份认证机制适用于跨域、分布式服务调用等多场景。

两种方式都可以实现登录认证,至于具体选型就根据自己实际业务需求来了。

参考文章:
https://www.cnblogs.com/RudeCrab/p/14251154.html#%E6%94%B6%E5...
如何使用jwt 完成注销(退出登录)功能 - 个人文章 - SegmentFault 思否
https://cloud.tencent.com/developer/news/837117

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/13320.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

强化学习技巧

此软件包处于维护模式&#xff0c;请使用Stable-Baselines3 (SB3)获取最新版本。您可以在 SB3 文档中找到迁移指南。 本节的目的是帮助您进行强化学习实验。它涵盖了有关 RL 的一般建议&#xff08;从哪里开始、选择哪种算法、如何评估算法等&#xff09;&#xff0c;以及使用自…

【Linux】System V 共享内存、消息队列、信号量

&#x1f34e;作者&#xff1a;阿润菜菜 &#x1f4d6;专栏&#xff1a;Linux系统编程 system V共享内存介绍 System V 共享内存是一种进程间通信的机制&#xff0c;它允许多个进程共享一块物理内存区域&#xff08;称为“段”&#xff09;。System V 共享内存的优点是效率高&…

OTG是什么意思?

OTG是什么意思&#xff1f; OTG是怎么样实现的&#xff1f; TYPE-C接口的手机如何实现同时充电OTG功能&#xff1f; OTG是什么意思&#xff1f; OTG是On-The-Go的缩写&#xff0c;是一项新兴技术&#xff0c;主要应用于不同的设备或移动设备间的联接&#xff0c;进行数据交…

基于遥感的自然生态环境检测——实验三:生态因子提取

实验三&#xff1a;生态因子提取 一、实验目标 生态因子生成&#xff1b;生态因子归一化&#xff1b;生态环境评价 二、实验内容 根据经过大气校正后的影像生产土地覆盖指数、土壤指数以及坡度等&#xff0c;对土地覆盖指数、土壤指数以及坡度进行密度分割归一化&#xff1…

“SCSA-T学习导图+”系列:下一代防火墙

本期引言&#xff1a; 近年来&#xff0c;随着数字化业务带给我们高效和便捷的同时&#xff0c;信息暴露面的增加、网络边界的模糊化以及黑客攻击的产业化&#xff0c;使得网络安全事件相较以往成指数级增加。传统防火墙基于五元组的方式对进出网络的数据流量进行访问控制&…

JavaScript(JS)-1.JS基础知识

1.JavaScript概念 (1)JavaScript是一门跨平台&#xff0c;面向对象的脚本语言&#xff0c;来控制网页行为的&#xff0c;它能使网页可交互 (2)W3C标准&#xff1a;网页主要由三部分组成 ①结构&#xff1a;HTML负责网页的基本结构&#xff08;页面元素和内容&#xff09;。 …

【Linux网络服务】Linux网络设置

一、查看网络配置 1.1ifconfig 1.2ip a 1.3什么是mtu 最大传输单元MTU&#xff0c;是指网络能够传输的最大数据包大小&#xff0c;以字节为单位。MTU的大小决定了发送端一次能够发送报文的最大字节数。如果MTU超过了接收端所能够承受的最大值&#xff0c;或者是超过了发送路径…

EIGRP 配置,详解拓扑表,路由汇聚

1.3 EIGRP 拓扑&#xff0c;路由以及汇聚 1.3.1 实验目的 通过对 EIGRP 拓扑&#xff0c;路由以及汇聚相关实验的练习&#xff0c;掌握 EIGRP 建立拓扑信息的方式&#xff0c; 度量计算方法&#xff0c;如何调整度量&#xff0c;非等价负载均衡&#xff0c;以及 EIGRP 末节路…

做完自动化测试,但别让不会汇报毁了你...

pytest 是一个成熟的全功能Python测试工具&#xff0c;可以帮助您编写更好的程序。它与 python 自带的 unittest 测试框架类似&#xff0c;但 pytest 使用起来更简洁和高效&#xff0c;并且兼容 unittest 框架。pytest 能够支持简单的单元测试和复杂的功能测试&#xff0c;pyte…

Verilog带参数的`define用法

宏除了可以进行简单的文本替换,还可以像函数和任务一样传递指定多个参数分别对文本进行对应的替换. 示例1&#xff1a; define Disp(pa,pb,pc) \initial \begin \#1200; \$display("%d \n",(papbpc)); \$display(" data_ pa data_ pb data_ pc %d",(…

C#中用程序代码修改了datagridview中的数据,保存时只对光标当前行有保存解决办法

C#中DataGridView绑定了DataTable后&#xff0c;通过代码修改DataGridView中的数据&#xff0c;总有一行&#xff08;被修改过并被用户选中的行集合中索引为0的行&#xff09;不能被UpDate回数据库的问题和解决办法 长江黄鹤 2017-06-26 | 300阅读 | 1转藏 转藏全屏朗读分…

真题详解(UML部署图)-软件设计(五十二)

真题详解&#xff08;地址索引&#xff09;-软件设计&#xff08;五十一)https://blog.csdn.net/ke1ying/article/details/130211684 瀑布模式&#xff1a;适应 开发大型项目&#xff0c;且需求明确。 演化模式&#xff1a;适应 对软件需求缺乏准确认知。 螺旋模式&#xff…

【linux】yum “应用商店” 的基本用法

好多工具 yum软件包查看软件包安装软件卸载软件 yum 通俗的讲&#xff0c;这就似我们手机上的应用商店&#xff0c;只不过是在linux下的。 我们可以用yum来下载东西。 软件包 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序。 但是这样…

19 calloc 和 realloc 虚拟内存分配的调试

前言 前面提到了 malloc 虚拟内存分配相关的内容 malloc 虚拟内存分配的调试(1) malloc 虚拟内存分配的调试(2) 这里提 calloc 和 realloc, 这两个函数 虽然没有 malloc 使用频率那么高 但是 还是有很大的知名度的, 本文这里 我们来看一下 calloc 此函数传入两个参数, 第…

Obsidian中如何创作思维导图Mind-map

使用插件 obsidian-mind-map 1.直接在社区下载安装 设置快捷键或者在左侧竖形打开命令面板搜索关键字“mind”&#xff0c; 或者为了便于使用&#xff0c;设置快捷键&#xff0c;在设置-第三方插件中-选择快捷键 然后按下你想设置的快捷键就可以 我这里设置成了CtrlAltM ,M是…

虚拟数字人的3种驱动方式

虚拟数字人是由计算机程序所构建的具有人类特征的虚拟实体&#xff0c;目前的虚拟数字人经过了三代的更迭&#xff0c;划分每一代更迭的标准则是虚拟数字人的驱动方式。 一、虚拟数字人1.0&#xff1a;动画&CG驱动 虚拟数字人1.0就是目前我们所熟知的&#xff0c;比如&am…

LiveCharts2 初步认识

文章目录 1 LiveCharts2 是什么&#xff1f;2 LiveCharts2 可以做什么&#xff1f;3 简单使用LiveCharts2 &#xff0c;实现动态曲线图 1 LiveCharts2 是什么&#xff1f; GitHub&#xff1a;https://github.com/beto-rodriguez/LiveCharts2 官网&#xff1a; https://lvchar…

C++ STL学习之【反向迭代器】

✨个人主页&#xff1a; 夜 默 &#x1f389;所属专栏&#xff1a; C修行之路 &#x1f38a;每篇一句&#xff1a; 图片来源 A year from now you may wish you had started today. 明年今日&#xff0c;你会希望此时此刻的自己已经开始行动了。 文章目录 &#x1f307;前言&a…

分布式锁-Redisson

分布式锁 1、分布式锁1.1 本地锁的局限性1.1.1 测试代码1.1.2 使用ab工具测试(单节点)1.1.3 本地锁问题演示(集群情况) 1.2 分布式锁实现的解决方案1.3 使用Redis实现分布式锁(了解即可)1.3.1 编写代码1.3.2 压测 1.4 使用Redisson解决分布式锁1.4.1 实现代码1.4.1 压测1.4.2 可…

2 常见模块库(2)

2.5 复用器与分路器模块 Mux是一种用于将多个信号组合成一个信号的模块。Mux模块的名称来源于多路复用器&#xff08;Multiplexer&#xff09;。 使用Mux可以将多个输入信号组合成一个向量或矩阵&#xff0c;以便在模型中传递和处理。Mux模块可以接受任意数量的输入信号&#x…