JWT -- 复盘

1、前言

1.1、Token流程

        先来回顾一下利用 token 进行用户身份验证的流程:

  1. 客户端使用用户名和密码请求登录
  2. 服务端收到请求,验证用户名和密码
  3. 验证成功后,服务端会签发一个 token再把这个 token 返回给客户端
  4. 客户端收到 token 后可以把它存储起来,比如放到 cookie 中
  5. 客户端每次向服务端请求资源时需要携带服务端签发的 token,可以在 cookie 或者 header 中携带
  6. 服务端收到请求,然后去验证客户端请求里面带着的 token,如果验证成功,就向客户端返回请求数据

单点登录:一次登录多处使用

        前提:单点登录多使用在分布式系统中

京东:单点登录,是将 token 放入到 cookie 中
案例:将浏览器的 cookie 禁用,则在登录京东则失效,无论如何登录不了

1.2、Token的优点

        基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:

  1. 支持跨域访问:cookie 默认是不支持跨域的,而将 token 放在 HTTP 请求头中可以有效支持跨域请求。这是因为 HTTP 头是可以自定义的,而且标准的跨域资源共享(CORS)策略允许指定哪些头信息可以跨域传输。

  2. 无状态:Token 是无状态的,存储了所有必要的用户信息。这使得后端服务可以不存储状态信息,从而使架构更简单,更易于扩展。这种方式尤其适用于分布式系统和微服务架构,其中各个服务需要轻量级并独立处理请求。

  3. 更适用CDN:使用 token 认证的无状态特性意味着请求可以由任何服务器处理,包括通过 CDN 分发的服务器。这样可以减少对原始服务器的直接请求,提高响应速度并降低延迟。

  4. 更适用于移动端:在非浏览器环境,如移动应用或桌面应用中,cookie 支持可能有限或不稳定,使用 token 可以简化认证过程,因为开发者可以更灵活地控制如何存储和传输 token(例如存储在本地存储或内存中)。

  5. 无需考虑CSRF:由于 token 通常不会自动随请求发送,与 cookie 不同,它需要显式地附加到 HTTP 请求的头部。这种特性意味着不会自动发送到创建它们的同一域,因此大大降低了 CSRF 攻击的风险。

JWT就是上述流程当中token的一种具体实现方式,其全称是 JSON Web Token

 1.3、JWT Token 

       JWT(JSON Web Tokens)的本质是一个字符串,它通过一定的方式对用户信息进行封装、编码和安全处理。下面我会用一个更加生活化的比喻来解释JWT的工作原理和特性:

        想象JWT是一封信件。信封上写着寄信人(头部Header),信的内容是关于寄信人的某些信息(载荷Payload),比如姓名、权限等。为了确保这封信在送达时内容没有被篡改,寄信人在信封上封了一层蜡,并在蜡上压上了自己的印章(签名Signature)。这样,收信人收到信后,只需检查印章是否完整,就能确定信件是否在途中被人篡改过。

在技术层面:

  1. 头部(Header):包含了使用的加密算法与令牌的类型,类似于“信封”的标识。
  2. 载荷(Payload):包含了要传输的信息,这些信息被称为“声明”(Claims),可以包含用户的身份信息以及其他任何需要的数据。
  3. 签名(Signature):服务器利用一个密钥对头部和载荷进行加密,加密后的字符串就是“印章”,用以确保数据在传输过程中未被篡改。

        当JWT在用户和服务器之间传递时,服务器可以通过检查签名来验证信息是否被篡改,以及用户的合法性。这个机制使得JWT非常适合用于身份验证与信息交换,而且由于JWT的自包含性,服务器无需连接数据库即可完成验证,提高了处理速度和减少了系统开销。

JWT的认证流程如下:

1、前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探。

2、后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个 JWT Token形成的JWT Token就是一个如同lll.zzz.xxx的字符串

3、后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的 JWT Token 即可

4、前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题)

5、后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等

6、验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

 

二、为什么要使用JWT

2.1、传统Session认证的弊端

1、用户认证的初次请求

  1. 用户提交认证信息:用户在客户端(如Web浏览器)输入用户名和密码,然后提交这些信息到服务器进行认证。

  2. 服务器处理认证:服务器接收到用户名和密码后,验证这些信息。如果认证成功,服务器需要创建一个会话来跟踪用户的状态,因为HTTP本身不会记住用户的状态或身份。

2、会话创建和会话ID的分发

  1. 创建会话和会话ID:认证成功后,服务器创建一个session,这个session包含有关用户的信息,如用户ID、权限等。每个session都会被分配一个唯一的session ID。

  2. 发送会话ID至客户端:一旦session创建,服务器通过HTTP响应将session ID作为cookie发送给用户的浏览器。服务器通过设置响应头中的Set-Cookie来实现这一点,cookie中会包含session ID。

3、客户端存储和后续请求

  1. 客户端存储Cookie:用户的浏览器接收到这个cookie后,将其保存在本地。此后,每当浏览器向服务器发送请求时,它会自动在请求头中包含这个cookie。

  2. 服务器识别并处理请求:服务器每次接收请求时,会首先检查请求头中是否包含有效的session ID。服务器通过session ID查找相应的session对象,如果找到,就可以识别用户并获取用户的登录状态和其他相关信息。

4、维护用户会话状态

  1. 会话状态的持续性:通过这种方式,即使HTTP协议是无状态的,服务器也能“记住”用户在多个请求之间的状态。这意味着用户在浏览不同页面或进行不同操作时无需重复登录。

5、会话结束

  1. 结束会话:当用户选择登出或者session过期时(由服务器决定何时过期),服务器将终止session,并可能要求浏览器删除包含session ID的cookie。

        通过这种机制,session认证允许Web应用跟踪用户的状态和身份,提供连贯的用户体验。然而,正如先前讨论的,这种方法在处理分布式系统、大量用户、移动应用或跨域请求时可能会面临一些挑战和限制。

        这就是为什么在许多现代应用中,特别是需要弹性扩展和微服务架构的场景中,越来越倾向于使用基于token的认证方式,如JWT。

通过一个生活中例子来类比传统基于session的认证过程:

        想象你去一个大型音乐会,入场时你需要出示购票凭证(这就像是用户在网站上输入用户名和密码)。一旦检票员验证了你的票是有效的,他们会在你的手腕上贴一个不易脱落的贴纸或戴上手环(这相当于服务器为用户创建session并发放一个session ID)。

        一旦你有了这个手环,你就可以自由地进出会场的不同区域,不需要每次进入新区域都重新出示你的购票凭证。只要你的手上戴着那个手环,工作人员就可以通过看手环确认你的入场资格(这就像Web服务器通过检查cookie中的session ID来确认用户的身份和状态)。

        每当你离开音乐会场地时,你可能会被要求剪掉手环,这表示你的访问权限已经结束,再次进入需要重新购买门票或验证(这与用户登出或session过期后服务器终止session相似)。

        这个例子生动地说明了session的工作原理:一次验证,多次使用,直到会话结束。同样地,在Web环境中,用户通过一次登录验证后,可以在不需要重新验证的情况下访问多个页面和服务,直到session结束。

 2.2、传统的session认证的问题

1. 服务器资源消耗大

        每个用户登录后,服务器都必须为其创建并维护一个session。这些session信息通常存储在服务器的内存中。随着在线用户数量的增加,这将显著增加服务器的内存消耗。在用户规模较大的系统中,这会导致显著的性能瓶颈和成本增加。

2. 不利于分布式系统

        在现代的云基础设施和微服务架构中,应用通常被部署在多个服务器或容器上以实现负载均衡和高可用性。由于session默认存储在一个服务器上,用户的后续请求若路由到不同的服务器,其session信息将不可用,除非通过集中式的session管理,如使用Redis等。这不仅增加了架构的复杂性,还可能引入新的性能瓶颈。

3. 移动端兼容性差

        移动设备上的应用可能不支持cookie,或者对cookie的处理方式与桌面浏览器不同。这使得基于cookie的session认证方法在移动端应用中不那么有效,尤其是在原生应用中。

4. 安全隐患

        Session认证依赖于cookie来存储session ID,如果cookie被截获(例如通过XSS攻击或者用户在不安全的网络环境中使用应用),攻击者可以利用这些信息冒充用户。此外,基于cookie的机制也容易受到CSRF攻击,尽管可以通过其他措施(如使用CSRF token)来缓解这一问题。

5. 前后端分离的挑战

        在前后端分离的架构中,前端通常通过API与后端通信。每次API请求都需要携带用户的身份信息。由于cookie和session信息需要在多个中间件之间传递,这增加了维护和调试的复杂性,特别是在跨域场景下。

6. 跨域问题

        基于cookie的session无法轻松处理跨域请求,因为浏览器出于安全考虑默认不会发送cookie到其他域。这限制了基于session的应用在单点登录和跨服务集成中的能力。

2.3、JWT认证的优势

1. 简洁和高效

        JWT是一种非常紧凑的令牌格式。因为它只是一个由三部分组成的字符串——头部(Header)、载荷(Payload)和签名(Signature),每部分之间通过点(.)分隔。由于其紧凑的格式,JWT在网络中的传输效率非常高,适用于各种网络环境,尤其是带宽有限的情况。

2. 跨平台和跨语言支持

        JWT基于JSON格式,这使得它非常容易在不同的编程语言和平台中处理。几乎所有现代编程语言都支持JSON解析和生成,这样JWT可以轻松地在各种客户端和服务器端技术之间使用,无论是Web、桌面还是移动应用。

3. 无状态和适用于分布式微服务

        JWT不需要在服务器端存储会话状态,因此它是无状态的。这一特性非常适合分布式系统和微服务架构,因为服务间的通信不需要每次都进行复杂的会话同步。每个服务只需要能够验证JWT的签名并解析其内容,就可以独立进行身份验证,大大简化了系统架构。

4. 单点登录友好

        JWT非常适合实现单点登录(SSO),因为它可以生成一次并由多个不同的系统验证。由于JWT不依赖于cookie,它可以通过前端存储(如LocalStorage或SessionStorage)在客户端进行管理,也可以作为URL参数安全地在不同应用间传递,这使得跨域身份验证成为可能。

5. 适合移动端应用

        在移动应用中,使用基于cookie的session认证往往不是最佳选择,因为移动平台对cookie的支持不如Web浏览器。JWT则可以在应用内部安全存储,如在设备的本地存储中,或者每次请求时通过HTTP头部发送,这使得它非常适合移动设备和原生应用。

三、JWT结构 

其实是上面为什么使用JWT也有提到的它的结构  -->(标头(Header)、有效载荷(Payload)和签名(Signature))

        确实,JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。
 

3.1、Header

        JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

{

    "alg": "HS256", // 指明签名使用的哈希算法,通常是HMAC SHA256或RSA。

    "typ": "JWT" //表示令牌的类型,标准中规定为“JWT”

}

3.2、Payload

        有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择

iss:发行人

exp:到期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID用于标识该JWT

        除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:

{

    "sub": "1234567890",

    "name": "zNuyoah",

    "admin": true

}

        注意:默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。

        JWT只是适合在网络中传输一些非敏感的信息。

3.3、Signature

        签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

        总结:JWT由 Header加密.Payload加密.(Header加密.Payload加密)再加密三部分组成

        注意:JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:
        1、header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据。
        2、signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。

        注意:secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值。

四、总结

4.1、什么是JWT

        JSON Web Token,通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全的传输信息。

4.2、JWT有什么用

        JWT最常用的场景就是授权认证,一旦用户登录,后续每个请求都将包含JWT,系统每次处理用户请求之前,都要进行JWT安全校验,通过之后再进行处理

4.3、JWT组成

        JWT由三部分组成,用 . 拼接

1.Header

{

    "alg": "HS256",

    "typ": "JWT"

}

2.Payload

{

    "sub": "1234567890",

    "name": "zNuyoah",

    "admin": true

}

3.Signature

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var Signature = HMACSHA256(encodedString, 'secret');

 五、简单代码演示

1、POM

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

2、测试 - 加密(重新生成的都不一样的,因为里面包含超时时间,每次执行的时间都不是同一时刻)

 

@Test
public void test() {
    JwtBuilder builder = Jwts.builder();
    String jwtToken = builder
            // header
            .setHeaderParam("typ", "JWT")
            .setHeaderParam("alg", "HS256")
            // payload
            .claim("username", "zNuyoah")
            .claim("role", "admin")
            // 设置主题
            .setSubject("admin-test")
            // 设置过期时间 - 一天
            .setExpiration(new Date(System.currentTimeMillis() + (1000 * 60 * 60 * 24)))
            .setId(UUID.randomUUID().toString())
            // signature
            .signWith(SignatureAlgorithm.HS256, "admin")
            .compact();

    System.out.println(jwtToken);
}

3、测试 - 解密

@Test
public void parse() {
    String token = "*************"; 
    JwtParser parser = Jwts.parser();
    Jws<Claims> claimsJws = parser.setSigningKey("admin").parseClaimsJws(token);

    Claims claims = claimsJws.getBody();
    System.out.println(claims.get("username"));     // zNuyoah
    System.out.println(claims.get("role"));         // admin
    System.out.println(claims.getId());             
    System.out.println(claims.getSubject());        // admin-test
    System.out.println(claims.getExpiration());     
}

 六、实际开发应用

        在Spring Boot项目中,结合JWT和Redis进行用户身份验证和会话管理是一种非常高效和安全的做法。

步骤 1: 生成并存储会话信息

        在用户成功登录后,服务器生成一个随机的Token(如使用UUID)。这个Token作为用户会话的唯一标识,并将其与用户的详细信息一起存入Redis中。这里,Redis起到了存储用户会话状态的作用,有效地替代了传统的session机制。设置适当的过期时间后,这个Token将在一定时间后自动失效,保障了安全性。

实现提示:

  • 使用UUID.randomUUID().toString()生成随机Token。
  • 使用Spring Data Redis或Jedis等库将Token和用户信息存储在Redis中。
  • 设置Token的过期时间以控制会话有效期。

步骤 2: 生成JWT

        将步骤1中生成的Token放入JWT的payload中,然后生成JWT。这个JWT将作为用户的身份凭证,传送给前端。

实现提示:

  • 在JWT的payload中加入Token作为自定义声明(claim),例如
  • { "token": "<random_token>" }
  • 使用库如jjwtjava-jwt来生成JWT。
  • 确保选择合适的加密算法和秘钥保护JWT的完整性和安全性。

步骤 3: 前端存储及传输JWT

        前端在收到JWT后,通常会存储在本地存储(如localStorage或sessionStorage)中。每次发送请求到服务器时,将JWT添加到HTTP请求的Authorization头部中,通常采用Bearer <token>格式。

实现提示:

  • 确保前端在发送请求时正确地设置Authorization头部。
  • 前端应该处理JWT过期或无效的情况,可能需要重新登录或刷新Token。

步骤 4: 后端验证JWT和会话

        后端需要验证每个请求中的JWT的合法性。这包括解析JWT,验证签名,检查Token有效性等。验证通过后,从JWT解析出随机Token,并使用它从Redis中查询用户信息。

实现提示:

  • 实现一个Spring Security的过滤器或者使用@RestControllerAdvice来处理所有进入的请求,从中解析和验证JWT。
  • 如果Redis中可以查询到与Token对应的用户信息,则认为用户已经登录,继续处理请求;如果查不到,返回错误响应,如401 Unauthorized。
  • 考虑JWT或Token在Redis中过期的情况,合理处理这些异常情况。
public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String JWT = request.getHeader("Authorization");
        try {
            // 1.校验JWT字符串
            DecodedJWT decodedJWT = JWTUtils.decode(JWT);
            // 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
            ...
            return true;
        }catch (SignatureVerificationException e){
            System.out.println("无效签名");
            e.printStackTrace();
        }catch (TokenExpiredException e){
            System.out.println("token已经过期");
            e.printStackTrace();
        }catch (AlgorithmMismatchException e){
            System.out.println("算法不一致");
            e.printStackTrace();
        }catch (Exception e){
            System.out.println("token无效");
            e.printStackTrace();
        }
        return false;
    }
}

 七、小记

        在实际开发中需要用下列手段来增加JWT的安全性:

1. 使用HTTPS传输

        使用HTTPS可以有效防止中间人攻击(MITM),这种攻击方式包括但不限于网络监听和数据劫持。HTTPS通过对所有传输数据进行加密,确保数据在传输过程中的机密性和完整性。即使数据被拦截,攻击者也无法直接读取或篡改加密的内容。

实施方法:

  • 在服务器配置SSL/TLS证书,强制使用HTTPS连接。
  • 确保所有API端点只通过HTTPS提供服务。
  • 配置HTTP严格传输安全(HSTS)来防止SSL剥离攻击。

2. 保证服务器的安全

        服务器的安全是保护JWT安全的关键,因为JWT的签名密钥必须保持绝对的安全。如果服务器被攻破,攻击者可能获得签名密钥,从而可以伪造有效的JWT。

实施方法:

  • 定期更新和打补丁操作系统和应用软件。
  • 使用防火墙、入侵检测系统(IDS)和入侵防御系统(IPS)来防护服务器。
  • 对服务器进行安全配置,限制不必要的服务和访问。
  • 定期进行安全审计和渗透测试。

3. 定期更换哈希签名密钥

        定期更换密钥可以降低密钥被破解的风险。如果密钥泄露或者被破解,定期更换密钥可以限制攻击者使用旧密钥造成的损害。

实施方法:

  • 设计一个密钥轮换计划,例如每三个月更换一次密钥。
  • 确保密钥更换过程中,旧密钥和新密钥在一段时间内共存,以确保在过渡期间不会因密钥更新而影响用户体验。
  • 使用密钥管理系统来保护和管理密钥,确保密钥的安全性和更换的自动化。

4. 增加JWT的复杂性和安全性

        除了上述措施,还可以通过增加JWT自身的复杂性和安全性来提高整体的安全水平。

实施方法:

  • 使用较长的密钥长度,比如使用256位以上的密钥,以提高破解的难度。
  • 对于非常敏感的应用,可以考虑对JWT的payload部分进行加密。
  • 限制JWT的有效期,设置较短的过期时间,减少JWT被滥用的风险。
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-Z_Nuyoah

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值