JWT(JSON Web Token)

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
.cn 域名,1个 12个月
简介: JWT(JSON Web Token)

常见的认证方式

HTTP Basic Auth

HTTP Basic Auth 一种最古老的安全认证方式,这种方式就是简单的访问API的时候,带上访问的username和password,由于信息会暴露出去,所以现在也越来越少用了。

Cookie + Session

服务端验证后,创建一个 Session 信息,服务端存储 Session信息,并且将 SessionID 存到 cookie,发送回浏览器。下次客户端再发起请求,自动带上 cookie 信息,服务端通过 cookie 的 SessionID 获取 Session 信息进行校验。

Token

基于 token 的鉴权机制类似于 HTTP 协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。第一次登陆成功后,服务端返回给客户端一个 token 值,客户端存储token,并在每次请求时附送上这个 token 值,服务端通过解析 token 的值判断用户的合法性。

JWT

JWT 是 token 的一种优化,把数据直接放在 token 中,然后对 token 加密,服务端获取token后,解密就可以获取客户端信息,不需要再去数据库查询客户端信息了。

什么是 Cookie

HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。

cookie 存储在客户端:cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。

cookie 是不可跨域的:每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。

什么是 Session

session 是另一种记录服务器和客户端会话状态的机制。session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中。image.pngsession 认证流程:

  • 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session。
  • 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器。
  • 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。
  • 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

Cookie 和 Session 的区别

  • 安全性:Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
  • 有效期不同:Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
  • 存储大小不同:单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

什么是 Token(令牌)

token 是客户端访问服务端时所需要的资源凭证。客户端每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里。基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库。token 完全由应用管理,所以它可以避开同源策略。image.pngtoken 的身份验证流程:

  • 客户端使用用户名跟密码请求登录。
  • 服务端收到请求,去验证用户名与密码。
  • 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端。
  • 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里。
  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 token。
  • 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据。

JWT 介绍

JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案,是一种认证授权机制。JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。

JWT 格式

JWT 的数据结构如下图所示:image.png它是一个很长的字符串,中间用点 .分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下:

Header(头部)
Payload(负载)
Signature(签名)

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子:

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用:

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_。这就是 Base64URL 算法。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用 . 分隔,就可以返回给用户。

JWT 的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

Authorization: Bearer <jwt token>

服务端收到 JWT Token 后,使用密钥进行解密,就可以得到客户端的相应信息了,不需要再去数据库查询客户端信息。

Token 和 JWT

  • 相同:
  • 都是访问资源的令牌。
  • 都可以记录用户的信息。
  • 都是使服务端无状态化。
  • 都是只有验证成功后,客户端才能访问服务端上受保护的资源。
  • 区别:
  • Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
  • JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT 自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。

JWT 实现

github地址:https://github.com/cr7258/jwt-lab, 本例使用 JJWT(Java JWT)来创建和验证 JSON Web Token(JWT)。

添加依赖

创建一个 Maven 项目并添加相关依赖:

<dependencies>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use
             RSASSA-PSS (PS256, PS384, PS512) algorithms.  JDK 11 or later does not require it for those algorithms:
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.60</version>
            <scope>runtime</scope>
        </dependency>
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
    </dependencies>

编写过滤器

编写过滤器,对请求验证 token:

package com.chengzw.filter;
import com.chengzw.util.JwtService;
import io.jsonwebtoken.Claims;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 过滤器,判断请求是否包含token
 * @author 程治玮
 * @since 2021/5/3 10:48 上午
 */
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request =(HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        response.setCharacterEncoding("utf-8");
        String token = request.getHeader("authorization"); //获取请求传来的token
        if( token == null){
            response.getWriter().write("请携带token");
            return;
        }
        Claims claims = JwtService.parsePersonJWT(token); //验证token
        if (claims == null) {
            response.getWriter().write("请携带token");
        }else {
            filterChain.doFilter(request,response);
        }
    }
}

注册过滤器

注册过滤器,并添加需要过滤的 URI 路径 /user/hello:

package com.chengzw.conf;
import com.chengzw.filter.MyFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * 过滤器进行注册,并添加需要过滤的路径 /user/hello
 * @author 程治玮
 * @since 2021/5/3 10:37 上午
 */
@Configuration
public class BeanRegisterConfig {
    @Bean
    public FilterRegistrationBean createFilterBean() {
        //过滤器注册类
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new MyFilter());
        registration.addUrlPatterns("/user/hello"); //需要过滤的接口
        return registration;
    }
}

编写JWT基础工具类

JWT 基础工具类包含两个部分:创建 JWT 和解析 JWT,JWS 是加密签名后的 JWT ,创建 JWS 主要有如下四步:

String jws = Jwts.builder() // 创建 JwtBulder 实例
    .setSubject("Bob")      // 添加 Header 参数和声明
    .signWith(key)          // 指定希望对 JWT 签名的密钥(可以是对称密钥,也可以是非对称密钥的私钥)
    .compact();             // 压缩和签名

具体实现代码:

package com.chengzw.util;
import io.jsonwebtoken.*;
import java.security.Key;
import java.util.Date;
import java.util.Map;
/**
 * JWT基础工具类
 * @author 程治玮
 * @since 2021/5/3 10:43 上午
 */
public class JwtUtils {
    /**
     * jwt解密,需要密钥和token,如果解密失败,说明token无效
     * @param jsonWebToken
     * @param signingKey
     * @return
     */
    public static Claims parseJWT(String jsonWebToken, Key signingKey) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(signingKey)
                    .parseClaimsJws(jsonWebToken)
                    .getBody();
            return claims;
        } catch (JwtException ex) {
            return null;
        }
    }
    /**
     * jwt = 头部(至少指定算法) + 身体(JWT编码的所有声明) + 签名(将标题和正文的组合通过标题中指定的算法计算得出)
     * jws:JWT可以加密签名成为jws
     * 创建token
     * @param map 主题,也差不多是个人的一些信息,为了好的移植,采用了map放个人信息,而没有采用JSON
     * @param audience 发送谁
     * @param issuer 个人签名
     * @param jwtId 相当于jwt的主键,不能重复
     * @param TTLMillis Token过期时间
     * @param signingKey 生成签名密钥
     * @return
     */
    public static String createJWT(Map map, String audience, String issuer, String jwtId, long TTLMillis, Key signingKey,SignatureAlgorithm signatureAlgorithm) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        //添加构成JWT的参数
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                .setIssuedAt(now)
                .setSubject(map.toString())
                .setIssuer(issuer)
                .setId(jwtId)
                .setAudience(audience)
                .signWith(signingKey, signatureAlgorithm);  //设置签名使用的签名算法和签名使用的秘钥
        //添加Token过期时间
        if (TTLMillis >= 0) {
            // 过期时间
            long expMillis = nowMillis + TTLMillis;
            // 现在是什么时间
            Date exp = new Date(expMillis);
            // 系统时间之前的token都是不可以被承认的
            builder.setExpiration(exp).setNotBefore(now);
        }
        //生成JWS(加密后的JWT)
        return builder.compact();
    }
}

封装 JWT 基础工具类

对 JWT 基础工具类进行二次封装,提供加密和解密的方法:

package com.chengzw.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.Map;
import java.util.UUID;
/**
 * JWT服务类,对JwtUtils进行二次封装,提供加密和解密的方法
 * @author 程治玮
 * @since 2021/5/3 10:43 上午
 */
public class JwtService {
    /**
     * token 过期时间, 单位: 秒. 这个值表示 30 天
     */
    private static final long TOKEN_EXPIRED_TIME = 30 * 24 * 60 * 60;
    /**
     * jwt 加密解密密钥
     */
    //签名密钥算法
    private static SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    //生成签名密钥
    //方式一:自己定义加密解密密钥
    //private static String JWT_SECRET = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=";
    //private static byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(JWT_SECRET);
    //private static Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
    //方式二:传入签名算法,自动生成密钥
    private static Key signingKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    /**
     * 个人签名
     */
    private static final String JWT_ISSUER = "CZW";
    /**
     * 描述:创建令牌
     *
     * @param map      主题,也差不多是个人的一些信息,为了好的移植,采用了map放个人信息,而没有采用JSON
     * @param audience 发送谁
     * @return java.lang.String
     */
    public static String createPersonToken(Map map, String audience) {
        String personToken = JwtUtils.createJWT(map, audience, UUID.randomUUID().toString(), JWT_ISSUER, TOKEN_EXPIRED_TIME, signingKey, signatureAlgorithm);
        return personToken;
    }
    /**
     * 描述:解密JWT
     *
     * @param personToken JWT字符串,也就是token字符串
     * @return io.jsonwebtoken.Claims
     */
    public static Claims parsePersonJWT(String personToken) {
        Claims claims = JwtUtils.parseJWT(personToken, signingKey);
        return claims;
    }
}

编写 Controller 入口类

Controller 类提供用户访问的入口:

package com.chengzw.controller;
import com.chengzw.util.JwtService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
 * @author 程治玮
 * @since 2021/5/3 10:45 上午
 */
@RestController
public class LoginController {
    //需要token验证才能访问
    @RequestMapping("user/hello")
    public String user(){
        return "hello";
    }
    //获取token
    @RequestMapping("user/token")
    public String token(){
        Map<String, Object> map = new HashMap<>();
        map.put("name", "chengzw");
        map.put("age", 21);
        return JwtService.createPersonToken(map, "chengzw");
    }
}

接口测试

第一次直接请求 /user/hello,会提示我们需要携带 token:

❯ curl http://localhost:8080/user/hello
请携带token

获取 token:

❯ curl http://localhost:8080/user/token
#返回token
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MjAwMTIwMTEsInN1YiI6IntuYW1lPWNoZW5nencsIGFnZT0yMX0iLCJpc3MiOiIwOGIxMDFjNC1hMmFjLTQ1OWQtYjU2ZS0wM2FkZTk2OWIwODYiLCJqdGkiOiJDWlciLCJhdWQiOiJjaGVuZ3p3IiwiZXhwIjoxNjIwMDE0NjAzLCJuYmYiOjE2MjAwMTIwMTF9.ZKX5Z3Acajg57MUQJZqFPWVpPbAGBIDiGigm4FgwmqM

然后在请求 Header 中带上 token 就可以成功访问了:

❯ curl http://localhost:8080/user/hello  -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MjAwMTIwMTEsInN1YiI6IntuYW1lPWNoZW5nencsIGFnZT0yMX0iLCJpc3MiOiIwOGIxMDFjNC1hMmFjLTQ1OWQtYjU2ZS0wM2FkZTk2OWIwODYiLCJqdGkiOiJDWlciLCJhdWQiOiJjaGVuZ3p3IiwiZXhwIjoxNjIwMDE0NjAzLCJuYmYiOjE2MjAwMTIwMTF9.ZKX5Z3Acajg57MUQJZqFPWVpPbAGBIDiGigm4FgwmqM"
#返回结果
hello

参考链接


目录
相关文章
|
3月前
|
XML JSON 前端开发
【Web前端揭秘】XML与JSON:数据界的双雄对决,你的选择将如何改写Web世界的未来?
【8月更文挑战第26天】本文深入探讨了XML和JSON这两种广泛使用的数据交换格式在Web前端开发中的应用。XML采用自定义标签描述数据结构,适用于复杂层次数据的表示,而JSON则以键值对形式呈现数据,更为轻量且易解析。通过对两种格式的示例代码、结构特点及应用场景的分析,本文旨在帮助读者更好地理解它们的差异,并根据实际需求选择最合适的数据交换格式。
60 1
|
3天前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
14 1
|
2月前
|
存储 中间件 API
ThinkPHP 集成 jwt 技术 token 验证
本文介绍了在ThinkPHP框架中集成JWT技术进行token验证的流程,包括安装JWT扩展、创建Token服务类、编写中间件进行Token校验、配置路由中间件以及测试Token验证的步骤和代码示例。
ThinkPHP 集成 jwt 技术 token 验证
|
1月前
|
存储 JSON 前端开发
JSON与现代Web开发:数据交互的最佳选择
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也便于机器解析和生成。它以文本格式存储数据,常用于Web应用中的数据传输,尤其是在客户端和服务器之间。
44 0
|
2月前
|
JSON 算法 安全
Web安全-JWT认证机制安全性浅析
Web安全-JWT认证机制安全性浅析
30 2
|
2月前
|
JSON 安全 数据安全/隐私保护
从0到1搭建权限管理系统系列三 .net8 JWT创建Token并使用
【9月更文挑战第22天】在.NET 8中,从零开始搭建权限管理系统并使用JWT(JSON Web Tokens)创建Token是关键步骤。JWT是一种开放标准(RFC 7519),用于安全传输信息,由头部、载荷和签名三部分组成。首先需安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,并在`Program.cs`中配置JWT服务。接着,创建一个静态方法`GenerateToken`生成包含用户名和角色的Token。最后,在控制器中使用`[Authorize]`属性验证和解析Token,从而实现身份验证和授权功能。
129 3
|
3月前
|
Java Spring 容器
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
51 0
|
3月前
|
JSON Java API
解码Spring Boot与JSON的完美融合:提升你的Web开发效率,实战技巧大公开!
【8月更文挑战第29天】Spring Boot作为Java开发的轻量级框架,通过`jackson`库提供了强大的JSON处理功能,简化了Web服务和数据交互的实现。本文通过代码示例介绍如何在Spring Boot中进行JSON序列化和反序列化操作,并展示了处理复杂JSON数据及创建RESTful API的方法,帮助开发者提高效率和应用性能。
141 0
|
3月前
|
API
【Azure Developer】记录一段验证AAD JWT Token时需要设置代理获取openid-configuration内容
【Azure Developer】记录一段验证AAD JWT Token时需要设置代理获取openid-configuration内容
|
3月前
|
JSON Java API
【Azure Developer】如何验证 Azure AD的JWT Token (JSON Web 令牌)?
【Azure Developer】如何验证 Azure AD的JWT Token (JSON Web 令牌)?