JWT技术--JSON Web Token

Posted 得过且过的勇者y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JWT技术--JSON Web Token相关的知识,希望对你有一定的参考价值。

一、JWT简介

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

使用方式:服务端根据规范生成一个令牌(token),并且发放给客户端(保存在客户端)。此时客户端请求服务端的时候就可以携带者令牌,以令牌来证明自己的身份信息。

作用:类似session保持登录状态的办法,通过token来代表用户身份。

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

二、为什么使用JWT

1、传统的Session认证

我们知道,http协议本身是一种无状态的协议,这意味着用户提供用户名和密码进行用户认证后,下一次请求还需要进行认证。因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让应用能识别是哪一个用户发出的请求,我们只能在服务器存储一份用户登录信息,这份信息会在响应时传递给客户端,告诉其保存为cookie(传输的是JsessionId而不是用户信息,不然用户修改cookie内容后可以获得其他权限),以便下次请求时发送给我们的应用,这样我们就知道请求应用的是哪个用户了。这就是传统的Session认证。

暴露的问题:

  1. 每个用户经过我们的应用认证后,我们的应用都要在服务端做一次记录,以便下次用户请求时的鉴别。通常而言session是保存在内存中的,随着认证用户的增多,服务端的开销会明显增大。

  2. 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话, 以为这下次请求还必须在这台服务器上,才能拿到授权的资源。这样在分布式应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

  3. 在前后端分离解耦后增加了部署的复杂性。通常用户一次请求要经过多次转发,如果使用session每次携带sessionId到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,给服务器增加了负担。sessionId就是一个特征值,表达的信息不够丰富,不容易扩展。而且如果后端应用是多节点部署,就需要实现session共享机制,不方便集群应用。

  4. 因为是基于cookie来进行应用识别的,cookie如果被截获,用户就会很容易受到CSRF/XSRF(跨站请求伪造)

    CSRF攻击的大致方式如下:某用户登录了A网站,认证信息保存在cookie中。当用户访问攻击者创建的B网站时,攻击者通过在B网站发送一个伪造的请求提交到A网站服务器上,让A网站服务器误以为请求来自于自己的网站,于是执行响应的操作,该用户的信息边遭到了篡改。总结起来就是,攻击者利用用户在浏览器中保存的认证信息,向对应的站点发送伪造请求。用户的认证是通过保存在cookie中的数据实现,在发送请求是,只要浏览器中保存了对应的cookie,服务器端就会认为用户已经处于登录状态,而攻击者正是利用了这一机制。

2、基于JWT认证

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

后端核对用户名和密码成功后,将用户的id等其他用户信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。Token:head.payload.signature

后端将JWT字符串作为登录成功的返回结果返回给前端,前端可以将返回结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。

前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)

后端检查是否存在,如存在验证JWT的有效性。例如检查签名是否正确,检查Token是否过期,检查Token的接收方是否为自己(可选)。

验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

3、JWT优势

  1. 简洁:可以通过URL、POST参数或者在HTTP Header发送,因为数据量小,传输速度也很快
  2. 自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
  3. 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
  4. 不需要在服务端保存会话信息,特别适合用于分布式微服务。
  5. 更适合用于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时使用token认证方式会简单很多
  6. 单点登录友好:由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话, token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题

三、JWT的结构

1、令牌组成

  • 标头(Header)
  • 有效负荷(Payload)
  • 签名(Signature)

因此JWT通常为:xxxx.yyyy.zzzz,即Header.Payload.Signature

2、Header

标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC、SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。

注意:Base64是一种编码,也就是说它是可以被翻译回原来的样子来的,它并不是一种加密过程

默认为:base64enc(“alg”:“HS256”,“typ”:“JWT”)

3、Payload

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成JWT结构的第二部分


	"sub":"123456789”,
	"name”:"John Doe",
    "admin":true

4、Signature

前面两部分都是使用Base64进行编码的,即前端可以解开获取其中的内容。Signature需要使用编码后的header和payload以及我们提供的一个秘钥,然后使用header中指定的签名算法进行签名,签名的作用是保证JWT没有被篡改过

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

实际上是对头部信息和负载内容进行签名,防止内容被篡改,如果有人对头部以及负载内容解码后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT上附带的签名是不一样的。如果要对新的头部和负载进行签名,由于不知道服务器加密时使用的秘钥,得出来的结果也是不一样的。

信息安全问题

在JWT中不应该在负载中加入任何敏感的数据,用户ID被知道也是安全的,但是像密码这样的内容就不能放在JWT中了。

四、JWT实现

1、测试

public class JWTTest 

    public static void main(String[] args) 
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.SECOND,1000);
        String sign = JWT.create()
                .withClaim("username", "cyh")//设置payload
                .withClaim("userId", 123)
                .withExpiresAt(instance.getTime())//设置令牌过期时间
                .sign(Algorithm.HMAC256("asd!de"));//签名
        System.out.println(sign);
    

    @Test
    public void test()
        //创建验证对象
        JWTVerifier build = JWT.require(Algorithm.HMAC256("asd!de")).build();
        DecodedJWT verify = build.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDkxNzg1MTAsInVzZXJJZCI6MTIzLCJ1c2VybmFtZSI6ImN5aCJ9.J19zl01GobHB1XpnnvCe8N-cjqCY6SDyQ3eQGiYBE7M");
        System.out.println(verify.getClaims());
        System.out.println(verify.getClaim("username"));
        System.out.println(verify.getClaim("userId"));
        System.out.println(verify.getExpiresAt());
        System.out.println(verify.getHeaderClaim("alg"));
        System.out.println(verify.getHeaderClaim("typ"));
    

2、封装

工具类

public class JWTUtils 

    private static final String SIGN="!ad#12~";

    //生成token
    public static String getToken(Map<String,String> map)
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,7);//默认七天过期
        JWTCreator.Builder builder = JWT.create();
        map.forEach(builder::withClaim);
        return builder.withExpiresAt(instance.getTime())//设置令牌过期时间
                .sign(Algorithm.HMAC256(SIGN));//签名
    

    //验证token合法性,获取token信息
    public static DecodedJWT verify(String token)
        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    

拦截器

public class JWTInterceptor implements HandlerInterceptor 

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        Map<String,Object> map = new HashMap<>();
        //获取请求头中的令牌
        String token = request.getHeader("token");
        try 
            JWTUtils.verify(token);//验证令牌
            return true;//放行请求
         catch (TokenExpiredException e) 
            e.printStackTrace();
            map.put("msg","token过期");
         catch (AlgorithmMismatchException e) 
            e.printStackTrace();
            map.put("msg","token算法不一致");
         catch (SignatureVerificationException e) 
            e.printStackTrace();
            map.put("msg","无效签名");
         catch (Exception e) 
            e.printStackTrace();
            map.put("msg","token无效");
        
        map.put("state",false);//设置状态
        //将map转为json
        String s = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(s);
        return false;
    

//增加拦截器
public class InterceptorConfig implements WebMvcConfigurer 

    @Override
    public void addInterceptors(InterceptorRegistry registry) 
        registry.addInterceptor(new JWTInterceptor())
                .addPathPatterns("user/test") //其他接口token验证
                .excludePathPatterns("user/login"); //放行用户操作
    

JWT( JSON Web Token )的 实践,以及与 Session 对比

技术文章第一时间送达!

源码精品专栏

 
  •  69 篇

  •  61 篇



来源:http://t.cn/Rexnkj2

  • session

  • 无状态登录

  • Json Web Token

  • 应用

  • 无状态 VS 有状态

  • 总结


Json Web Token 是 rfc7519 出的一份标准,使用 JSON 来传递数据,用于判定用户是否登录状态。

jwt 之前,使用 session 来做用户认证。

以下代码均使用 javascript 编写。 
但是,对于 Java 也是想通的。

session

传统登录的方式是使用 session + token

token 是指在客户端使用 token 作为用户状态凭证,浏览器一般存储在 localStorage 或者 cookie 中。

session 是指在服务器端使用 redis 或者 sql 类数据库,存储 user_id 以及 token 的键值对关系,基本工作原理如下。

const sessions = {
  "ABCED1"10086,
  "CDEFA0"10010
}

// 通过 token 获取 user_id, 完成认证过程
function getUserIdByToken (token{
  return sessions[token]
}

如果存储在 cookie 中就是经常听到的 session + cookie 的登录方案。其实存储在 cookielocalStorage 甚至 IndexedDB 或者 WebSQL 各有利弊,核心思想一致。

关于 cookie 以及 token 优缺点,在 token authetication vs cookies 中有讨论。

如果不使用 cookie,可以采取 localStorage + Authorization 的方式进行认证。

// http 的头,每次请求权限接口时,需要携带 Authorization Header
const headers = {
  Authorization`Bearer ${localStorage.get('token')}`
}

推荐一个库 localForage,使用 IndexedDBWebSQL 以及 IndexedDB 做键值对存储。

无状态登录

session 需要在数据库中保持用户及token对应信息,所以叫 有状态。

试想一下,如何在数据库中不保持用户状态也可以登录。

第一种方法: 前端直接传 user_id 给服务端

缺点也特别特别明显,容易被用户篡改成任务 user_id,权限设置形同虚设。不过思路正确,接着往下走。

改进: 对 user_id 进行对称加密

比上边略微强点,如果说上一种方法是空窗户,这种方法就是糊了纸的窗户。

改进: 对 user_id 不需要加密,只需要进行签名,保证不被篡改

这便是 jwt 的思想,user_id,加密算法和签名一起存储到客户端,每次请求接口时,服务器判断签名是否一致。

Json Web Token

  • jwt.io

jwt 由 HeaderPayload 以及 Signature 由 . 拼接而成。

Header

Header 由非对称加密算法和类型组成,如下

const header = {
  // 加密算法
  alg: 'HS256',
  type'jwt'
}

Payload

Payload 中由 Registered Claim 以及需要通信的数据组成。这些数据字段也叫 Claim

Registered Claim 中比较重要的是 "exp" Claim 表示过期时间,在用户登录时会设置过期时间。

const payload = {
  // 表示 jwt 创建时间
  iat: 1532135735,

  // 表示 jwt 过期时间
  exp: 1532136735,

  // 用户 id,用以通信
  user_id: 10086
}

Signature

Sign 由 HeaderPayload 以及 secretOrPrivateKey 计算而成。

对于 secretOrPrivateKey,如果加密算法采用 HMAC,则为字符串,如果采用 RSA 或者 ECDSA,则为 PrivateKey。

// 由 HMACSHA256 算法进行签名,secret 不能外泄
const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)

// jwt 由三部分拼接而成
const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign

从生成 jwt 规则可知客户端可以解析出 payload,因此不要在 payload 中携带敏感数据,比如用户密码

校验

在生成规则中可知,jwt 前两部分是对 header 以及 payload 的 base64 编码。

当服务器收到客户端的 token 后,解析前两部分得到 header 以及 payload,并使用 header 中的算法与 secretOrPrivateKey 进行签名,判断与 jwt 中的签名是否一致。

如何判断 token 过期?

应用

由上可知,jwt 并不对数据进行加密,而是对数据进行签名,保证不被篡改。除了在登录中可以用到,在进行邮箱校验和图形验证码也可以用到。

图形验证码

在登录时,输入密码错误次数过多会出现图形验证码。

图形验证码的原理是给客户端一个图形,并且在服务器端保存与这个图片配对的字符串,以前也大都通过 session 来实现。

可以把验证码配对的字符串作为 secret,进行无状态校验。

const jwt = require('jsonwebtoken')

// 假设验证码为字符验证码,字符为 ACDE,10分钟失效
const token = jwt.sign({ userId10085 }, secrect + 'ACDE', { expiresIn60 * 10 })

邮箱校验

现在网站在注册成功后会进行邮箱校验,具体做法是给邮箱发一个链接,用户点开链接校验成功。

// 把邮箱以及用户id绑定在一起
const code = jwt.sign({ email, userId }, secret, { expiresIn60 * 30 })

// 在此链接校验验证码
const link = `https://example.com/code=${code}`

无状态 VS 有状态

关于无状态和有状态,在其它技术方向也有对比,比如 React 的 stateLess component 以及 stateful component,函数式编程中的副作用可以理解为状态,http 也是一个无状态协议,需要靠 header 以及 cookie 携带状态。

在用户认证这里,有无状态是指是否依赖外部数据存储,如 mysql,redis 等。

思考以下几个关于登录的问题如何使用 session 以及 jwt 实现

当用户注销时,如何使该 token 失效

因为 jwt 无状态,不保存用户设备信息,没法单纯使用它完成以上问题,可以再利用数据库保存一些状态完成。

  • session: 只需要把 user_id 对应的 token 清掉即可

  • jwt: 使用 redis,维护一张黑名单,用户注销时加入黑名单(签名),过期时间与 jwt 的过期时间保持一致。

如何允许用户只能在一个设备登录,如微信

  • session: 使用 sql 类数据库,对用户数据库表添加 token 字段并加索引,每次登陆重置 token 字段,每次请求需要权限接口时,根据 token 查找 user_id

  • jwt: 假使使用 sql 类数据库,对用户数据库表添加 token 字段(不需要添加索引),每次登陆重置 token 字段,每次请求需要权限接口时,根据 jwt 获取 user_id,根据 user_id 查用户表获取 token 判断 token 是否一致。另外也可以使用计数器的方法,如下一个问题。

对于这个需求,session 稍微简单些,毕竟 jwt 也需要依赖数据库。

如何允许用户只能在最近五个设备登录,如诸多播放器

  • session: 使用 sql 类数据库,创建 token 数据库表,有 id, token, user_id 三个字段,user 与 token 表为 1:m 关系。每次登录添加一行记录。根据 token 获取 user_id,再根据 user_id 获取该用户有多少设备登录,超过 5 个,则删除最小 id 一行。

  • jwt: 使用计数器,使用 sql 类数据库,在用户表中添加字段 count,默认值为 0,每次登录 count 字段自增1,每次登录创建的 jwt 的 Payload 中携带数据 current_count 为用户的 count 值。每次请求权限接口时,根据 jwt 获取 count 以及 current_count,根据 user_id 查用户表获取 count,判断与 current_count 差值是否小于 5

对于这个需求,jwt 略简单些,而使用 session 还需要多维护一张 token 表。

如何允许用户只能在最近五个设备登录,而且使某一用户踢掉除现有设备外的其它所有设备,如诸多播放器

  • session: 在上一个问题的基础上,删掉该设备以外其它所有的token记录。

  • jwt: 在上一个问题的基础上,对 count + 5,并对该设备重新赋值为新的 count。

如何显示该用户登录设备列表 / 如何踢掉特定用户

  • session: 在 token 表中新加列 device

  • jwt: 需要服务器端保持设备列表信息,做法与 session 一样,使用 jwt 意义不大

总结

从以上问题得知,如果不需要控制登录设备数量以及设备信息,无状态的 jwt 是一个不错的选择。一旦涉及到了设备信息,就需要对 jwt 添加额外的状态支持,增加了认证的复杂度,此时选用 session 是一个不错的选择。

jwt 不是万能的,是否采用 jwt,需要根据业务需求来确定。




已在知识星球更新源码解析如下:

JWT( JSON Web Token )的 实践,以及与 Session 对比


源码不易↓↓↓↓

【好看】支持老艿艿↓↓

以上是关于JWT技术--JSON Web Token的主要内容,如果未能解决你的问题,请参考以下文章

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

JSON Web Token (JWT) RFC7519

JWT技术--JSON Web Token

JSON Web Token(JWT)

JSON Web Token(JWT)