likes
comments
collection
share

JWT从入门到入土

作者站长头像
站长
· 阅读数 20

JWT介绍

JWT (JSON Web Token) 是一种开放标准,用于在网络应用间传递信息的一种方式。它由三个部分组成,通过点号连接:头部(Header)、载荷(Payload)和签名(Signature) ,头部包含了关于令牌的元数据,如算法类型和令牌类型。载荷包含了要传递的声明信息,如用户身份、权限等。签名是对头部和载荷进行签名的结果,用于验证令牌的真实性和完整性。

它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用机密(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。 虽然可以对 JWT 进行加密,以便在各方之间提供保密性,但是我们将关注已签名的Token。签名Token可以验证其中包含的声明的完整性,而加密Token可以向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,该签名还证明只有持有私钥的一方才是对其进行签名的一方( 签名技术是保证传输的信息不可抵赖,并不能保证信息传输的安全 )。

JWT的结构

JWT 的结构由三部分组成,它们通过点号(.)进行分隔。这三部分分别是头部(Header)、载荷(Payload)和签名(Signature)。

  1. 头部(Header):头部通常由两部分组成,它们被 Base64 编码后放置在 JWT 的第一个部分。第一部分是令牌的类型(typ),一般为"JWT"。第二部分是指定使用的签名算法(alg),例如"HMAC SHA256"或"RSA"等。

示例头部:

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

**

  1. 载荷(Payload):载荷也是由两部分组成的 Base64 编码的字符串,放置在 JWT 的第二部分。第一部分是一系列声明(Claims),用于描述关于 JWT 的信息,如过期时间(exp)、颁发者(iss)、用户身份等。第二部分是自定义的声明,用于存储应用程序需要的其他数据。

示例载荷:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

**

  1. 签名(Signature):签名是对头部和载荷应用签名算法后的结果。它用于验证 JWT 的真实性和完整性,确保 JWT 在传输过程中没有被篡改。签名的生成需要使用一个密钥(秘密或公开的),密钥只有服务端知道。

签名示例:

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

**

这是一个典型的 JWT 结构:xxx.yyy.zzz,其中 “xxx” 是经过 Base64 编码的头部,“yyy” 是经过 Base64 编码的载荷,“zzz” 是签名。

虽然 JWT 中的内容可以被解码,但签名部分使用了密钥,确保了令牌的真实性和完整性,并阻止了对令牌的篡改。

JWT如何生成与解析

JWT生成:

  1. 定义一个头部(Header),包含了令牌的类型(typ)和使用的签名算法(alg)。通常是一个 JSON 对象。
  2. 定义一个载荷(Payload),包含了要传递的声明信息,例如用户身份、权限等。通常是一个 JSON 对象。
  3. 使用 Base64 编码对头部和载荷进行编码,并通过点号连接得到 JWT 的第一部分。
  4. 使用指定的算法将编码后的头部和载荷以及一个密钥进行签名,生成签名(Signature)。签名可以确保令牌的真实性和完整性。
  5. 将签名以及第一部分的 JWT 内容进行连接,得到最终的 JWT。

代码演示:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtExample {
    public static void main(String[] args) {
        // 定义密钥
        String secretKey = "your_secret_key";

        // 定义过期时间
        long expirationTime = 3600000; // 1小时

        // 定义当前时间
        long currentTime = System.currentTimeMillis();

        // 定义 payload(载荷),包含需要传递的声明信息
        Claims claims = Jwts.claims();
        claims.put("sub", "1234567890");
        claims.put("name", "John Doe");
        claims.put("iat", currentTime);

        // 生成 JWT
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(currentTime))
                .setExpiration(new Date(currentTime + expirationTime))
                .signWith(SignatureAlgorithm.HS256, secretKey);

        String token = builder.compact();
        System.out.println("Generated Token: " + token);
    }
}

JWT解析:

  1. 使用点号对 JWT 进行分割,得到头部(Header)、载荷(Payload)和签名(Signature)三部分的内容。
  2. 对头部和载荷进行 Base64 解码,得到原始的头部和载荷内容。
  3. 使用密钥和指定的算法对原始的头部和载荷进行签名验证。通过验证签名,可以确定 JWT 的真实性和完整性。
  4. 如果签名验证通过,可以将解码后的头部和载荷内容进行进一步处理和使用。

代码演示:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

public class JwtExample {
    public static void main(String[] args) {
        // 定义密钥
        String secretKey = "your_secret_key";

        // 定义要解析的 JWT
        String token = "your_generated_token_here";

        // 解析 JWT
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)
                .getBody();

        System.out.println("Decoded Token: " + claims);
    }
}

JWT访问API流程

  1. 注册应用程序:首先,你需要在服务器端注册应用程序,以便获取相应的客户端密钥(Client Secret)和客户端标识符(Client ID)。这些凭据用于对应用程序进行身份验证和授权。
  2. 获取访问令牌:在客户端发起请求时,需要提供认证凭据(如客户端密钥、客户端标识符和用户名密码)。通过向身份认证服务器(如 OAuth 2.0 提供商)发送身份验证请求,可以获取访问令牌(Access Token)和可选的刷新令牌(Refresh Token)。
  3. 发送访问请求:在请求 API 资源时,需要将访问令牌作为身份验证凭证附加到请求的头部(或查询参数)中。通常使用"Authorization"头部,其值为"Bearer"加上访问令牌,如:“Authorization: Bearer <access_token>”。
  4. 验证令牌:服务端接收到请求后,会解析 JWT,校验签名以验证令牌的真实性和完整性。它会使用事先与服务器共享的密钥进行解码和签名验证。同时,还会检查令牌中的声明信息,如过期时间、颁发者等,来确保令牌是有效的。
  5. 授权验证:在验证令牌的基础上,服务端可能还会对令牌的持有者进行授权验证。这可能涉及到检查用户的权限、角色等信息,来决定用户是否有权访问特定的 API 资源。
  6. 响应请求:如果令牌有效且授权验证通过,服务端会返回请求所需的数据或执行相应的操作。响应通常包含请求的 API 资源的数据或其他指示信息。

SpringBoot整合JWT案例

项目采用SpringBoot+mybatis实现,业务逻辑非常简单,业务流程图如下:

JWT从入门到入土

以下的关键代码来展示整个流程

第一步: Spring Boot 框架的 JwtTokenAdminInterceptor 拦截器,用于校验 JWT 令牌的有效性。

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:{}", empId);
            //将用户id存储到ThreadLocal
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
 }

第二步:

  1. 创建一个实现了 HandlerInterceptor 接口的自定义拦截器类,例如 CustomInterceptor
@Component
public class CustomInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 在请求处理之前执行的逻辑
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 请求处理之后,视图渲染之前执行的逻辑
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求处理完成后执行的逻辑,即在视图渲染完成后执行,通常用于资源清理等操作
    }
}
  1. 在你的配置类(例如 Spring Boot 的配置类)中进行注册。
@Configuration
public class MyConfiguration implements WebMvcConfigurer {

    @Autowired
    private CustomInterceptor customInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(customInterceptor)
                .addPathPatterns("/**"); // 添加拦截的路径规则
    }
}

这样,当你的应用启动时,自定义拦截器将会被注册,可以拦截指定的请求并执行相应的逻辑。

第三步:

实现登录接口,我们从请求体中获取用户名和密码,然后调用 UserService 的方法进行用户登录验证。 如果登录验证成功,我们使用 JwtUtil.generateToken(user.getId()) 生成 JWT 令牌。

最后,将 JWT 令牌以响应头的方式返回给客户端,同时返回状态码为200(HttpStatus.OK)表示登录成功。 如果验证失败,则返回状态码为401(HttpStatus.UNAUTHORIZED)表示无效的凭证。

@RestController
@RequestMapping("/api")
public class AuthController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
        // 从请求体中获取用户名和密码
        String username = loginRequest.getUsername();
        String password = loginRequest.getPassword();

        // 调用UserService的方法进行登录验证
        User user = userService.login(username, password);

        if (user != null) {
            // 登录成功,生成JWT令牌
            String token = JwtUtil.generateToken(user.getId());

            // 将令牌以响应头的方式返回给客户端
            HttpHeaders headers = new HttpHeaders();
            headers.add("Authorization", "Bearer " + token);
            return new ResponseEntity<>("Login successful", headers, HttpStatus.OK);
        } else {
            return new ResponseEntity<>("Invalid credentials", HttpStatus.UNAUTHORIZED);
        }
    }
}

为什么使用JWT

由于 JSON 没有 XML 那么冗长,所以当对它进行编码时,它的大小也更小,这使得 JWT 比 SAML 更加紧凑。这使得 JWT 成为在 HTML 和 HTTP 环境中传递的一个很好的选择。 在安全性方面,SWT 只能由使用 HMAC 算法的共享秘密对称签名。但是,JWT 和 SAML Token可以使用 X.509证书形式的公钥/私钥对进行签名。与签名 JSON 的简单性相比,使用 XML 数字签名,签名 XML 而不引入模糊的安全漏洞是非常困难的。 JSON 解析器在大多数编程语言中都很常见,因为它们直接映射到对象。相反,XML 没有自然的文档到对象映射。这使得使用 JWT 比使用 SAML 断言更容易。 关于使用,JWT 是在 Internet 规模上使用的。这突出了在多个平台(尤其是移动平台)上对 JSON Web 令牌进行客户端处理的便利性。

JWT有什么好处

  1. 简单和轻量:JWT使用JSON格式表示信息,非常简洁和易于理解。它的体积相对较小,可以轻松在网络间传输。
  2. 无状态:JWT是无状态的,即服务器不需要在存储用户状态信息。每个请求都携带JWT,服务器可以根据JWT验证用户身份和权限,无需查询数据库或其他状态存储。
  3. 跨域支持:由于JWT是通过URL或HTTP头传输的,因此可以轻松跨域传输。
  4. 可扩展性:JWT使用一个JSON对象来存储用户声明和其他相关信息,这使得它非常灵活和可扩展。您可以自定义声明字段以满足您的业务需求。
  5. 安全性:JWT使用签名对数据进行验证和完整性保护。服务器可以使用密钥对JWT进行加密和签名,并确保接收到的JWT没有被篡改。
  6. 可在多种应用场景中使用:JWT广泛应用于Web应用、移动应用和服务间通信等场景,如身份验证、单点登录(SSO)、API访问控制等。