JWT从入门到入土

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"等。

示例头部:

json 复制代码
{
  "typ": "JWT",
  "alg": "HS256"
}

**

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

示例载荷:

json 复制代码
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

**

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

签名示例:

scss 复制代码
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。

代码演示:

java 复制代码
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. 如果签名验证通过,可以将解码后的头部和载荷内容进行进一步处理和使用。

代码演示:

typescript 复制代码
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实现,业务逻辑非常简单,业务流程图如下:

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

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

java 复制代码
/**
 * 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
java 复制代码
@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 的配置类)中进行注册。
typescript 复制代码
@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)表示无效的凭证。

less 复制代码
@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访问控制等。
相关推荐
缺点内向3 小时前
Java:创建、读取或更新 Excel 文档
java·excel
带刺的坐椅3 小时前
Solon v3.4.7, v3.5.6, v3.6.1 发布(国产优秀应用开发框架)
java·spring·solon
四谎真好看5 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程5 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t5 小时前
ZIP工具类
java·zip
lang201509286 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan6 小时前
第10章 Maven
java·maven
百锦再7 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说7 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多7 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring